Spring 5.0.3 RequestRejectedException: Die Anforderung wurde abgelehnt, da die URL nicht normalisiert wurde


85

Ich bin mir nicht sicher, ob dies ein Fehler in Spring 5.0.3 oder eine neue Funktion ist, mit der ich Probleme beheben kann.

Nach dem Upgrade wird dieser Fehler angezeigt. Interessanterweise ist dieser Fehler nur auf meinem lokalen Computer. Der gleiche Code in der Testumgebung mit dem HTTPS-Protokoll funktioniert einwandfrei.

Auch weiterhin...

Der Grund, warum ich diesen Fehler erhalte, ist, dass meine URL zum Laden der resultierenden JSP-Seite lautet /location/thisPage.jsp. Das Auswerten des Codes request.getRequestURI()gibt mir ein Ergebnis /WEB-INF/somelocation//location/thisPage.jsp. Wenn ich die URL der JSP-Seite darauf korrigiere location/thisPage.jsp, funktionieren die Dinge einwandfrei.

Meine Frage ist also, ob ich im Code /aus dem JSPPfad entfernen soll, da dies in Zukunft erforderlich ist. Oder Springhat einen Fehler eingeführt, da der einzige Unterschied zwischen meinem Computer und der Testumgebung das Protokoll im HTTPVergleich zum Protokoll ist HTTPS.

 org.springframework.security.web.firewall.RequestRejectedException: The request was rejected because the URL was not normalized.
    at org.springframework.security.web.firewall.StrictHttpFirewall.getFirewalledRequest(StrictHttpFirewall.java:123)
    at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:194)
    at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:186)
    at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:357)
    at org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:270)


1
Das Problem soll in 5.1.0 behoben werden. Derzeit hat 5.0.0 dieses Problem nicht.
Java_dude

Antworten:


64

In der Spring-Sicherheitsdokumentation wird der Grund für das Blockieren // in der Anforderung angegeben.

