Spring 5.0.3 RequestRejectedException: запрос был отклонен, поскольку URL-адрес не был нормализован


88

Не уверен, что это ошибка 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)


1
Проблему планируется решить в 5.1.0; В настоящее время 5.0.0 не имеет этой проблемы.
java_dude

Ответы:


67

В документации по безопасности Spring упоминается причина блокировки // в запросе.

Например, он может содержать последовательности обхода пути (например, /../) или несколько косых черт (//), которые также могут приводить к сбою сопоставления с образцом. Некоторые контейнеры нормализуют их перед выполнением сопоставления сервлетов, а другие - нет. Для защиты от подобных проблем FilterChainProxy использует стратегию HttpFirewall для проверки и заключения запроса. Ненормализованные запросы автоматически отклоняются по умолчанию, а параметры пути и повторяющиеся косые черты удаляются для целей сопоставления.

Итак, есть два возможных решения -

  1. удалить двойную косую черту (предпочтительный подход)
  2. Разрешите // в Spring Security, настроив StrictHttpFirewall с помощью приведенного ниже кода.

Шаг 1 Создайте собственный брандмауэр, разрешающий косую черту в URL-адресе.

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

Шаг 2 Затем настройте этот bean-компонент в веб-безопасности.

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

Шаг 2 - необязательный шаг, Spring Boot просто нужно объявить bean-компонент типа HttpFirewall


Да, была введена безопасность обхода пути. Это новая функция, и это могло вызвать проблему. В чем я не слишком уверен, поскольку, как вы видите, он работает по HTTPS, а не по HTTP. Я бы предпочел подождать, пока эта ошибка не будет устранена jira.spring.io/browse/SPR-16419
java_dude

очень возможно, часть нашей проблемы ... но ... пользователь не набирает //, поэтому я пытаюсь понять, как этот второй / добавляется в первую очередь ... если spring генерирует наш jstl, он не должен добавлять его или нормализовать после добавления.
xenoterracide

4
На самом деле это не решает проблему, по крайней мере, для Spring Security 5.1.1. Вы должны использовать DefaultHttpFirewall, если вам нужны URL-адреса с двумя косыми чертами, например a / b // c. Метод isNormalized нельзя настроить или переопределить в StrictHttpFirewall.
Джейсон Виннебек,

Есть ли шанс, что кто-то может подсказать, как это сделать только в Spring, а не в Boot?
schoon

28

setAllowUrlEncodedSlash(true)у меня не сработало. По-прежнему внутренний метод isNormalizedвозвращается falseпри двойном слэше.

Я заменил StrictHttpFirewallс DefaultHttpFirewallналичием только следующий код:

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

Хорошо работает для меня.
Любой риск при использовании DefaultHttpFirewall?


1
Да. Тот факт, что вы не можете создать запасной ключ для своего соседа по комнате, не означает, что вы должны класть единственный ключ под коврик. Не рекомендуется. Безопасность менять не следует.
java_dude

16
@java_dude Замечательно, что вы вообще не предоставили никакой информации или обоснования, просто смутная аналогия.
kaqqao 03

Другой вариант - StrictHttpFirewallсоздать подкласс, чтобы дать немного больше контроля над отклонением URL-адресов, как подробно описано в этом ответе .
vallismortis

1
У меня это сработало, но мне также пришлось добавить это в свой XML-файл bean-компонента:<sec:http-firewall ref="defaultHttpFirewall"/>
Джейсон Виннебек,

1
Каковы последствия использования этого решения?
Фелипе Дезидерати

10

Я столкнулся с той же проблемой:

Версия Spring Boot = 1.5.10
Версия Spring Security = 4.2.4


Проблема возникла на конечных точках, где имя ModelAndViewпредставления было определено с помощью предшествующей косой черты . Пример:

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

Если я убрал косую черту, все заработало. Пример:

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

Я также провел несколько тестов с RedirectView, и, похоже, он работал с предыдущей косой чертой.


2
Это не решение. Что, если это была ошибка на стороне Spring. Если они его изменят, вам придется снова отменить все изменения. Я бы предпочел подождать до 5.1, так как к тому времени он будет решен.
java_dude 05

1
Нет, вам не нужно отменять изменение, потому что определение viewName без предшествующей косой черты отлично работает в более старых версиях.
Torsten Ojaperv

Вот в чем и проблема. Если все работало нормально и вы ничего не меняли, значит, в Spring появилась ошибка. Путь всегда должен начинаться с "/". Ознакомьтесь с любой весенней документацией. Проверьте это github.com/spring-projects/spring-security/issues/5007 & github.com/spring-projects/spring-security/issues/5044
java_dude

1
Меня это тоже укусило. Обновление всего ModelAndView без начального символа '/' устранило проблему
Натан Перье,

jira.spring.io/browse/SPR-16740 Я обнаружил ошибку, но удаление ведущего / не было для меня исправлением, и в большинстве случаев мы просто возвращаем имя представления в виде строки (из контроллера) . Необходимо рассматривать представление перенаправления как решение.
xenoterracide

6

Как только я использовал двойную косую черту при вызове API, я получил ту же ошибку.

Мне пришлось позвонить по адресу http: // localhost: 8080 / getSomething, но я сделал это как http: // localhost: 8080 // getSomething . Я решил это, убрав лишнюю косую черту.


можем ли мы написать для этого некоторую обработку исключений, чтобы мы могли сообщить клиенту о неправильном вводе?
YouAreAwesome

4

В моем случае, обновленном с 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


1
Добавьте описание в свой код, объясняя, что происходит и почему. Это хорошая практика. В противном случае ваш ответ может быть удален. Он уже отмечен как низкокачественный.
herrbischoff 08

3

Нижеприведенное решение представляет собой чистый обходной путь, который не ставит под угрозу безопасность, поскольку мы используем такой же строгий брандмауэр.

Шаги по исправлению следующие:

ШАГ 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.


Спасибо. Это подход, который я использовал временно, чтобы позволить нам обновлять микросервис Java до тех пор, пока не будут обновлены все интерфейсные приложения. Мне не нужны были шаги 3 и 4, чтобы можно было считать // нормализованным. Я просто закомментировал условие, которое проверяло наличие двойной косой черты в isNormalized, а затем настроил bean-компонент для использования вместо этого класса CustomStrictHttpFirewall.
gtaborga 09

Есть ли более простой способ обхода с помощью config? Но без отключения брандмауэра ..
Prathamesh dhanawade

0

В моем случае проблема была вызвана тем, что я не вошел в систему с помощью Postman, поэтому я открыл соединение на другой вкладке с файлом cookie сеанса, который я взял из заголовков в моем сеансе Chrome.

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.