Spring 5.0.3 RequestRejectedException: URL normalize edilmediği için istek reddedildi


89

Bunun Spring 5.0.3'teki bir hata mı yoksa benim tarafımdaki sorunları düzeltmek için yeni bir özellik mi olduğundan emin değilim.

Yükseltmeden sonra bu hatayı alıyorum. İlginç bir şekilde bu hata yalnızca yerel makinemde. HTTPS protokollü test ortamında aynı kod iyi çalışıyor.

Devam ediyor...

Bu hatayı almamın nedeni, sonuçta ortaya çıkan JSP sayfasını yüklemek için URL'min olmasıdır /location/thisPage.jsp. Kodu değerlendirmek request.getRequestURI()bana sonuç veriyor /WEB-INF/somelocation//location/thisPage.jsp. JSP sayfasının URL'sini buna düzeltirsemlocation/thisPage.jsp , işler iyi gidiyor.

Sorum şu Yani, kaldırmalısınız /gelen JSPileri gidiyor gerekli olan budur çünkü kodunda yolu. Veya Springmakinem ile test ortamım arasındaki tek fark protokole HTTPkarşı olduğu için bir hata ortaya çıkardı 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
Sorunun 5.1.0'da çözülmesi planlanıyor; Şu anda 5.0.0'da bu sorun yok.
java_dude

Yanıtlar:


73

Spring Security Documentation , istekte engelleme nedenini // belirtir.

Örneğin, yol geçiş dizileri (/../ gibi) veya birden çok eğik çizgi (//) içerebilir ve bu da kalıp eşleşmelerinin başarısız olmasına neden olabilir. Bazı kapsayıcılar, sunucu uygulaması eşleştirmesini gerçekleştirmeden önce bunları normalleştirir, ancak diğerleri yapmaz. FilterChainProxy, bu gibi sorunlara karşı koruma sağlamak için isteği kontrol etmek ve paketlemek için bir HttpFirewall stratejisi kullanır. Normalleştirilmemiş istekler varsayılan olarak otomatik olarak reddedilir ve eşleştirme amacıyla yol parametreleri ve yinelenen eğik çizgiler kaldırılır.

Yani iki olası çözüm var -

  1. çift ​​eğik çizgiyi kaldırın (tercih edilen yaklaşım)
  2. Aşağıdaki kodu kullanarak StrictHttpFirewall'u özelleştirerek Spring Security'de // izin verin.

Adım 1 URL'de eğik çizgiye izin veren özel güvenlik duvarı oluşturun.

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

Adım 2 Ve sonra bu bean'i web güvenliğinde yapılandırın

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

Adım 2 isteğe bağlı bir adımdır, Spring Boot sadece tipin bildirilmesi için bir bean'e ihtiyaç duyar HttpFirewallve onu filtre zincirinde otomatik olarak yapılandırır.

Spring Security 5.4 Güncellemesi

Spring security 5.4 ve üzerinde (Spring Boot> = 2.4.0), aşağıdaki bean'i oluşturarak reddedilen istek hakkında şikayet eden çok fazla logdan kurtulabiliriz.

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

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

Evet, yol geçiş güvenliği getirildi. Bu yeni bir özellik ve bu soruna neden olmuş olabilir. HTTP'de değil HTTPS'de çalıştığını gördüğünüzden pek emin değilim. Bu hata çözülene kadar beklemeyi tercih ederim jira.spring.io/browse/SPR-16419
java_dude

büyük olasılıkla sorunumuzun bir parçası ... ama ... kullanıcı bir // yazmıyor bu yüzden bu saniyenin nasıl eklendiğini anlamaya çalışıyorum ... eğer bahar bizim jstl url bunu eklememeli veya ekledikten sonra normalleştirmemelidir.
xenoterracide

5
Bu, en azından Spring Security 5.1.1 için çözümü gerçekten çözmez. A / b // c gibi iki eğik çizgi içeren URL'lere ihtiyacınız varsa DefaultHttpFirewall'u kullanmanız gerekir. İsNormalized yöntemi StrictHttpFirewall'da yapılandırılamaz veya geçersiz kılınamaz.
Jason Winnebeck

Birisinin Boot'un aksine, bunu yalnızca Spring'de nasıl yapacağına dair ipuçları vermesi ihtimali var mı?
gemi

29

setAllowUrlEncodedSlash(true)benim için çalışmadı. Çift eğik çizgi olduğunda hala dahili yöntem isNormalizedgeri döner false.

Yalnızca aşağıdaki kodu StrictHttpFirewallkullanarak değiştirdim DefaultHttpFirewall:

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

Benim için iyi çalışıyor.
Kullanarak herhangi bir risk var DefaultHttpFirewallmı?


1
Evet. Oda arkadaşınız için yedek anahtar yaratamamanız, tek anahtarı paspasın altına koymanız gerektiği anlamına gelmez. Tavsiye edilmiyor. Güvenlik değiştirilmemelidir.
java_dude

18
@java_dude Hiçbir bilgi veya mantık sağlamamış olmanız harika, sadece belirsiz bir benzetme.
kaqqao

Diğer bir seçenek, bu yanıttaStrictHttpFirewall ayrıntılı olarak açıklandığı gibi, URL'lerin reddedilmesi üzerinde biraz daha fazla kontrol sağlamak için alt sınıflara ayırmaktır .
vallismortis

1
Bu benim için çalıştı ama bunu fasulye <sec:http-firewall ref="defaultHttpFirewall"/>
XML'ime

1
Bu çözümü kullanmanın etkileri nelerdir?
Felipe Desiderati

10

Aynı sorunla karşılaştım:

Spring Boot sürümü = 1.5.10
Spring Security sürümü = 4.2.4


Sorun, ModelAndViewviewName'in önceki bir eğik çizgiyle tanımlandığı uç noktalarda meydana geldi . Misal:

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

Eğik çizgiyi kaldırırsam iyi çalıştı. Misal:

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

Ayrıca RedirectView ile bazı testler yaptım ve önceki bir eğik çizgi ile çalışıyor gibiydi.


2
Çözüm bu değil. Ya bu Bahar tarafındaki bir hataysa. Değiştirirlerse, tüm değişikliği tekrar geri almanız gerekecektir. O zamana kadar çözülmek üzere işaretlenen 5.1'e kadar beklemeyi tercih ederim.
java_dude

1
Hayır, değişikliği geri almak zorunda değilsiniz çünkü viewName'i eğik çizgiden önce tanımlamadan eski sürümlerde iyi çalışır.
Torsten Ojaperv

Sorun tam olarak budur. İyi çalıştıysa ve hiçbir şeyi değiştirmediyseniz, Bahar bir hata oluşturmuştur. Yol her zaman "/" ile başlamalıdır. Herhangi bir yay belgesine göz atın. Bunlara göz atın github.com/spring-projects/spring-security/issues/5007 & github.com/spring-projects/spring-security/issues/5044
java_dude

1
Bu beni de ısırdı. Baştaki '/' olmadan tüm ModelAndView'ı güncellemek sorunu çözdü
Nathan Perrier

jira.spring.io/browse/SPR-16740 Bir hata açtım, ancak baştaki / işaretini kaldırmak benim için bir düzeltme olmadı ve çoğu durumda görünüm adını bir dize olarak döndürüyoruz (denetleyiciden) . Çözüm olarak yönlendirme görünümüne bakmanız gerekir.
xenoterracide


5

Benim durumumda, 4.2.12 için yay securiy-web 3.1.3 yükseltme defaultHttpFirewalldeğiştirildi DefaultHttpFirewalliçin StrictHttpFirewallvarsayılan olarak. Bu nedenle, aşağıdaki gibi XML yapılandırmasında tanımlayın:

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

set HTTPFirewallolarakDefaultHttpFirewall


1
Lütfen kodunuza neler olduğunu ve neden olduğunu açıklayan bir açıklama ekleyin. Bu iyi bir uygulamadır. Yapmazsanız, cevabınızın silinme riski vardır. Zaten düşük kaliteli olarak işaretlendi.
herrbischoff

3

Aşağıdaki çözüm temiz bir çözümdür ve aynı katı güvenlik duvarını kullandığımız için güvenlikten ödün vermez.

Düzeltme adımları aşağıdaki gibidir:

ADIM 1: Aşağıdaki gibi StrictHttpFirewall'u geçersiz kılan bir Sınıf oluşturun .

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

}

