You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@jspwiki.apache.org by ju...@apache.org on 2026/09/25 14:45:33 UTC

(jspwiki) 29/33: JSPWIKI-1298 see jira for details

This is an automated email from the ASF dual-hosted git repository.

juanpablo-santos pushed a commit to branch master-2.x
in repository https://gitbox.apache.org/repos/asf/jspwiki.git

commit 3bf036a99b1dc4b4e84b596cba9ce410dd7123ee
Author: Alex O'Ree <al...@apache.org>
AuthorDate: Sun Aug 23 15:20:56 2026 -0400

    JSPWIKI-1298 see jira for details
---
 .../wiki/http/filter/CsrfProtectionFilter.java      | 21 ++++++++++++++++++++-
 .../apache/wiki/attachment/AttachmentServlet.java   | 15 +++++++++++++++
 .../src/main/webapp/templates/210/AttachmentTab.jsp |  5 +++--
 .../main/webapp/templates/default/AttachmentTab.jsp |  5 +++--
 4 files changed, 41 insertions(+), 5 deletions(-)

diff --git a/jspwiki-http/src/main/java/org/apache/wiki/http/filter/CsrfProtectionFilter.java b/jspwiki-http/src/main/java/org/apache/wiki/http/filter/CsrfProtectionFilter.java
index c9d25dde2..eb7b8d5cf 100644
--- a/jspwiki-http/src/main/java/org/apache/wiki/http/filter/CsrfProtectionFilter.java
+++ b/jspwiki-http/src/main/java/org/apache/wiki/http/filter/CsrfProtectionFilter.java
@@ -33,6 +33,7 @@ import javax.servlet.ServletResponse;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 import java.io.IOException;
+import java.util.Locale;
 
 
 /**
@@ -54,7 +55,7 @@ public class CsrfProtectionFilter implements Filter {
     /** {@inheritDoc} */
     @Override
     public void doFilter( final ServletRequest request, final ServletResponse response, final FilterChain chain ) throws IOException, ServletException {
-        if( isPost( ( HttpServletRequest ) request ) ) {
+        if( isPost( ( HttpServletRequest ) request ) && !isMultipartAttachmentUpload( ( HttpServletRequest ) request ) ) {
             final Engine engine = Wiki.engine().find( request.getServletContext(), null );
             final Session session = Wiki.session().find( engine, ( HttpServletRequest ) request );
             if( !requestContainsValidCsrfToken( request, session ) ) {
@@ -84,6 +85,24 @@ public class CsrfProtectionFilter implements Filter {
         return "POST".equalsIgnoreCase( request.getMethod() );
     }
 
+    /**
+     * Multipart uploads carry their anti-CSRF token in the request body, which {@code getParameter()} cannot read
+     * without consuming the stream. Requiring the token as a request parameter would force it into the URL query
+     * string, where it leaks into access logs, proxies and browser history. For the attachment servlet - the only
+     * multipart consumer, which parses the body itself and performs the same token check before acting on the
+     * request - the check is therefore delegated instead of applied here. Multipart POSTs to any other path keep
+     * being validated by this filter, so the body trick cannot be used to smuggle query-string parameters past it.
+     *
+     * @param request the inbound request
+     * @return true if this is a multipart POST for the attachment servlet
+     */
+    private static boolean isMultipartAttachmentUpload( final HttpServletRequest request ) {
+        final String contentType = request.getContentType();
+        return "/attach".equals( request.getServletPath() )
+                && contentType != null
+                && contentType.toLowerCase( Locale.ENGLISH ).startsWith( "multipart/" );
+    }
+
     /** {@inheritDoc} */
     @Override
     public void destroy() {
diff --git a/jspwiki-main/src/main/java/org/apache/wiki/attachment/AttachmentServlet.java b/jspwiki-main/src/main/java/org/apache/wiki/attachment/AttachmentServlet.java
index 9868683b2..d0c481f2c 100644
--- a/jspwiki-main/src/main/java/org/apache/wiki/attachment/AttachmentServlet.java
+++ b/jspwiki-main/src/main/java/org/apache/wiki/attachment/AttachmentServlet.java
@@ -38,6 +38,7 @@ import org.apache.wiki.api.providers.WikiProvider;
 import org.apache.wiki.api.spi.Wiki;
 import org.apache.wiki.auth.AuthorizationManager;
 import org.apache.wiki.auth.permissions.PermissionFactory;
+import org.apache.wiki.http.filter.CsrfProtectionFilter;
 import org.apache.wiki.i18n.InternationalizationManager;
 import org.apache.wiki.preferences.Preferences;
 import org.apache.wiki.ui.progress.ProgressItem;
@@ -412,6 +413,7 @@ public class AttachmentServlet extends HttpServlet {
         final String errorPage = m_engine.getURL( ContextEnum.WIKI_ERROR.getRequestContext(), "", null ); // If something bad happened, Upload should be able to take care of most stuff
         String nextPage = errorPage;
         final String progressId = req.getParameter( "progressid" );
+        String csrfToken = req.getParameter( CsrfProtectionFilter.ANTICSRF_PARAM );
 
         // Check that we have a file upload request
         if( !ServletFileUpload.isMultipartContent(req) ) {
@@ -460,12 +462,25 @@ public class AttachmentServlet extends HttpServlet {
                     case "nextpage":
                         nextPage = validateNextPage( item.getString( StandardCharsets.UTF_8.name() ), errorPage );
                         break;
+                    case CsrfProtectionFilter.ANTICSRF_PARAM:
+                        csrfToken = item.getString( StandardCharsets.UTF_8.name() );
+                        break;
                     }
                 } else {
                     fileItems.add( item );
                 }
             }
 
+            // The CsrfProtectionFilter delegates multipart requests to this servlet, since it cannot read the
+            // token from a multipart body without consuming it. Enforce the token here - taken from the multipart
+            // form field written by <wiki:CsrfProtection/> (or, for older clients, the query string) - before
+            // acting on anything else in the request, including the "Broken file upload" redirect below.
+            final Session session = Wiki.session().find( m_engine, req );
+            if( csrfToken == null || !csrfToken.equals( session.antiCsrfToken() ) ) {
+                LOG.error( "Incorrect {} received for {}", CsrfProtectionFilter.ANTICSRF_PARAM, req.getPathInfo() );
+                throw new RedirectException( "Missing or invalid CSRF token", errorPage );
+            }
+
             if(fileItems.isEmpty()) {
                 throw new RedirectException( "Broken file upload", errorPage );
 
diff --git a/jspwiki-war/src/main/webapp/templates/210/AttachmentTab.jsp b/jspwiki-war/src/main/webapp/templates/210/AttachmentTab.jsp
index c732ca2b3..63aaaa140 100644
--- a/jspwiki-war/src/main/webapp/templates/210/AttachmentTab.jsp
+++ b/jspwiki-war/src/main/webapp/templates/210/AttachmentTab.jsp
@@ -31,12 +31,13 @@
   Context c = Context.findContext(pageContext);
 %>
 <c:set var="progressId" value="<%= c.getEngine().getManager( ProgressManager.class ).getNewProgressIdentifier() %>" />
-<c:set var="csrfProtection" value="<%= c.getWikiSession().antiCsrfToken() %>" />
 
 <div id="addattachment">
 <h3><fmt:message key="attach.add"/></h3>
 <wiki:Permission permission="upload">
-  <form action="<wiki:Link jsp='attach' format='url'><wiki:Param name='progressid' value='${progressId}'/><wiki:Param name='X-XSRF-TOKEN' value='${csrfProtection}'/></wiki:Link>"
+  <%-- The anti-CSRF token travels as a hidden multipart form field (see <wiki:CsrfProtection/> below), NOT as a
+       query-string parameter: query strings end up in access logs, proxies and browser history. --%>
+  <form action="<wiki:Link jsp='attach' format='url'><wiki:Param name='progressid' value='${progressId}'/></wiki:Link>"
          class="wikiform"
             id="uploadform"
         method="post"
diff --git a/jspwiki-war/src/main/webapp/templates/default/AttachmentTab.jsp b/jspwiki-war/src/main/webapp/templates/default/AttachmentTab.jsp
index 29016589c..abc4f8017 100644
--- a/jspwiki-war/src/main/webapp/templates/default/AttachmentTab.jsp
+++ b/jspwiki-war/src/main/webapp/templates/default/AttachmentTab.jsp
@@ -33,11 +33,12 @@
   Context c = Context.findContext(pageContext);
 %>
 <c:set var="progressId" value="<%= c.getEngine().getManager( ProgressManager.class ).getNewProgressIdentifier() %>" />
-<c:set var="csrfProtection" value="<%= c.getWikiSession().antiCsrfToken() %>" />
 <div class="page-content">
 <wiki:Permission permission="upload">
 
-  <form action="<wiki:Link jsp='attach' format='url'><wiki:Param name='progressid' value='${progressId}'/><wiki:Param name='X-XSRF-TOKEN' value='${csrfProtection}'/></wiki:Link>"
+  <%-- The anti-CSRF token travels as a hidden multipart form field (see <wiki:CsrfProtection/> below), NOT as a
+       query-string parameter: query strings end up in access logs, proxies and browser history. --%>
+  <form action="<wiki:Link jsp='attach' format='url'><wiki:Param name='progressid' value='${progressId}'/></wiki:Link>"
          class="accordion<wiki:HasAttachments></wiki:HasAttachments>"
             id="uploadform"
         method="post"