|
19 | 19 | import java.util.Arrays;
|
20 | 20 | import java.util.Collection;
|
21 | 21 | import java.util.Collections;
|
| 22 | +import java.util.Enumeration; |
22 | 23 | import java.util.HashSet;
|
23 | 24 | import java.util.List;
|
| 25 | +import java.util.Map; |
24 | 26 | import java.util.Set;
|
25 | 27 | import java.util.function.Predicate;
|
| 28 | +import java.util.regex.Pattern; |
26 | 29 | import javax.servlet.http.HttpServletRequest;
|
27 | 30 | import javax.servlet.http.HttpServletResponse;
|
28 | 31 |
|
|
74 | 77 | * Rejects hosts that are not allowed. See
|
75 | 78 | * {@link #setAllowedHostnames(Predicate)}
|
76 | 79 | * </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> |
77 | 96 | * </ul>
|
78 | 97 | *
|
79 | 98 | * @see DefaultHttpFirewall
|
@@ -111,6 +130,18 @@ public class StrictHttpFirewall implements HttpFirewall {
|
111 | 130 |
|
112 | 131 | private Predicate<String> allowedHostnames = hostname -> true;
|
113 | 132 |
|
| 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 | + |
114 | 145 | public StrictHttpFirewall() {
|
115 | 146 | urlBlocklistsAddAll(FORBIDDEN_SEMICOLON);
|
116 | 147 | urlBlocklistsAddAll(FORBIDDEN_FORWARDSLASH);
|
@@ -330,6 +361,77 @@ public void setAllowUrlEncodedPercent(boolean allowUrlEncodedPercent) {
|
330 | 361 | }
|
331 | 362 | }
|
332 | 363 |
|
| 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 | + |
333 | 435 | /**
|
334 | 436 | * <p>
|
335 | 437 | * Determines which hostnames should be allowed. The default is to allow any hostname.
|
@@ -370,6 +472,144 @@ public FirewalledRequest getFirewalledRequest(HttpServletRequest request) throws
|
370 | 472 | throw new RequestRejectedException("The requestURI was rejected because it can only contain printable ASCII characters.");
|
371 | 473 | }
|
372 | 474 | 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 | + |
373 | 613 | @Override
|
374 | 614 | public void reset() {
|
375 | 615 | }
|
|
0 commit comments