Skip to content

Commit 3f80348

Browse files
ChristopherSchultzmarkt-asf
authored andcommitted
Avoid adding multiple CSRF tokens to a URL (#923)
* Avoid adding multiple CSRF tokens to a URL * Add javadoc * Handle special case where query-string contains a ?
1 parent 817bf0a commit 3f80348

File tree

3 files changed

+189
-0
lines changed

3 files changed

+189
-0
lines changed

java/org/apache/catalina/filters/CsrfPreventionFilter.java

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -552,6 +552,8 @@ public String encodeRedirectUrl(String url) {
552552

553553
@Override
554554
public String encodeRedirectURL(String url) {
555+
url = removeQueryParameters(url, nonceRequestParameterName);
556+
555557
if (shouldAddNonce(url)) {
556558
return addNonce(super.encodeRedirectURL(url));
557559
} else {
@@ -567,6 +569,8 @@ public String encodeUrl(String url) {
567569

568570
@Override
569571
public String encodeURL(String url) {
572+
url = removeQueryParameters(url, nonceRequestParameterName);
573+
570574
if (shouldAddNonce(url)) {
571575
return addNonce(super.encodeURL(url));
572576
} else {
@@ -588,6 +592,85 @@ private boolean shouldAddNonce(String url) {
588592
return true;
589593
}
590594

595+
/**
596+
* Removes zero or more query parameters from a URL. All instances of the query parameter and any associated
597+
* values will be removed.
598+
*
599+
* @param url The URL whose query parameters should be removed.
600+
* @param parameterName The name of the parameter to remove.
601+
*
602+
* @return The URL without any instances of the query parameter <code>parameterName</code> present.
603+
*/
604+
public static String removeQueryParameters(String url, String parameterName) {
605+
if (null != parameterName) {
606+
// Check for query string
607+
int q = url.indexOf('?');
608+
if (q > -1) {
609+
610+
// Look for parameter end
611+
int start = q + 1;
612+
int pos = url.indexOf('&', start);
613+
614+
int iterations = 0;
615+
616+
while (-1 < pos) {
617+
// Process all parameters
618+
if (++iterations > 100000) {
619+
// Just in case things get out of control
620+
throw new IllegalStateException("Way too many loop iterations");
621+
}
622+
623+
int eq = url.indexOf('=', start);
624+
int paramNameEnd;
625+
if (-1 == eq || eq > pos) {
626+
paramNameEnd = pos;
627+
// Found no equal sign at all or past next & ; Parameter is all name.
628+
} else {
629+
// Found this param's equal sign
630+
paramNameEnd = eq;
631+
}
632+
if (parameterName.equals(url.substring(start, paramNameEnd))) {
633+
// Remove the parameter
634+
url = url.substring(0, start) + url.substring(pos + 1); // +1 to consume the &
635+
} else {
636+
start = pos + 1; // Go to next parameter
637+
}
638+
pos = url.indexOf('&', start);
639+
}
640+
641+
// Check final parameter
642+
String paramName;
643+
pos = url.indexOf('=', start);
644+
645+
if (-1 < pos) {
646+
paramName = url.substring(start, pos);
647+
} else {
648+
paramName = url.substring(start);
649+
// No value
650+
}
651+
if (paramName.equals(parameterName)) {
652+
// Remove this parameter
653+
654+
// Remove any trailing ? or & as well
655+
char c = url.charAt(start - 1);
656+
if ('?' == c || '&' == c) {
657+
start--;
658+
}
659+
660+
url = url.substring(0, start);
661+
} else {
662+
// Remove trailing ? if it's there. Is this worth it?
663+
int length = url.length();
664+
if (length == q + 1) {
665+
url = url.substring(0, length - 1);
666+
}
667+
}
668+
}
669+
}
670+
671+
return url;
672+
}
673+
591674
/*
592675
* Return the specified URL with the nonce added to the query string.
593676
*

test/org/apache/catalina/filters/TestCsrfPreventionFilter.java

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525
import java.util.Collections;
2626
import java.util.Iterator;
2727
import java.util.function.Predicate;
28+
import java.util.regex.Matcher;
29+
import java.util.regex.Pattern;
2830

2931
import javax.servlet.http.HttpServletResponse;
3032

@@ -196,6 +198,106 @@ public void testNoNonceMimeMatcher() {
196198
Assert.assertEquals("/foo/images/home.jpg", response.encodeURL("/foo/images/home.jpg"));
197199
}
198200

201+
@Test
202+
public void testMultipleTokens() {
203+
String nonce = "TESTNONCE";
204+
String testURL = "/foo/bar?" + Constants.CSRF_NONCE_SESSION_ATTR_NAME + "=sample";
205+
CsrfPreventionFilter.CsrfResponseWrapper response = new CsrfPreventionFilter.CsrfResponseWrapper(new NonEncodingResponse(),
206+
Constants.CSRF_NONCE_SESSION_ATTR_NAME, nonce, null);
207+
208+
Assert.assertTrue("Original URL does not contain CSRF token",
209+
testURL.contains(Constants.CSRF_NONCE_SESSION_ATTR_NAME));
210+
211+
String result = response.encodeURL(testURL);
212+
213+
int pos = result.indexOf(Constants.CSRF_NONCE_SESSION_ATTR_NAME);
214+
Assert.assertTrue("Result URL does not contain CSRF token",
215+
pos >= 0);
216+
pos = result.indexOf(Constants.CSRF_NONCE_SESSION_ATTR_NAME, pos + 1);
217+
Assert.assertFalse("Result URL contains multiple CSRF tokens: " + result,
218+
pos >= 0);
219+
}
220+
221+
@Test
222+
public void testURLCleansing() {
223+
String[] urls = new String[] {
224+
"/foo/bar",
225+
"/foo/bar?",
226+
"/foo/bar?csrf",
227+
"/foo/bar?csrf&",
228+
"/foo/bar?csrf=",
229+
"/foo/bar?csrf=&",
230+
"/foo/bar?csrf=abc",
231+
"/foo/bar?csrf=abc&bar=foo",
232+
"/foo/bar?bar=foo&csrf=abc",
233+
"/foo/bar?bar=foo&csrf=abc&foo=bar",
234+
"/foo/bar?csrfx=foil&bar=foo&csrf=abc&foo=bar",
235+
"/foo/bar?csrfx=foil&bar=foo&csrf=abc&foo=bar&csrf=def",
236+
"/foo/bar?csrf=&csrf&csrf&csrf&csrf=abc&csrf=",
237+
"/foo/bar?xcsrf=&xcsrf&xcsrf&xcsrf&xcsrf=abc&xcsrf=",
238+
"/foo/bar?xcsrf=&xcsrf&xcsrf&csrf=foo&xcsrf&xcsrf=abc&csrf=bar&xcsrf=&",
239+
"/foo/bar?bar=fo?&csrf=abc",
240+
"/foo/bar?bar=fo?&csrf=abc&baz=boh",
241+
};
242+
243+
String csrfParameterName = "csrf";
244+
245+
for(String url : urls) {
246+
String result = CsrfPreventionFilter.CsrfResponseWrapper.removeQueryParameters(url, csrfParameterName);
247+
248+
Assert.assertEquals("Failed to cleanse URL '" + url + "' properly", dumbButAccurateCleanse(url, csrfParameterName), result);
249+
}
250+
251+
}
252+
253+
private static String dumbButAccurateCleanse(String url, String csrfParameterName) {
254+
// Get rid of [&csrf=value] with optional =value
255+
Pattern p = Pattern.compile(Pattern.quote("&") + Pattern.quote(csrfParameterName) + "(=[^&]*)?(&|$)");
256+
257+
// Do this iteratively to get everything
258+
Matcher m = p.matcher(url);
259+
260+
while (m.find()) {
261+
url = m.replaceFirst("$2");
262+
m = p.matcher(url);
263+
}
264+
265+
// Get rid of [?csrf=value] with optional =value
266+
url = url.replaceAll(Pattern.quote("?") + csrfParameterName + "(=[^&]*)?(&|$)", "?");
267+
268+
if (url.endsWith("?")) {
269+
// Special case: it's possible to have multiple ? in a URL:
270+
// the query-string is technically allowed to contain ? characters.
271+
// Only trim the trailing ? if it is actually the quest-string
272+
// separator.
273+
if(url.lastIndexOf('?', url.length() - 2) < 0) {
274+
url = url.substring(0, url.length() - 1);
275+
}
276+
}
277+
278+
return url;
279+
}
280+
281+
@Test
282+
public void testDuplicateTokens() {
283+
String nonce = "TESTNONCE";
284+
String testURL = "/foo/bar?" + Constants.CSRF_NONCE_SESSION_ATTR_NAME + "=" + nonce;
285+
CsrfPreventionFilter.CsrfResponseWrapper response = new CsrfPreventionFilter.CsrfResponseWrapper(new NonEncodingResponse(),
286+
Constants.CSRF_NONCE_SESSION_ATTR_NAME, nonce, null);
287+
288+
Assert.assertTrue("Original URL does not contain CSRF token",
289+
testURL.contains(Constants.CSRF_NONCE_SESSION_ATTR_NAME));
290+
291+
String result = response.encodeURL(testURL);
292+
293+
int pos = result.indexOf(Constants.CSRF_NONCE_SESSION_ATTR_NAME);
294+
Assert.assertTrue("Result URL does not contain CSRF token",
295+
pos >= 0);
296+
pos = result.indexOf(Constants.CSRF_NONCE_SESSION_ATTR_NAME, pos + 1);
297+
Assert.assertFalse("Result URL contains multiple CSRF tokens: " + result,
298+
pos >= 0);
299+
}
300+
199301
private static class MimeTypeServletContext extends TesterServletContext {
200302
private String mimeType;
201303

webapps/docs/changelog.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,10 @@
113113
failed when made from within a web application with resource caching
114114
enabled if the target resource was packaged in a JAR file. (markt)
115115
</fix>
116+
<fix>
117+
Pull request <pr>923</pr>: Avoid adding multiple CSRF tokens to a URL in
118+
the <code>CsrfPreventionFilter</code>. (schultz)
119+
</fix>
116120
</changelog>
117121
</subsection>
118122
<subsection name="Coyote">

0 commit comments

Comments
 (0)