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