Beispielsweise kann es Pfadüberquerungssequenzen (wie /../) oder mehrere Schrägstriche (//) enthalten, die auch dazu führen können, dass Musterübereinstimmungen fehlschlagen. Einige Container normalisieren diese, bevor die Servlet-Zuordnung durchgeführt wird, andere jedoch nicht. Zum Schutz vor solchen Problemen verwendet FilterChainProxy eine HttpFirewall-Strategie, um die Anforderung zu überprüfen und zu verpacken. Nicht normalisierte Anforderungen werden standardmäßig automatisch abgelehnt, und Pfadparameter und doppelte Schrägstriche werden zu Abgleichszwecken entfernt.

Es gibt also zwei mögliche Lösungen -

  1. doppelten Schrägstrich entfernen (bevorzugter Ansatz)
  2. Erlauben Sie // in Spring Security, indem Sie die StrictHttpFirewall mit dem folgenden Code anpassen.

Schritt 1 Erstellen Sie eine benutzerdefinierte Firewall, die einen Schrägstrich in der URL ermöglicht.

@Bean
public HttpFirewall allowUrlEncodedSlashHttpFirewall() {
    StrictHttpFirewall firewall = new StrictHttpFirewall();
    firewall.setAllowUrlEncodedSlash(true);    
    return firewall;
}

Schritt 2 Konfigurieren Sie diese Bean anschließend in Websecurity

@Override
public void configure(WebSecurity web) throws Exception {
    //@formatter:off
    super.configure(web);
    web.httpFirewall(allowUrlEncodedSlashHttpFirewall());
....
}

Schritt 2 ist ein optionaler Schritt. Spring Boot benötigt lediglich eine Bean vom Typ HttpFirewall


Ja, die Pfadüberquerungssicherheit wurde eingeführt. Das ist eine neue Funktion, die das Problem verursacht haben könnte. Was ich nicht sicher bin, wie Sie sehen, funktioniert es auf HTTPS und nicht auf HTTP. Ich würde lieber warten, bis dieser Fehler behoben ist. Jira.spring.io/browse/SPR-16419
java_dude

Sehr wahrscheinlich Teil unseres Problems ... aber ... der Benutzer gibt kein // ein, also versuche ich herauszufinden, wie dieses zweite / überhaupt hinzugefügt wird ... wenn der Frühling unser generiert jstl url sollte es nicht hinzufügen oder normalisieren, nachdem es hinzugefügt wurde.
Xenoterracide

4
Dies löst die Lösung zumindest für Spring Security 5.1.1 nicht wirklich. Sie müssen DefaultHttpFirewall verwenden, wenn Sie URLs mit zwei Schrägstrichen wie a / b // c benötigen. Die isNormalized-Methode kann in StrictHttpFirewall nicht konfiguriert oder überschrieben werden.
Jason Winnebeck

Gibt es eine Chance, dass jemand Hinweise gibt, wie dies im Frühling allein im Gegensatz zu Boot zu tun ist?
Schoon

27

setAllowUrlEncodedSlash(true)hat bei mir nicht funktioniert. Noch interne Methode isNormalizedRückkehrfalse , wenn ein doppelter Schrägstrich mit.

I ersetzt StrictHttpFirewallmit DefaultHttpFirewallnur den folgenden Code aufweist:

@Bean
public HttpFirewall defaultHttpFirewall() {
    return new DefaultHttpFirewall();
}

Arbeitet gut für mich.
Irgendein Risiko durch die Verwendung DefaultHttpFirewall?


1
Ja. Nur weil Sie keinen Ersatzschlüssel für Ihren Mitbewohner erstellen können, heißt das nicht, dass Sie den einzigen Schlüssel unter die Fußmatte legen sollten. Nicht empfohlen. Sicherheit sollte nicht geändert werden.
Java_Dode

15
@java_dude Großartig, wie Sie überhaupt keine Informationen oder Gründe angegeben haben, nur eine vage Analogie.
Kaqqao

Eine andere Option ist die Unterklasse StrictHttpFirewall, um etwas mehr Kontrolle über die Ablehnung von URLs zu erhalten, wie in dieser Antwort beschrieben .
Vallismortis

1
Dies funktionierte für mich, aber ich musste dies auch in meine Bean XML hinzufügen:<sec:http-firewall ref="defaultHttpFirewall"/>
Jason Winnebeck

1
Welche Auswirkungen hat die Verwendung dieser Lösung?
Felipe Desiderati

10

Ich bin auf dasselbe Problem gestoßen mit:

Spring Boot-Version = 1.5.10
Spring Security-Version = 4.2.4


Das Problem trat an den Endpunkten auf, an denen der ModelAndViewAnsichtsname mit einem vorhergehenden Schrägstrich definiert wurde . Beispiel:

ModelAndView mav = new ModelAndView("/your-view-here");

Wenn ich den Schrägstrich entfernt habe, hat es gut funktioniert. Beispiel:

ModelAndView mav = new ModelAndView("your-view-here");

Ich habe auch einige Tests mit RedirectView durchgeführt und es schien mit einem vorhergehenden Schrägstrich zu funktionieren.


2
Das ist nicht die Lösung. Was wäre, wenn dies ein Fehler auf der Frühlingsseite wäre? Wenn sie es ändern, müssen Sie alle Änderungen erneut rückgängig machen. Ich würde lieber bis 5.1 warten, da es markiert ist, um bis dahin gelöst zu werden.
java_dude

1
Nein, Sie müssen die Änderung nicht zurücksetzen, da das Definieren von viewName ohne vorangestellten Schrägstrich in älteren Versionen problemlos funktioniert.
Torsten Ojaperv

Genau das ist das Problem. Wenn es gut funktioniert hat und Sie nichts geändert haben, hat Spring einen Fehler eingeführt. Der Pfad sollte immer mit "/" beginnen. Überprüfen Sie alle Federdokumentationen. Überprüfen Sie diese aus github.com/spring-projects/spring-security/issues/5007 & github.com/spring-projects/spring-security/issues/5044
java_dude

1
Das hat mich auch gebissen. Das Aktualisieren aller ModelAndView ohne das führende '/' hat das Problem behoben
Nathan Perrier

jira.spring.io/browse/SPR-16740 Ich habe einen Fehler geöffnet, aber das Entfernen des führenden / war für mich keine Lösung. In den meisten Fällen geben wir den Ansichtsnamen nur als Zeichenfolge zurück (vom Controller). . Sie müssen die Umleitungsansicht als Lösung betrachten.
Xenoterracide


3

In meinem Fall wurde das Upgrade von spring-securiy-web 3.1.3 auf 4.2.12 standardmäßig defaultHttpFirewallvon DefaultHttpFirewallauf geändert StrictHttpFirewall. Definieren Sie es einfach in der XML-Konfiguration wie folgt:

<bean id="defaultHttpFirewall" class="org.springframework.security.web.firewall.DefaultHttpFirewall"/>
<sec:http-firewall ref="defaultHttpFirewall"/>

einstellen HTTPFirewallalsDefaultHttpFirewall


1
Bitte fügen Sie Ihrem Code eine Beschreibung hinzu, in der erklärt wird, was los ist und warum. Dies ist eine gute Praxis. Wenn Sie dies nicht tun, besteht die Gefahr, dass Ihre Antwort gelöscht wird. Es wurde bereits als minderwertig gekennzeichnet.
Herrbischoff

2

Die folgende Lösung ist eine saubere Lösung. Sie beeinträchtigt nicht die Sicherheit, da wir dieselbe strenge Firewall verwenden.

Die Schritte zur Befestigung sind wie folgt:

SCHRITT 1: Erstellen Sie eine Klasse, die die StrictHttpFirewall wie folgt überschreibt .

package com.biz.brains.project.security.firewall;

import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.http.HttpMethod;
import org.springframework.security.web.firewall.DefaultHttpFirewall;
import org.springframework.security.web.firewall.FirewalledRequest;
import org.springframework.security.web.firewall.HttpFirewall;
import org.springframework.security.web.firewall.RequestRejectedException;

public class CustomStrictHttpFirewall implements HttpFirewall {
    private static final Set<String> ALLOW_ANY_HTTP_METHOD = Collections.unmodifiableSet(Collections.emptySet());

    private static final String ENCODED_PERCENT = "%25";

    private static final String PERCENT = "%";

    private static final List<String> FORBIDDEN_ENCODED_PERIOD = Collections.unmodifiableList(Arrays.asList("%2e", "%2E"));

    private static final List<String> FORBIDDEN_SEMICOLON = Collections.unmodifiableList(Arrays.asList(";", "%3b", "%3B"));

    private static final List<String> FORBIDDEN_FORWARDSLASH = Collections.unmodifiableList(Arrays.asList("%2f", "%2F"));

    private static final List<String> FORBIDDEN_BACKSLASH = Collections.unmodifiableList(Arrays.asList("\\", "%5c", "%5C"));

    private Set<String> encodedUrlBlacklist = new HashSet<String>();

    private Set<String> decodedUrlBlacklist = new HashSet<String>();

    private Set<String> allowedHttpMethods = createDefaultAllowedHttpMethods();

    public CustomStrictHttpFirewall() {
        urlBlacklistsAddAll(FORBIDDEN_SEMICOLON);
        urlBlacklistsAddAll(FORBIDDEN_FORWARDSLASH);
        urlBlacklistsAddAll(FORBIDDEN_BACKSLASH);

        this.encodedUrlBlacklist.add(ENCODED_PERCENT);
        this.encodedUrlBlacklist.addAll(FORBIDDEN_ENCODED_PERIOD);
        this.decodedUrlBlacklist.add(PERCENT);
    }

    public void setUnsafeAllowAnyHttpMethod(boolean unsafeAllowAnyHttpMethod) {
        this.allowedHttpMethods = unsafeAllowAnyHttpMethod ? ALLOW_ANY_HTTP_METHOD : createDefaultAllowedHttpMethods();
    }

    public void setAllowedHttpMethods(Collection<String> allowedHttpMethods) {
        if (allowedHttpMethods == null) {
            throw new IllegalArgumentException("allowedHttpMethods cannot be null");
        }
        if (allowedHttpMethods == ALLOW_ANY_HTTP_METHOD) {
            this.allowedHttpMethods = ALLOW_ANY_HTTP_METHOD;
        } else {
            this.allowedHttpMethods = new HashSet<>(allowedHttpMethods);
        }
    }

    public void setAllowSemicolon(boolean allowSemicolon) {
        if (allowSemicolon) {
            urlBlacklistsRemoveAll(FORBIDDEN_SEMICOLON);
        } else {
            urlBlacklistsAddAll(FORBIDDEN_SEMICOLON);
        }
    }

    public void setAllowUrlEncodedSlash(boolean allowUrlEncodedSlash) {
        if (allowUrlEncodedSlash) {
            urlBlacklistsRemoveAll(FORBIDDEN_FORWARDSLASH);
        } else {
            urlBlacklistsAddAll(FORBIDDEN_FORWARDSLASH);
        }
    }

    public void setAllowUrlEncodedPeriod(boolean allowUrlEncodedPeriod) {
        if (allowUrlEncodedPeriod) {
            this.encodedUrlBlacklist.removeAll(FORBIDDEN_ENCODED_PERIOD);
        } else {
            this.encodedUrlBlacklist.addAll(FORBIDDEN_ENCODED_PERIOD);
        }
    }

    public void setAllowBackSlash(boolean allowBackSlash) {
        if (allowBackSlash) {
            urlBlacklistsRemoveAll(FORBIDDEN_BACKSLASH);
        } else {
            urlBlacklistsAddAll(FORBIDDEN_BACKSLASH);
        }
    }

    public void setAllowUrlEncodedPercent(boolean allowUrlEncodedPercent) {
        if (allowUrlEncodedPercent) {
            this.encodedUrlBlacklist.remove(ENCODED_PERCENT);
            this.decodedUrlBlacklist.remove(PERCENT);
        } else {
            this.encodedUrlBlacklist.add(ENCODED_PERCENT);
            this.decodedUrlBlacklist.add(PERCENT);
        }
    }

    private void urlBlacklistsAddAll(Collection<String> values) {
        this.encodedUrlBlacklist.addAll(values);
        this.decodedUrlBlacklist.addAll(values);
    }

    private void urlBlacklistsRemoveAll(Collection<String> values) {
        this.encodedUrlBlacklist.removeAll(values);
        this.decodedUrlBlacklist.removeAll(values);
    }

    @Override
    public FirewalledRequest getFirewalledRequest(HttpServletRequest request) throws RequestRejectedException {
        rejectForbiddenHttpMethod(request);
        rejectedBlacklistedUrls(request);

        if (!isNormalized(request)) {
            request.setAttribute("isNormalized", new RequestRejectedException("The request was rejected because the URL was not normalized."));
        }

        String requestUri = request.getRequestURI();
        if (!containsOnlyPrintableAsciiCharacters(requestUri)) {
            request.setAttribute("isNormalized",  new RequestRejectedException("The requestURI was rejected because it can only contain printable ASCII characters."));
        }
        return new FirewalledRequest(request) {
            @Override
            public void reset() {
            }
        };
    }

    private void rejectForbiddenHttpMethod(HttpServletRequest request) {
        if (this.allowedHttpMethods == ALLOW_ANY_HTTP_METHOD) {
            return;
        }
        if (!this.allowedHttpMethods.contains(request.getMethod())) {
            request.setAttribute("isNormalized",  new RequestRejectedException("The request was rejected because the HTTP method \"" +
                    request.getMethod() +
                    "\" was not included within the whitelist " +
                    this.allowedHttpMethods));
        }
    }

    private void rejectedBlacklistedUrls(HttpServletRequest request) {
        for (String forbidden : this.encodedUrlBlacklist) {
            if (encodedUrlContains(request, forbidden)) {
                request.setAttribute("isNormalized",  new RequestRejectedException("The request was rejected because the URL contained a potentially malicious String \"" + forbidden + "\""));
            }
        }
        for (String forbidden : this.decodedUrlBlacklist) {
            if (decodedUrlContains(request, forbidden)) {
                request.setAttribute("isNormalized",  new RequestRejectedException("The request was rejected because the URL contained a potentially malicious String \"" + forbidden + "\""));
            }
        }
    }

    @Override
    public HttpServletResponse getFirewalledResponse(HttpServletResponse response) {
        return new FirewalledResponse(response);
    }

    private static Set<String> createDefaultAllowedHttpMethods() {
        Set<String> result = new HashSet<>();
        result.add(HttpMethod.DELETE.name());
        result.add(HttpMethod.GET.name());
        result.add(HttpMethod.HEAD.name());
        result.add(HttpMethod.OPTIONS.name());
        result.add(HttpMethod.PATCH.name());
        result.add(HttpMethod.POST.name());
        result.add(HttpMethod.PUT.name());
        return result;
    }

    private static boolean isNormalized(HttpServletRequest request) {
        if (!isNormalized(request.getRequestURI())) {
            return false;
        }
        if (!isNormalized(request.getContextPath())) {
            return false;
        }
        if (!isNormalized(request.getServletPath())) {
            return false;
        }
        if (!isNormalized(request.getPathInfo())) {
            return false;
        }
        return true;
    }

    private static boolean encodedUrlContains(HttpServletRequest request, String value) {
        if (valueContains(request.getContextPath(), value)) {
            return true;
        }
        return valueContains(request.getRequestURI(), value);
    }

    private static boolean decodedUrlContains(HttpServletRequest request, String value) {
        if (valueContains(request.getServletPath(), value)) {
            return true;
        }
        if (valueContains(request.getPathInfo(), value)) {
            return true;
        }
        return false;
    }

    private static boolean containsOnlyPrintableAsciiCharacters(String uri) {
        int length = uri.length();
        for (int i = 0; i < length; i++) {
            char c = uri.charAt(i);
            if (c < '\u0020' || c > '\u007e') {
                return false;
            }
        }

        return true;
    }

    private static boolean valueContains(String value, String contains) {
        return value != null && value.contains(contains);
    }

    private static boolean isNormalized(String path) {
        if (path == null) {
            return true;
        }

        if (path.indexOf("//") > -1) {
            return false;
        }

        for (int j = path.length(); j > 0;) {
            int i = path.lastIndexOf('/', j - 1);
            int gap = j - i;

            if (gap == 2 && path.charAt(i + 1) == '.') {
                // ".", "/./" or "/."
                return false;
            } else if (gap == 3 && path.charAt(i + 1) == '.' && path.charAt(i + 2) == '.') {
                return false;
            }

            j = i;
        }

        return true;
    }

}

SCHRITT 2: Erstellen Sie eine FirewalledResponse- Klasse

package com.biz.brains.project.security.firewall;

import java.io.IOException;
import java.util.regex.Pattern;

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;

class FirewalledResponse extends HttpServletResponseWrapper {
    private static final Pattern CR_OR_LF = Pattern.compile("\\r|\\n");
    private static final String LOCATION_HEADER = "Location";
    private static final String SET_COOKIE_HEADER = "Set-Cookie";

    public FirewalledResponse(HttpServletResponse response) {
        super(response);
    }

    @Override
    public void sendRedirect(String location) throws IOException {
        // TODO: implement pluggable validation, instead of simple blacklisting.
        // SEC-1790. Prevent redirects containing CRLF
        validateCrlf(LOCATION_HEADER, location);
        super.sendRedirect(location);
    }

    @Override
    public void setHeader(String name, String value) {
        validateCrlf(name, value);
        super.setHeader(name, value);
    }

    @Override
    public void addHeader(String name, String value) {
        validateCrlf(name, value);
        super.addHeader(name, value);
    }

    @Override
    public void addCookie(Cookie cookie) {
        if (cookie != null) {
            validateCrlf(SET_COOKIE_HEADER, cookie.getName());
            validateCrlf(SET_COOKIE_HEADER, cookie.getValue());
            validateCrlf(SET_COOKIE_HEADER, cookie.getPath());
            validateCrlf(SET_COOKIE_HEADER, cookie.getDomain());
            validateCrlf(SET_COOKIE_HEADER, cookie.getComment());
        }
        super.addCookie(cookie);
    }

    void validateCrlf(String name, String value) {
        if (hasCrlf(name) || hasCrlf(value)) {
            throw new IllegalArgumentException(
                    "Invalid characters (CR/LF) in header " + name);
        }
    }

    private boolean hasCrlf(String value) {
        return value != null && CR_OR_LF.matcher(value).find();
    }
}

SCHRITT 3: Erstellen Sie einen benutzerdefinierten Filter, um die RejectedException zu unterdrücken

package com.biz.brains.project.security.filter;

import java.io.IOException;
import java.util.Objects;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpHeaders;
import org.springframework.security.web.firewall.RequestRejectedException;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.GenericFilterBean;

import lombok.extern.slf4j.Slf4j;

@Component
@Slf4j
@Order(Ordered.HIGHEST_PRECEDENCE)
public class RequestRejectedExceptionFilter extends GenericFilterBean {

        @Override
        public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
            try {
                RequestRejectedException requestRejectedException=(RequestRejectedException) servletRequest.getAttribute("isNormalized");
                if(Objects.nonNull(requestRejectedException)) {
                    throw requestRejectedException;
                }else {
                    filterChain.doFilter(servletRequest, servletResponse);
                }
            } catch (RequestRejectedException requestRejectedException) {
                HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
                HttpServletResponse httpServletResponse = (HttpServletResponse) servletResponse;
                log
                    .error(
                            "request_rejected: remote={}, user_agent={}, request_url={}",
                            httpServletRequest.getRemoteHost(),  
                            httpServletRequest.getHeader(HttpHeaders.USER_AGENT),
                            httpServletRequest.getRequestURL(), 
                            requestRejectedException
                    );

                httpServletResponse.sendError(HttpServletResponse.SC_NOT_FOUND);
            }
        }
}

