Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;


/**
Expand All @@ -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 ) ) {
Expand Down Expand Up @@ -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() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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) ) {
Expand Down Expand Up @@ -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 <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", nextPage );

Expand Down
5 changes: 3 additions & 2 deletions jspwiki-war/src/main/webapp/templates/210/AttachmentTab.jsp
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Loading