Spring 5.0.3 RequestRejectedException: la richiesta è stata rifiutata perché l'URL non è stato normalizzato


89

Non sono sicuro che si tratti di un bug con Spring 5.0.3 o di una nuova funzionalità per risolvere le cose da parte mia.

Dopo l'aggiornamento, ricevo questo errore. È interessante notare che questo errore è solo sulla mia macchina locale. Lo stesso codice sull'ambiente di test con protocollo HTTPS funziona bene.

Continuando ...

Il motivo per cui ricevo questo errore è perché il mio URL per caricare la pagina JSP risultante è /location/thisPage.jsp. La valutazione del codice request.getRequestURI()mi dà il risultato /WEB-INF/somelocation//location/thisPage.jsp. Se correggo l'URL della pagina JSP su questo location/thisPage.jsp, le cose funzionano bene.

Quindi la mia domanda è: dovrei rimuovere /dal JSPpercorso nel codice perché è ciò che è richiesto in futuro. Oppure Springha introdotto un bug in quanto l'unica differenza tra la mia macchina e l'ambiente di test è il protocollo HTTPrispetto a 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
Il problema dovrebbe essere risolto nella 5.1.0; Attualmente 5.0.0 non presenta questo problema.
java_dude

Risposte:


73

La documentazione di Spring Security menziona il motivo del blocco // nella richiesta.

