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.