// Copyright 2022 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
////////////////////////////////////////////////////////////////////////////////

import com.code_intelligence.jazzer.api.FuzzedDataProvider;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.http.HttpMethod;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.security.web.firewall.StrictHttpFirewall;
import org.springframework.security.web.firewall.RequestRejectedException;
import com.code_intelligence.jazzer.api.FuzzerSecurityIssueMedium;
import java.util.function.Predicate;
import java.util.Enumeration;
import java.util.regex.Pattern;
import java.util.regex.Matcher;
import java.util.ArrayList;
import java.util.List;
import jakarta.servlet.http.HttpServletRequest;
import java.math.BigInteger;

public class StrictHttpFirewallFuzzer {
    record Header(String n, String v) {};

    // https://github.com/spring-projects/spring-security/blob/main/web/src/main/java/org/springframework/security/web/firewall/StrictHttpFirewall.java#L127
    private static Pattern ASSIGNED_AND_NOT_ISO_CONTROL_PATTERN = Pattern.compile("[\\p{IsAssigned}||[\\p{IsControl}]]*");
    private static Predicate<String> ASSIGNED_AND_NOT_ISO_CONTROL_PREDICATE = (s) -> ASSIGNED_AND_NOT_ISO_CONTROL_PATTERN.matcher(s).matches();

    public static void fuzzerTestOneInput(FuzzedDataProvider data) throws Exception {
        StrictHttpFirewall firewall = new StrictHttpFirewall();
	    MockHttpServletRequest request = new MockHttpServletRequest("GET", "");

        boolean invalidMethod = data.consumeBoolean();

        if (invalidMethod) {
            request.setMethod(data.consumeString(50));
        }

        List<Header> maliciousHeaders = new ArrayList<Header>();
        List<String> maliciousParameterNames = new ArrayList<String>();
        List<String> maliciousUrls = new ArrayList<String>();

        // Feed fuzzer data into the request
        for (int i = 0; i < data.consumeInt(1, 5); i++) {
            String url = data.consumeString(400);
            switch (data.consumeInt(0, 5)) {
                case 0:
                    request.setPathInfo(url);
                    maliciousUrls.add(url);
                    break;
                case 1:
                    request.setContextPath(url);
                    maliciousUrls.add(url);
                    break;
                case 2:
                    request.setRequestURI(url);
                    maliciousUrls.add(url);
                    break;
                case 3:
                    request.setServletPath(url);
                    maliciousUrls.add(url);
                    break;
                case 4:
                    String parameterName = data.consumeAsciiString(100).trim();
                    if (parameterName.isEmpty()) {
                        break;
                    }
                    request.addParameter(parameterName, "Dummy value");
                    maliciousParameterNames.add(parameterName);
                    break;
                case 5:
                    Header header = new Header(data.consumeString(100), data.consumeString(100));
                    if (header.v().isEmpty() || header.n().isEmpty()) {
                        break;
                    }
                    request.addHeader(header.n(), header.v());
                    maliciousHeaders.add(header);
            }
        }

        HttpServletRequest servletRequest;
        try {
            servletRequest = firewall.getFirewalledRequest(request);

            // getHeader() and getParameter() should throw a rejection exception if it contains invalid chars
            for (Header header : maliciousHeaders) {
                servletRequest.getHeader(header.n());
            }

            for (String parameterName : maliciousParameterNames) {
                servletRequest.getParameter(parameterName);
            }

        } catch (RequestRejectedException e) {
            return;
        }

        for (String forbiddenChar : firewall.getEncodedUrlBlocklist()) {
            if (request.getPathInfo() != null && request.getPathInfo().contains(forbiddenChar)
                || request.getRequestURI() != null && request.getRequestURI().contains(forbiddenChar)
                || request.getContextPath() != null && request.getContextPath().contains(forbiddenChar)
                || request.getServletPath() != null && request.getServletPath().contains(forbiddenChar)) {
                throw new FuzzerSecurityIssueMedium("Malicious char not filtered: " + forbiddenChar);
            }
        }

        for (Header header : maliciousHeaders) {
            validate(header.n());
            String v = servletRequest.getHeader(header.n());
            validate(v);
        }

        for (String name : maliciousParameterNames) {
            validate(name);
        }
    }

    // Wrapper for validateAllowedHeaderName, validateAllowedHeaderValue, validateAllowedParameterName & validateAllowedParameterValue 
    private static void validate(String value) {
        if (!value.isEmpty() && ASSIGNED_AND_NOT_ISO_CONTROL_PREDICATE.test(value)) {
            throw new FuzzerSecurityIssueMedium("Malicious char not filtered: " + value);
        }
    }
} 

@@ -1360,16 +1360,98 @@
 Integer;
+%0Aimport java.nio.charset.StandardCharsets;%0Aimport java.lang.IllegalStateException;
 %0A%0Apublic
@@ -1531,433 +1531,8 @@
 %7D;%0A%0A
-    // https://github.com/spring-projects/spring-security/blob/main/web/src/main/java/org/springframework/security/web/firewall/StrictHttpFirewall.java#L127%0A    private static Pattern ASSIGNED_AND_NOT_ISO_CONTROL_PATTERN = Pattern.compile(%22%5B%5C%5Cp%7BIsAssigned%7D%7C%7C%5B%5C%5Cp%7BIsControl%7D%5D%5D*%22);%0A    private static Predicate%3CString%3E ASSIGNED_AND_NOT_ISO_CONTROL_PREDICATE = (s) -%3E ASSIGNED_AND_NOT_ISO_CONTROL_PATTERN.matcher(s).matches();%0A%0A
     
@@ -2959,13 +2959,8 @@
 sume
-Ascii
 Stri
@@ -2970,15 +2970,8 @@
 100)
-.trim()
 ;%0A  
@@ -4163,16 +4163,40 @@
 ception 
+%7C IllegalStateException 
 e) %7B%0A   
@@ -5121,272 +5121,388 @@
  // 
-Wrapper
+Check
  for 
+in
 valid
-ateAllowedHeaderName, validateAllowedHeaderValue, validateAllowedParameterName & validateAllowedParameterValue %0A    private static void validate(String value) %7B%0A        if (!value.isEmpty() && ASSIGNED_AND_NOT_ISO_CONTROL_PREDICATE.test(value
+ chars%0A    // https://docs.spring.io/spring-security/site/docs/current/api/org/springframework/security/web/firewall/StrictHttpFirewall.html#setAllowedHeaderNames(java.util.function.Predicate)%0A    private static void validate(String value) %7B%0A        for (char c : value.toCharArray()) %7B%0A            if (Character.isISOControl(c) %7C%7C !Character.isDefined(c
 )) %7B%0A
+    
     
@@ -5579,19 +5579,85 @@
 ed: 
-%22 + value);
+%5C%5Cx%22 + String.format(%22%2504x%22, (int) c) + %22 in %60%22 + value + %22%60%22);%0A            %7D
 %0A   

fix malicious char detection on StrictHttpFirewallFuzzer (#8630)