Skip to content

Commit dfa9234

Browse files
committed
Validate headers and parameters in StrictHttpFirewall
Adds methods to configure validation of header names and values and parameter names and values: * setAllowedHeaderNames(Predicate) * setAllowedHeaderValues(Predicate) * setAllowedParameterNames(Predicate) * setAllowedParameterValues(Predicate) By default, header names, header values, and parameter names that contain ISO control characters or unassigned unicode characters are rejected. No parameter value validation is performed by default. Issue gh-8644
1 parent 88028d8 commit dfa9234

File tree

3 files changed

+445
-0
lines changed

3 files changed

+445
-0
lines changed

web/src/main/java/org/springframework/security/web/FilterInvocation.java

+62
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@
2323
import java.lang.reflect.InvocationHandler;
2424
import java.lang.reflect.Method;
2525
import java.lang.reflect.Proxy;
26+
import java.util.Collections;
27+
import java.util.Enumeration;
28+
import java.util.LinkedHashMap;
29+
import java.util.Map;
2630

2731
import javax.servlet.FilterChain;
2832
import javax.servlet.ServletRequest;
@@ -31,6 +35,7 @@
3135
import javax.servlet.http.HttpServletRequestWrapper;
3236
import javax.servlet.http.HttpServletResponse;
3337

38+
import org.springframework.http.HttpHeaders;
3439
import org.springframework.security.web.util.UrlUtils;
3540

