Skip to content

Commit c71352c

Browse files
candrewsjzheaux
authored andcommitted
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 c71352c

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

Lines changed: 62 additions & 0 deletions
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

Lines changed: 240 additions & 0 deletions
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)