Ad esempio, potrebbe contenere sequenze di attraversamento del percorso (come /../) o più barre (//) che potrebbero anche causare il fallimento delle corrispondenze di pattern. Alcuni contenitori li normalizzano prima di eseguire la mappatura servlet, ma altri no. Per proteggersi da problemi come questi, FilterChainProxy utilizza una strategia HttpFirewall per controllare e racchiudere la richiesta. Le richieste non normalizzate vengono automaticamente rifiutate per impostazione predefinita e i parametri del percorso e le barre duplicate vengono rimossi per scopi di corrispondenza.

Quindi ci sono due possibili soluzioni:

  1. rimuovere la doppia barra (approccio preferito)
  2. Consenti // in Spring Security personalizzando StrictHttpFirewall utilizzando il codice seguente.

Passaggio 1 Creare un firewall personalizzato che consenta la barra nell'URL.

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

Passaggio 2 E quindi configurare questo bean in websecurity

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

Il passaggio 2 è un passaggio facoltativo, Spring Boot ha solo bisogno di un bean per essere dichiarato di tipo HttpFirewalle lo configurerà automaticamente nella catena di filtri.

Aggiornamento Spring Security 5.4

In Spring Security 5.4 e versioni successive (Spring Boot> = 2.4.0), possiamo sbarazzarci di troppi log che lamentano la richiesta rifiutata creando il bean sottostante.

import org.springframework.security.web.firewall.RequestRejectedHandler;
import org.springframework.security.web.firewall.HttpStatusRequestRejectedHandler;

@Bean
RequestRejectedHandler requestRejectedHandler() {
   return new HttpStatusRequestRejectedHandler();
}

Sì, è stata introdotta la sicurezza di attraversamento del percorso. Questa è una nuova funzionalità e questo potrebbe aver causato il problema. Cosa che non sono troppo sicuro come vedi funziona su HTTPS e non su HTTP. Preferisco aspettare che questo bug venga risolto jira.spring.io/browse/SPR-16419
java_dude

molto probabilmente parte del nostro problema ... ma ... l'utente non sta digitando // quindi sto cercando di capire come viene aggiunto quel secondo / in primo luogo ... se la primavera sta generando il nostro jstl url non dovrebbe aggiungerlo o normalizzarlo dopo averlo aggiunto.
xenoterracide

5
Questo in realtà non risolve la soluzione, almeno per Spring Security 5.1.1. Devi usare DefaultHttpFirewall se hai bisogno di URL con due barre come a / b // c. Il metodo isNormalized non può essere configurato o sovrascritto in StrictHttpFirewall.
Jason Winnebeck

C'è qualche possibilità che qualcuno possa dare indicazioni su come farlo solo in primavera rispetto a Boot?
schoon

29

setAllowUrlEncodedSlash(true)non ha funzionato per me. Il metodo ancora interno isNormalizedritorna falsequando si ha una doppia barra.

Ho sostituito StrictHttpFirewallcon DefaultHttpFirewallavendo solo il seguente codice:

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

Funziona bene per me.
Qualche rischio usando DefaultHttpFirewall?


1
Sì. Solo perché non puoi creare una chiave di riserva per il tuo coinquilino, non significa che dovresti mettere l'unica chiave sotto lo zerbino. Non consigliato. La sicurezza non dovrebbe essere modificata.
java_dude

18
@java_dude Fantastico come non hai fornito alcuna informazione o motivazione, solo una vaga analogia.
kaqqao

Un'altra opzione è la sottoclasse StrictHttpFirewallper dare un po 'più di controllo sul rifiuto degli URL, come dettagliato in questa risposta .
vallismortis

1
Questo ha funzionato per me, ma ho anche dovuto aggiungere questo nel mio bean XML:<sec:http-firewall ref="defaultHttpFirewall"/>
Jason Winnebeck

1
Quali sono le implicazioni dell'utilizzo di questa soluzione?
Felipe Desiderati

10

Ho riscontrato lo stesso problema con:

Versione Spring Boot = 1.5.10
Versione Spring Security = 4.2.4


Il problema si è verificato sugli endpoint, dove ModelAndViewviewName è stato definito con una barra anteriore precedente . Esempio:

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

Se ho rimosso la barra ha funzionato bene. Esempio:

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

Ho anche fatto alcuni test con RedirectView e sembrava funzionare con una barra anteriore precedente.


2
Questa non è la soluzione. E se questo fosse un bug sul lato primaverile. Se lo modificano, dovrai annullare nuovamente tutte le modifiche. Preferirei aspettare fino a 5.1 poiché è contrassegnato per essere risolto per allora.
java_dude

1
No, non è necessario annullare la modifica perché la definizione di viewName senza la barra anteriore precedente funziona correttamente sulle versioni precedenti.
Torsten Ojaperv

Questo è esattamente il problema. Se ha funzionato bene e non hai modificato nulla, Spring ha introdotto un bug. Il percorso dovrebbe sempre iniziare con "/". Controlla tutta la documentazione sulla primavera.
Dai

1
Anche questo mi ha morso. L'aggiornamento di tutto ModelAndView senza il "/" principale ha risolto il problema
Nathan Perrier

jira.spring.io/browse/SPR-16740 Ho aperto un bug, ma la rimozione della / principale non è stata una correzione per me, e nella maggior parte dei casi stiamo solo restituendo il nome della vista come una stringa (dal controller) . È necessario considerare la visualizzazione di reindirizzamento come una soluzione.
xenoterracide


5

Nel mio caso, aggiornato da spring-securiy-web 3.1.3 a 4.2.12, è defaultHttpFirewallstato modificato da DefaultHttpFirewalla StrictHttpFirewallper impostazione predefinita. Quindi definiscilo nella configurazione XML come di seguito:

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

imposta HTTPFirewallcomeDefaultHttpFirewall


1
Si prega di aggiungere una descrizione al codice che spieghi cosa sta succedendo e perché. Questa è una buona pratica. Se non lo fai, la tua risposta rischia di essere cancellata. È già stato contrassegnato come di bassa qualità.
herrbischoff

3

La soluzione di seguito è un lavoro pulito in giro. Non compromette la sicurezza perché utilizziamo lo stesso firewall rigoroso.

I passaggi per il fissaggio sono i seguenti:

PASSAGGIO 1: creare una classe che sovrascriva StrictHttpFirewall come di seguito.

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;
    }

}

PASSAGGIO 2: creare una classe FirewalledResponse

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();
    }
}

PASSAGGIO 3: creare un filtro personalizzato per eliminare l' eccezione RejectedException

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);
            }
        }
}

PASSAGGIO 4: aggiungere il filtro personalizzato alla catena di filtri a molla nella configurazione di sicurezza

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

Ora usando la correzione sopra, possiamo gestire RequestRejectedExceptioncon la pagina Errore 404.


Grazie. Questo è l'approccio che ho utilizzato temporaneamente per consentirci di aggiornare il nostro microservizio Java fino a quando le app front-end non saranno tutte aggiornate. Non avevo bisogno dei passaggi 3 e 4 per consentire a "//" di essere considerato normalizzato. Ho appena commentato la condizione che controllava la doppia barra in isNormalized e quindi ho configurato un bean per utilizzare invece la classe CustomStrictHttpFirewall.
gtaborga

C'è una soluzione più semplice tramite config? Ma senza spegnere il firewall ..
Prathamesh dhanawade

0

Nel mio caso, il problema era causato dal mancato accesso a Postman, quindi ho aperto una connessione in un'altra scheda con un cookie di sessione che ho preso dalle intestazioni nella mia sessione di Chrome.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.