ADIM 2: FirewalledResponse sınıfı oluşturun

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

ADIM 3: Reddedilen İstisnayı bastırmak için özel bir Filtre oluşturun

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

ADIM 4: Özel filtreyi güvenlik yapılandırmasında yaylı filtre zincirine ekleyin

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

Şimdi yukarıdaki düzeltmeyi kullanarak RequestRejectedExceptionHata 404 sayfasıyla başa çıkabiliriz .


Teşekkür ederim. Bu, ön uç uygulamaların tümü yükseltilene kadar Java mikro hizmetimizi yükseltmemize izin vermek için geçici olarak kullandığım yaklaşımdır. '//' nin normalleştirilmiş kabul edilmesine başarılı bir şekilde izin vermek için 3. ve 4. adımlara ihtiyacım yoktu. İsNormalized'de çift eğik çizgiyi kontrol eden koşulu yorumladım ve bunun yerine bir bean'i CustomStrictHttpFirewall sınıfını kullanacak şekilde yapılandırdım.
gtaborga

Yapılandırma aracılığıyla daha kolay bir çözüm var mı? Ancak güvenlik duvarını kapatmadan ..
Prathamesh dhanawade

0

Benim durumumda, sorun Postman ile oturum açmamamdan kaynaklanıyordu, bu yüzden Chrome oturumumdaki başlıklardan aldığım bir oturum çereziyle başka bir sekmede bağlantı açtım.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.