From adc4af6d09587429404252ceb2315f55670d4846 Mon Sep 17 00:00:00 2001 From: Alex O'Ree Date: Sun, 23 Aug 2026 15:20:56 -0400 Subject: [PATCH] JSPWIKI-1298 see jira for details --- .../http/filter/CsrfProtectionFilter.java | 21 ++++++++++++++++++- .../http/filter/ReferrerPolicyFilter.java | 4 +++- .../wiki/ajax/WikiAjaxDispatcherServlet.java | 7 ++++++- .../wiki/attachment/AttachmentServlet.java | 15 +++++++++++++ .../webapp/templates/210/AttachmentTab.jsp | 5 +++-- .../templates/default/AttachmentTab.jsp | 5 +++-- 6 files changed, 50 insertions(+), 7 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 cc15c05e27..cd43cf5a5d 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 @@ Licensed to the Apache Software Foundation (ASF) under one import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import java.io.IOException; +import java.util.Locale; /** @@ -54,7 +55,7 @@ public void init( final FilterConfig filterConfig ) { /** {@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 @@ static boolean isPost( final HttpServletRequest request ) { 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-http/src/main/java/org/apache/wiki/http/filter/ReferrerPolicyFilter.java b/jspwiki-http/src/main/java/org/apache/wiki/http/filter/ReferrerPolicyFilter.java index 8067664a6a..7176614812 100644 --- a/jspwiki-http/src/main/java/org/apache/wiki/http/filter/ReferrerPolicyFilter.java +++ b/jspwiki-http/src/main/java/org/apache/wiki/http/filter/ReferrerPolicyFilter.java @@ -32,7 +32,9 @@ */ public class ReferrerPolicyFilter implements Filter { - private String mode = "no-referrer-when-downgrade"; + // Default to same-origin: wiki URLs can carry sensitive query parameters, so the URL must never reach a + // third-party destination via the Referer header. Deployments may still override via ReferrerPolicyPValue. + private String mode = "same-origin"; /** {@inheritDoc} */ @Override diff --git a/jspwiki-main/src/main/java/org/apache/wiki/ajax/WikiAjaxDispatcherServlet.java b/jspwiki-main/src/main/java/org/apache/wiki/ajax/WikiAjaxDispatcherServlet.java index 5883971f40..05ad4fa649 100644 --- a/jspwiki-main/src/main/java/org/apache/wiki/ajax/WikiAjaxDispatcherServlet.java +++ b/jspwiki-main/src/main/java/org/apache/wiki/ajax/WikiAjaxDispatcherServlet.java @@ -140,7 +140,12 @@ private void performAction( final HttpServletRequest req, final HttpServletRespo res.setCharacterEncoding( m_engine.getContentEncoding().displayName() ); final String actionName = AjaxUtil.getNextPathPart( req.getRequestURI(), servlet.getServletMapping() ); if (!(servlet instanceof DefaultSearchManager.PluginSearch)) { - final String xsrfToken = req.getParameter("X-XSRF-TOKEN"); + // Prefer the request header: a token sent as a GET parameter ends up in access logs and + // browser history. The parameter is still accepted for compatibility with older clients. + String xsrfToken = req.getHeader("X-XSRF-TOKEN"); + if (xsrfToken == null) { + xsrfToken = req.getParameter("X-XSRF-TOKEN"); + } if (!wikiSession.antiCsrfToken().equals(xsrfToken)) { res.sendError(400, "X-XSRF-TOKEN missing or invalid."); WikiEventManager.fireEvent(this, 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 bfae2a0ac0..fa41de7e21 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 @@ -51,6 +51,7 @@ Licensed to the Apache Software Foundation (ASF) under one 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; @@ -406,6 +407,7 @@ protected String upload( final HttpServletRequest req ) throws RedirectException 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( !JakartaServletFileUpload.isMultipartContent(req) ) { @@ -463,12 +465,25 @@ protected String upload( final HttpServletRequest req ) throws RedirectException case "nextpage": nextPage = validateNextPage( item.getString( StandardCharsets.UTF_8 ), errorPage ); break; + case CsrfProtectionFilter.ANTICSRF_PARAM: + csrfToken = item.getString( StandardCharsets.UTF_8 ); + 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 (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", nextPage ); diff --git a/jspwiki-war/src/main/webapp/templates/210/AttachmentTab.jsp b/jspwiki-war/src/main/webapp/templates/210/AttachmentTab.jsp index a549ebeb16..e9d85846de 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); %> -

-