Nie jestem pewien, czy jest to błąd w Spring 5.0.3, czy nowa funkcja, która naprawia rzeczy po mojej stronie.
Po uaktualnieniu pojawia się ten błąd. Co ciekawe ten błąd występuje tylko na moim lokalnym komputerze. Ten sam kod w środowisku testowym z protokołem HTTPS działa dobrze.
Kontynuacja ...
Powodem, dla którego otrzymuję ten błąd, jest to, że mój adres URL do wczytywania wynikowej strony JSP to /location/thisPage.jsp
. Ocena kodu request.getRequestURI()
daje mi wynik /WEB-INF/somelocation//location/thisPage.jsp
. Jeśli poprawię adres URL strony JSP location/thisPage.jsp
, wszystko działa dobrze.
Więc moje pytanie brzmi, czy powinienem usunąć /
ze JSP
ścieżki w kodzie, ponieważ jest to wymagane w przyszłości. Albo Spring
wprowadził błąd, ponieważ jedyną różnicą między moją maszyną a środowiskiem testowym jest protokół HTTP
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)
źródło
Odpowiedzi:
Spring Security Documentation podaje przyczynę zablokowania // w żądaniu.
Istnieją więc dwa możliwe rozwiązania -
Krok 1 Utwórz niestandardową zaporę sieciową, która umożliwia ukośnik w adresie URL.
@Bean public HttpFirewall allowUrlEncodedSlashHttpFirewall() { StrictHttpFirewall firewall = new StrictHttpFirewall(); firewall.setAllowUrlEncodedSlash(true); return firewall; }
Krok 2 A następnie skonfiguruj ten bean w zabezpieczeniach internetowych
@Override public void configure(WebSecurity web) throws Exception { //@formatter:off super.configure(web); web.httpFirewall(allowUrlEncodedSlashHttpFirewall()); .... }
Krok 2 jest krokiem opcjonalnym, Spring Boot potrzebuje tylko komponentu bean, aby zadeklarować typ
HttpFirewall
źródło
setAllowUrlEncodedSlash(true)
nie działa dla mnie. Nadal wewnętrzna metodaisNormalized
powracafalse
po podwójnym ukośniku.Wymieniłem
StrictHttpFirewall
zDefaultHttpFirewall
mając tylko następujący kod:@Bean public HttpFirewall defaultHttpFirewall() { return new DefaultHttpFirewall(); }
U mnie działa dobrze.
Jakiekolwiek ryzyko związane z używaniem
DefaultHttpFirewall
?źródło
StrictHttpFirewall
aby dać nieco większą kontrolę nad odrzucaniem adresów URL, jak szczegółowo opisano w tej odpowiedzi .<sec:http-firewall ref="defaultHttpFirewall"/>
Napotkałem ten sam problem z:
Problem wystąpił na punktach końcowych, w których
ModelAndView
nazwa_widoku została zdefiniowana z poprzedzającym ukośnikiem . Przykład:ModelAndView mav = new ModelAndView("/your-view-here");
Jeśli usunąłem cięcie, działało dobrze. Przykład:
ModelAndView mav = new ModelAndView("your-view-here");
Zrobiłem również kilka testów z RedirectView i wydawało się, że działa z poprzedzającym ukośnikiem.
źródło
Kiedy użyłem podwójnego ukośnika podczas wywoływania API, otrzymałem ten sam błąd.
Musiałem zadzwonić pod adres http: // localhost: 8080 / getSomething, ale polubiłem http: // localhost: 8080 // getSomething . Rozwiązałem to, usuwając dodatkowy ukośnik.
źródło
W moim przypadku, zaktualizowany z spring-securiy-web 3.1.3 do 4.2.12,
defaultHttpFirewall
został zmieniony zDefaultHttpFirewall
abyStrictHttpFirewall
domyślnie. Po prostu zdefiniuj to w konfiguracji XML, jak poniżej:<bean id="defaultHttpFirewall" class="org.springframework.security.web.firewall.DefaultHttpFirewall"/> <sec:http-firewall ref="defaultHttpFirewall"/>
ustaw
HTTPFirewall
jakoDefaultHttpFirewall
źródło
Poniższe rozwiązanie to czyste obejście, które nie zagraża bezpieczeństwu, ponieważ używamy tego samego ścisłego firewalla.
Kroki do mocowania są następujące:
KROK 1: Utwórz klasę przesłaniającą StrictHttpFirewall jak poniżej.
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; } }
KROK 2: Utwórz klasę 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(); } }
KROK 3: Utwórz niestandardowy filtr, aby pominąć odrzucony wyjątek
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); } } }
KROK 4: Dodaj niestandardowy filtr do łańcucha filtrów sprężynowych w konfiguracji bezpieczeństwa
@Override protected void configure(HttpSecurity http) throws Exception { http.addFilterBefore(new RequestRejectedExceptionFilter(), ChannelProcessingFilter.class); }
Teraz korzystając z powyższej poprawki, możemy sobie poradzić
RequestRejectedException
ze stroną błędu 404.źródło
W moim przypadku problem był spowodowany niezalogowaniem się do Postmana, więc otworzyłem połączenie na innej karcie z sesyjnym plikiem cookie, który wziąłem z nagłówków mojej sesji Chrome.
źródło