3641
/**
@@ -161,6 +166,8 @@ class DummyRequest extends HttpServletRequestWrapper {
161166
private String pathInfo;
162167
private String queryString;
163168
private String method;
169+
private final HttpHeaders headers = new HttpHeaders();
170+
private final Map<String, String[]> parameters = new LinkedHashMap<>();
164171

165172
DummyRequest() {
166173
super(UNSUPPORTED_REQUEST);
@@ -232,6 +239,61 @@ public void setQueryString(String queryString) {
232239
public String getServerName() {
233240
return null;
234241
}
242+
243+
@Override
244+
public String getHeader(String name) {
245+
return this.headers.getFirst(name);
246+
}
247+
248+
@Override
249+
public Enumeration<String> getHeaders(String name) {
250+
return Collections.enumeration(this.headers.get(name));
251+
}
252+
253+
@Override
254+
public Enumeration<String> getHeaderNames() {
255+
return Collections.enumeration(this.headers.keySet());
256+
}
257+
258+
@Override
259+
public int getIntHeader(String name) {
260+
String value = this.headers.getFirst(name);
261+
if (value == null ) {
262+
return -1;
263+
}
264+
else {
265+
return Integer.parseInt(value);
266+
}
267+
}
268+
269+
public void addHeader(String name, String value) {
270+
this.headers.add(name, value);
271+
}
272+
273+
@Override
274+
public String getParameter(String name) {
275+
String[] arr = this.parameters.get(name);
276+
return (arr != null && arr.length > 0 ? arr[0] : null);
277+
}
278+
279+
@Override
280+
public Map<String, String[]> getParameterMap() {
281+
return Collections.unmodifiableMap(this.parameters);
282+
}
283+
284+
@Override
285+
public Enumeration<String> getParameterNames() {
286+
return Collections.enumeration(this.parameters.keySet());
287+
}
288+
289+
@Override
290+
public String[] getParameterValues(String name) {
291+
return this.parameters.get(name);
292+
}
293+
294+
public void setParameter(String name, String... values) {
295+
this.parameters.put(name, values);
296+
}
235297
}
236298

237299
final class UnsupportedOperationExceptionInvocationHandler implements InvocationHandler {

web/src/main/java/org/springframework/security/web/firewall/StrictHttpFirewall.java

+240
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,13 @@
1919
import java.util.Arrays;
2020
import java.util.Collection;
2121
import java.util.Collections;
22+
import java.util.Enumeration;
2223
import java.util.HashSet;
2324
import java.util.List;
25+
import java.util.Map;
2426
import java.util.Set;
2527
import java.util.function.Predicate;
28+
import java.util.regex.Pattern;
2629
import javax.servlet.http.HttpServletRequest;
2730
import javax.servlet.http.HttpServletResponse;
2831

@@ -74,6 +77,22 @@
7477
* Rejects hosts that are not allowed. See
7578
* {@link #setAllowedHostnames(Predicate)}
7679
* </li>
80+
* <li>
81+
* Reject headers names that are not allowed. See
82+
* {@link #setAllowedHeaderNames(Predicate)}
83+
* </li>
84+
* <li>
85+
* Reject headers values that are not allowed. See
86+
* {@link #setAllowedHeaderValues(Predicate)}
87+
* </li>
88+
* <li>
89+
* Reject parameter names that are not allowed. See
90+
* {@link #setAllowedParameterNames(Predicate)}
91+
* </li>
92+
* <li>
93+
* Reject parameter values that are not allowed. See
94+
* {@link #setAllowedParameterValues(Predicate)}
95+
* </li>
7796
* </ul>
7897
*
7998
* @see DefaultHttpFirewall
@@ -111,6 +130,18 @@ public class StrictHttpFirewall implements HttpFirewall {
111130

112131
private Predicate<String> allowedHostnames = hostname -> true;
113132

133+
private static final Pattern ASSIGNED_AND_NOT_ISO_CONTROL_PATTERN = Pattern.compile("[\\p{IsAssigned}&&[^\\p{IsControl}]]*");
134+
135+
private static final Predicate<String> ASSIGNED_AND_NOT_ISO_CONTROL_PREDICATE = s -> ASSIGNED_AND_NOT_ISO_CONTROL_PATTERN.matcher(s).matches();
136+
137+
private Predicate<String> allowedHeaderNames = ASSIGNED_AND_NOT_ISO_CONTROL_PREDICATE;
138+
139+
private Predicate<String> allowedHeaderValues = ASSIGNED_AND_NOT_ISO_CONTROL_PREDICATE;
140+
141+
private Predicate<String> allowedParameterNames = ASSIGNED_AND_NOT_ISO_CONTROL_PREDICATE;
142+
143+
private Predicate<String> allowedParameterValues = value -> true;
144+
114145
public StrictHttpFirewall() {
115146
urlBlocklistsAddAll(FORBIDDEN_SEMICOLON);
116147
urlBlocklistsAddAll(FORBIDDEN_FORWARDSLASH);
@@ -330,6 +361,77 @@ public void setAllowUrlEncodedPercent(boolean allowUrlEncodedPercent) {
330361
}
331362
}
332363

364+
/**
365+
* <p>
366+
* Determines which header names should be allowed.
367+
* The default is to reject header names that contain ISO control characters
368+
* and characters that are not defined.
369+
* </p>
370+
*
371+
* @param allowedHeaderNames the predicate for testing header names
372+
* @see Character#isISOControl(int)
373+
* @see Character#isDefined(int)
374+
* @since 5.4
375+
*/
376+
public void setAllowedHeaderNames(Predicate<String> allowedHeaderNames) {
377+
if (allowedHeaderNames == null) {
378+
throw new IllegalArgumentException("allowedHeaderNames cannot be null");
379+
}
380+
this.allowedHeaderNames = allowedHeaderNames;
381+
}
382+
383+
/**
384+
* <p>
385+
* Determines which header values should be allowed.
386+
* The default is to reject header values that contain ISO control characters
387+
* and characters that are not defined.
388+
* </p>
389+
*
390+
* @param allowedHeaderValues the predicate for testing hostnames
391+
* @see Character#isISOControl(int)
392+
* @see Character#isDefined(int)
393+
* @since 5.4
394+
*/
395+
public void setAllowedHeaderValues(Predicate<String> allowedHeaderValues) {
396+
if (allowedHeaderValues == null) {
397+
throw new IllegalArgumentException("allowedHeaderValues cannot be null");
398+
}
399+
this.allowedHeaderValues = allowedHeaderValues;
400+
}
401+
/*
402+
* Determines which parameter names should be allowed.
403+
* The default is to reject header names that contain ISO control characters
404+
* and characters that are not defined.
405+
* </p>
406+
*
407+
* @param allowedParameterNames the predicate for testing parameter names
408+
* @see Character#isISOControl(int)
409+
* @see Character#isDefined(int)
410+
* @since 5.4
411+
*/
412+
public void setAllowedParameterNames(Predicate<String> allowedParameterNames) {
413+
if (allowedParameterNames == null) {
414+
throw new IllegalArgumentException("allowedParameterNames cannot be null");
415+
}
416+
this.allowedParameterNames = allowedParameterNames;
417+
}
418+
419+
/**
420+
* <p>
421+
* Determines which parameter values should be allowed.
422+
* The default is to allow any parameter value.
423+
* </p>
424+
*
425+
* @param allowedParameterValues the predicate for testing parameter values
426+
* @since 5.4
427+
*/
428+
public void setAllowedParameterValues(Predicate<String> allowedParameterValues) {
429+
if (allowedParameterValues == null) {
430+
throw new IllegalArgumentException("allowedParameterValues cannot be null");
431+
}
432+
this.allowedParameterValues = allowedParameterValues;
433+
}
434+
333435
/**
334436
* <p>
335437
* Determines which hostnames should be allowed. The default is to allow any hostname.
@@ -370,6 +472,144 @@ public FirewalledRequest getFirewalledRequest(HttpServletRequest request) throws
370472
throw new RequestRejectedException("The requestURI was rejected because it can only contain printable ASCII characters.");
371473
}
372474
return new FirewalledRequest(request) {
475+
@Override
476+
public long getDateHeader(String name) {
477+
if (!allowedHeaderNames.test(name)) {
478+
throw new RequestRejectedException("The request was rejected because the header name \"" + name + "\" is not allowed.");
479+
}
480+
return super.getDateHeader(name);
481+
}
482+
483+
@Override
484+
public int getIntHeader(String name) {
485+
if (!allowedHeaderNames.test(name)) {
486+
throw new RequestRejectedException("The request was rejected because the header name \"" + name + "\" is not allowed.");
487+
}
488+
return super.getIntHeader(name);
489+
}
490+
491+
@Override
492+
public String getHeader(String name) {
493+
if (!allowedHeaderNames.test(name)) {
494+
throw new RequestRejectedException("The request was rejected because the header name \"" + name + "\" is not allowed.");
495+
}
496+
String value = super.getHeader(name);
497+
if (value != null && !allowedHeaderValues.test(value)) {
498+
throw new RequestRejectedException("The request was rejected because the header value \"" + value + "\" is not allowed.");
499+
}
500+
return value;
501+
}
502+
503+
@Override
504+
public Enumeration<String> getHeaders(String name) {
505+
if (!allowedHeaderNames.test(name)) {
506+
throw new RequestRejectedException("The request was rejected because the header name \"" + name + "\" is not allowed.");
507+
}
508+
509+
Enumeration<String> valuesEnumeration = super.getHeaders(name);
510+
return new Enumeration<String>() {
511+
@Override
512+
public boolean hasMoreElements() {
513+
return valuesEnumeration.hasMoreElements();
514+
}
515+
516+
@Override
517+
public String nextElement() {
518+
String value = valuesEnumeration.nextElement();
519+
if (!allowedHeaderValues.test(value)) {
520+
throw new RequestRejectedException("The request was rejected because the header value \"" + value + "\" is not allowed.");
521+
}
522+
return value;
523+
}
524+
};
525+
}
526+
527+
@Override
528+
public Enumeration<String> getHeaderNames() {
529+
Enumeration<String> namesEnumeration = super.getHeaderNames();
530+
return new Enumeration<String>() {
531+
@Override
532+
public boolean hasMoreElements() {
533+
return namesEnumeration.hasMoreElements();
534+
}
535+
536+
@Override
537+
public String nextElement() {
538+
String name = namesEnumeration.nextElement();
539+
if (!allowedHeaderNames.test(name)) {
540+
throw new RequestRejectedException("The request was rejected because the header name \"" + name + "\" is not allowed.");
541+
}
542+
return name;
543+
}
544+
};
545+
}
546+
547+
@Override
548+
public String getParameter(String name) {
549+
if (!allowedParameterNames.test(name)) {
550+
throw new RequestRejectedException("The request was rejected because the parameter name \"" + name + "\" is not allowed.");
551+
}
552+
String value = super.getParameter(name);
553+
if (value != null && !allowedParameterValues.test(value)) {
554+
throw new RequestRejectedException("The request was rejected because the parameter value \"" + value + "\" is not allowed.");
555+
}
556+
return value;
557+
}
558+
559+
@Override
560+
public Map<String, String[]> getParameterMap() {
561+
Map<String, String[]> parameterMap = super.getParameterMap();
562+
for (Map.Entry<String, String[]> entry : parameterMap.entrySet()) {
563+
String name = entry.getKey();
564+
String[] values = entry.getValue();
565+
if (!allowedParameterNames.test(name)) {
566+
throw new RequestRejectedException("The request was rejected because the parameter name \"" + name + "\" is not allowed.");
567+
}
568+
for (String value: values) {
569+
if (!allowedParameterValues.test(value)) {
570+
throw new RequestRejectedException("The request was rejected because the parameter value \"" + value + "\" is not allowed.");
571+
}
572+
}
573+
}
574+
return parameterMap;
575+
}
576+
577+
@Override
578+
public Enumeration<String> getParameterNames() {
579+
Enumeration<String> namesEnumeration = super.getParameterNames();
580+
return new Enumeration<String>() {
581+
@Override
582+
public boolean hasMoreElements() {
583+
return namesEnumeration.hasMoreElements();
584+
}
585+
586+
@Override
587+
public String nextElement() {
588+
String name = namesEnumeration.nextElement();
589+
if (!allowedParameterNames.test(name)) {
590+
throw new RequestRejectedException("The request was rejected because the parameter name \"" + name + "\" is not allowed.");
591+
}
592+
return name;
593+
}
594+
};
595+
}
596+
597+
@Override
598+
public String[] getParameterValues(String name) {
599+
if (!allowedParameterNames.test(name)) {
600+
throw new RequestRejectedException("The request was rejected because the parameter name \"" + name + "\" is not allowed.");
601+
}
602+
String[] values = super.getParameterValues(name);
603+
if (values != null) {
604+
for (String value: values) {
605+
if (!allowedParameterValues.test(value)) {
606+
throw new RequestRejectedException("The request was rejected because the parameter value \"" + value + "\" is not allowed.");
607+
}
608+
}
609+
}
610+
return values;
611+
}
612+
373613
@Override
374614
public void reset() {
375615
}

0 commit comments

Comments
 (0)