SCHRITT 4: Fügen Sie den benutzerdefinierten Filter in der Sicherheitskonfiguration zur Federfilterkette hinzu

@Override
protected void configure(HttpSecurity http) throws Exception {
     http.addFilterBefore(new RequestRejectedExceptionFilter(),
             ChannelProcessingFilter.class);
}

Jetzt mit der obigen Korrektur können wir RequestRejectedExceptionmit Fehler 404 Seite behandeln.


Danke dir. Dies ist der Ansatz, den ich vorübergehend verwendet habe, um unseren Java-Mikroservice zu aktualisieren, bis alle Front-End-Apps aktualisiert sind. Ich brauchte die Schritte 3 und 4 nicht, um '//' erfolgreich als normalisiert zu betrachten. Ich habe gerade die Bedingung auskommentiert, bei der in isNormalized nach einem doppelten Schrägstrich gesucht wurde, und dann eine Bean konfiguriert, um stattdessen die CustomStrictHttpFirewall-Klasse zu verwenden.
Gtaborga

Gibt es eine einfachere Problemumgehung über die Konfiguration? Aber ohne die Firewall
auszuschalten

0

In meinem Fall wurde das Problem dadurch verursacht, dass ich nicht mit Postman angemeldet war. Daher habe ich in einem anderen Tab eine Verbindung mit einem Sitzungscookie geöffnet, den ich aus den Headern meiner Chrome-Sitzung entnommen habe.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.