Ответ 1
Проблема планируется решить в 5.1.0; В настоящее время 5.0.0 не имеет этой проблемы.
Не уверен, что это ошибка в Spring 5.0.3 или новая функция, чтобы исправить ситуацию с моей стороны.
После обновления я получаю эту ошибку. Интересно, что эта ошибка только на моей локальной машине. Тот же код в тестовой среде с протоколом HTTPS работает нормально.
Продолжение...
Причина, по которой я получаю эту ошибку, заключается в том, что мой URL для загрузки результирующей страницы JSP - /location/thisPage.jsp
. Оценка кода request.getRequestURI()
дает мне результат /WEB-INF/somelocation//location/thisPage.jsp
. Если я исправлю URL-адрес страницы JSP в этом location/thisPage.jsp
, все будет хорошо.
Поэтому мой вопрос заключается в том, должен ли я удалить /
из JSP
пути в коде, потому что это то, что требуется в будущем. Или Spring
внес ошибку, поскольку единственное различие между моей машиной и тестовой средой - это протокол HTTP
против 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)
Проблема планируется решить в 5.1.0; В настоящее время 5.0.0 не имеет этой проблемы.
Spring Security Documentation упоминает причину блокировки//в запросе.
Например, он может содержать последовательности обхода пути (например,/../) или несколько косых черт (//), что также может привести к сбою сопоставления с шаблоном. Некоторые контейнеры нормализуют их перед выполнением отображения сервлета, но другие этого не делают. Чтобы защитить от подобных проблем, FilterChainProxy использует стратегию HttpFirewall для проверки и переноса запроса. Ненормализованные запросы автоматически отклоняются по умолчанию, а параметры пути и дублирующие косые черты удаляются для сопоставления.
Таким образом, есть два возможных решения -
Шаг 1 Создайте собственный брандмауэр, который позволяет использовать косую черту в URL.
@Bean
public HttpFirewall allowUrlEncodedSlashHttpFirewall() {
StrictHttpFirewall firewall = new StrictHttpFirewall();
firewall.setAllowUrlEncodedSlash(true);
return firewall;
}
Шаг 2 А затем настройте этот компонент в web-безопасности
@Override
public void configure(WebSecurity web) throws Exception {
//@formatter:off
super.configure(web);
web.httpFirewall(allowUrlEncodedSlashHttpFirewall());
....
}
Шаг 2 является необязательным, Spring Boot просто нужно объявить bean-компонент типа HttpFirewall
setAllowUrlEncodedSlash(true)
не работал для меня. Все еще внутренний метод isNormalized
возвращает false
при наличии двойной косой черты.
Я заменил StrictHttpFirewall
на DefaultHttpFirewall
, добавив только следующий код:
@Bean
public HttpFirewall defaultHttpFirewall() {
return new DefaultHttpFirewall();
}
Хорошо работает для меня.
Есть ли риск при использовании DefaultHttpFirewall
?
Я столкнулся с той же проблемой с:
Версия Spring Boot = 1.5.10
Версия Spring Security = 4.2.4
Проблема возникла на конечных точках, где ModelAndView
ModelAndView было определено с предыдущей косой чертой. Пример:
ModelAndView mav = new ModelAndView("/your-view-here");
Если я убрал косую черту, все заработало нормально. Пример:
ModelAndView mav = new ModelAndView("your-view-here");
Я также провел несколько тестов с RedirectView, и, похоже, он работал с предыдущей косой чертой.
Как только я использовал двойную косую черту при вызове API, я получил ту же ошибку.
Мне пришлось позвонить http://localhost: 8080/getSomething, но я сделал, как http://localhost: 8080//getSomething. Я решил это, удалив лишнюю косую черту.
Ниже приведено чистое решение проблемы. Оно не ставит под угрозу безопасность, поскольку мы используем такой же строгий брандмауэр.
Шаги для исправления, как показано ниже:
ШАГ 1. Создайте класс, переопределяющий StrictHttpFirewall, как показано ниже.
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;
}
}
ШАГ 2. Создание класса 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();
}
}
ШАГ 3. Создание пользовательского фильтра для подавления исключения 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);
}
}
}
ШАГ 4: Добавьте пользовательский фильтр в цепочку пружинных фильтров в конфигурации безопасности
@Override
protected void configure(HttpSecurity http) throws Exception {
http.addFilterBefore(new RequestRejectedExceptionFilter(),
ChannelProcessingFilter.class);
}
Теперь, используя вышеуказанное исправление, мы можем обработать RequestRejectedException
со страницей ошибки 404.
В моем случае проблема была вызвана тем, что вы не вошли в систему с Postman, поэтому я открыл соединение на другой вкладке с файлом cookie сеанса, который я взял из заголовков в моем сеансе Chrome.
В моем случае при обновлении с spring-securiy-web 3.1.3 до 4.2.12 значение defaultHttpFirewall
было изменено с DefaultHttpFirewall
на StrictHttpFirewall
по умолчанию.
Так что просто определите его в конфигурации XML, как показано ниже:
<bean id="defaultHttpFirewall" class="org.springframework.security.web.firewall.DefaultHttpFirewall"/>
<sec:http-firewall ref="defaultHttpFirewall"/>
установить HTTPFirewall
как DefaultHttpFirewall