Как перехватить requestrejectedexception в spring?

Я вижу тонна записей RequestRejectedException в моем журнале Tomcat (образец вставлен ниже). Они начали появляться в моем файле журнала после обновления второстепенной версии (Spring Security 4.2.4, IIRC) несколько месяцев назад, так что это явно новая функция безопасности в Spring, которая включена по умолчанию. Похожая проблема - сообщил здесь, но мой вопрос конкретно касается того, как перехватывать эти исключения в контроллере. Для этой проблемы задокументирована ошибка Spring Security (Обеспечить способ обработки RequestRejectedException). Однако они не нацелены на решение этой проблемы до весны 5.1.

Я понимаю почему выбрасываются эти исключения, и я не хочу отключить эту функцию безопасности.

Я хочу получить некоторый контроль над этой функцией, чтобы:

  1. Я знаю, что не блокирую доступ к своему сайту законным пользователям.
  2. Я вижу, какие запросы вызывают это (это атаки SQL-инъекций?)
  3. Я могу настроить ответ сервера. Брандмауэр Spring Security выгружает полную трассировку стека на веб-клиент (раскрытие информации) вместе с 500 Internal Server Error (что в высшей степени неверно, это должен быть 400 Bad Request).

Я хочу найти способ записать запрошенный URL-адрес, но также подавить трассировку стека специально для этих исключений, потому что они загрязняют мои файлы журналов, не предоставляя мне никакой полезной информации. В оптимальном случае я хотел бы перехватывать эти исключения и обрабатывать их на уровне моего приложения, а не сообщать о них вообще в журнале Tomcat.

Например, это одна из тысяч записей журнала, которые появляются каждый день в моем catalina.out:

Aug 10, 2018 2:01:36 PM org.apache.catalina.core.StandardWrapperValve invoke
SEVERE: Servlet.service() for servlet [dispatcher] in context with path [] threw exception
org.springframework.security.web.firewall.RequestRejectedException: The request was rejected because the URL contained a potentially malicious String ";"
        at org.springframework.security.web.firewall.StrictHttpFirewall.rejectedBlacklistedUrls(StrictHttpFirewall.java:265)
        at org.springframework.security.web.firewall.StrictHttpFirewall.getFirewalledRequest(StrictHttpFirewall.java:245)
        at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:193)
        at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:177)
        at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:347)
        at org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:263)
        at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
        at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
        at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:198)
        at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:96)
        at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:496)
        at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:140)
        at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:81)
        at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:87)
        at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:342)
        at org.apache.coyote.ajp.AjpProcessor.service(AjpProcessor.java:486)
        at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:66)
        at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:790)
        at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1459)
        at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49)
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
        at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
        at java.lang.Thread.run(Thread.java:748)

Я вижу более 3200 из них за двухдневный период, и он быстро стал крупнейшим участником моего файла журнала catalina.out до такой степени, что не позволяет мне видеть другие, законные проблемы. По сути, эта новая функция Spring Security представляет собой форму встроенного отказа в обслуживании, и с апреля она потратила впустую часы моего времени. Я не говорю, что это не важная функция, просто то, что реализация по умолчанию полностью испорчена, и я хочу найти способ получить некоторый контроль над ней как как разработчик, так и как системный администратор.

Я использую настраиваемый контроллер ошибок для перехвата многих других типов исключений (включая IOException) в Spring. Однако RequestRejectedException, похоже, по какой-то причине проваливается.

Это соответствующая часть моего ErrorController.java, чтобы дать представление о том, чего я пытаюсь достичь:

@ControllerAdvice
public final class ErrorController
{
    /**
     * Logger.
     */
    private static final Logger LOGGER = Logger.getLogger(ErrorController.class.getName());

    /**
     * Generates an Error page by intercepting exceptions generated from HttpFirewall.
     *
     * @param ex A RequestRejectedException exception.
     * @return The tile definition name for the page.
     */
    @ExceptionHandler(RequestRejectedException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public String handleRequestRejectedException(final HttpServletRequest request, final RequestRejectedException ex)
    {
        if (LOGGER.isLoggable(Level.INFO))
        {
            LOGGER.log(Level.INFO, "Request Rejected", ex);
        }

        LOGGER.log(Level.WARNING, "Rejected request for [" + request.getRequestURL().toString() + "]. Reason: " + ex.getMessage());
        return "errorPage";
    }

    /**
     * Generates a Server Error page.
     *
     * @param ex An exception.
     * @return The tile definition name for the page.
     */
    @ExceptionHandler(Exception.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public String handleException(final Exception ex)
    {
        if (LOGGER.isLoggable(Level.SEVERE))
        {
            LOGGER.log(Level.SEVERE, "Server Error", ex);
        }

        return "errorPage";
    }
}

Этот контроллер ошибок работает для многих исключений. Например, он успешно перехватил этот IllegalStateException:

Aug 05, 2018 7:50:30 AM com.mycompany.spring.controller.ErrorController handleException
SEVERE: Server Error
java.lang.IllegalStateException: Cannot create a session after the response has been committed
        at org.apache.catalina.connector.Request.doGetSession(Request.java:2999)
...

Однако это не перехват RequestRejectedException (на что указывает отсутствие «Ошибка сервера» в первом примере журнала выше).

Как я могу перехватить RequestRejectedException в контроллере ошибок?

Я не могу понять, кто добавляет лишнюю косую черту? Я набираю «example.com/app/main» в адресное поле браузера, и Spring каким-то образом получает «/ app // main».

Marvo 16.09.2019 21:38

Записано как Ошибка безопасности Spring 5007. Отмечено как исправленное в версии 5.4.0-M1 в июле 2020 года; вышла версия 5.4.0 в сентябре 2020 года.

Raedwald 16.03.2021 11:09

Однако у меня такая же проблема с Spring Security 5.4.2. Так что, я думаю, у них не столько проблема исправлено, сколько облегчение реализации обходного пути.

Raedwald 16.03.2021 11:11
34
3
19 588
6

Ответы 6

Я реализовал подкласс StrictHttpFirewall, который записывает информацию запроса в консоль и генерирует новое исключение с подавленной трассировкой стека. Это частично решает мою проблему (по крайней мере, теперь я вижу плохие запросы).

Если вы просто хотите увидеть отклоненные запросы без трассировки стека, это ответ, который вы ищете.

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


Ведение журнала HttpFirewall.java

Этот класс расширяет StrictHttpFirewall для перехвата RequestRejectedException и генерирует новое исключение с метаданными из запроса и подавленной трассировкой стека.

import java.util.logging.Level;
import java.util.logging.Logger;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.security.web.firewall.FirewalledRequest;
import org.springframework.security.web.firewall.RequestRejectedException;
import org.springframework.security.web.firewall.StrictHttpFirewall;

/**
 * Overrides the StrictHttpFirewall to log some useful information about blocked requests.
 */
public final class LoggingHttpFirewall extends StrictHttpFirewall
{
    /**
     * Logger.
     */
    private static final Logger LOGGER = Logger.getLogger(LoggingHttpFirewall.class.getName());

    /**
     * Default constructor.
     */
    public LoggingHttpFirewall()
    {
        super();
        return;
    }

    /**
     * Provides the request object which will be passed through the filter chain.
     *
     * @returns A FirewalledRequest (required by the HttpFirewall interface) which
     *          inconveniently breaks the general contract of ServletFilter because
     *          we can't upcast this to an HttpServletRequest. This prevents us
     *          from re-wrapping this using an HttpServletRequestWrapper.
     * @throws RequestRejectedException if the request should be rejected immediately.
     */
    @Override
    public FirewalledRequest getFirewalledRequest(final HttpServletRequest request) throws RequestRejectedException
    {
        try
        {
            return super.getFirewalledRequest(request);
        } catch (RequestRejectedException ex) {
            if (LOGGER.isLoggable(Level.WARNING))
            {
                LOGGER.log(Level.WARNING, "Intercepted RequestBlockedException: Remote Host: " + request.getRemoteHost() + " User Agent: " + request.getHeader("User-Agent") + " Request URL: " + request.getRequestURL().toString());
            }

            // Wrap in a new RequestRejectedException with request metadata and a shallower stack trace.
            throw new RequestRejectedException(ex.getMessage() + ".\n Remote Host: " + request.getRemoteHost() + "\n User Agent: " + request.getHeader("User-Agent") + "\n Request URL: " + request.getRequestURL().toString())
            {
                private static final long serialVersionUID = 1L;

                @Override
                public synchronized Throwable fillInStackTrace()
                {
                    return this; // suppress the stack trace.
                }
            };
        }
    }

    /**
     * Provides the response which will be passed through the filter chain.
     * This method isn't extensible because the request may already be committed.
     * Furthermore, this is only invoked for requests that were not blocked, so we can't
     * control the status or response for blocked requests here.
     *
     * @param response The original HttpServletResponse.
     * @return the original response or a replacement/wrapper.
     */
    @Override
    public HttpServletResponse getFirewalledResponse(final HttpServletResponse response)
    {
        // Note: The FirewalledResponse class is not accessible outside the package.
        return super.getFirewalledResponse(response);
    }
}

WebSecurityConfig.java

В WebSecurityConfig установите брандмауэр HTTP на LoggingHttpFirewall.

@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter
{
    /**
     * Default constructor.
     */
    public WebSecurityConfig()
    {
        super();
        return;
    }

    @Override
    public final void configure(final WebSecurity web) throws Exception
    {
        super.configure(web);
        web.httpFirewall(new LoggingHttpFirewall()); // Set the custom firewall.
        return;
    }
}

Результаты

После развертывания этого решения в производственной среде я быстро обнаружил, что поведение StrictHttpFirewall по умолчанию блокирует индексирование моего сайта Google!

Aug 13, 2018 1:48:56 PM com.mycompany.spring.security.AnnotatingHttpFirewall getFirewalledRequest
WARNING: Intercepted RequestBlockedException: Remote Host: 66.249.64.223 User Agent: Mozilla/5.0 (Linux; Android 6.0.1; Nexus 5X Build/MMB29P) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.96 Mobile Safari/537.36 (compatible; Googlebot/2.1; +http://www.google.com/bot.html) Request URL: https://www.mycompany.com/10.1601/tx.3784;jsessionid=692804549F9AB55F45DBD0AFE2A97FFD

Как только я это обнаружил, я быстро развернул новую версию (включенную в мой другой ответ), которая ищет ;jsessionid= и разрешает эти запросы. Вполне могут быть другие запросы, которые также должны пройти, и теперь у меня есть способ их обнаружить.

Это также может быть обработано простым фильтром, который приведет к ответу с ошибкой 404

@Component
@Slf4j
@Order(Ordered.HIGHEST_PRECEDENCE)
public class LogAndSuppressRequestRejectedExceptionFilter extends GenericFilterBean {

    @Override
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
        try {
            chain.doFilter(req, res);
        } catch (RequestRejectedException e) {
            HttpServletRequest request = (HttpServletRequest) req;
            HttpServletResponse response = (HttpServletResponse) res;

            log
                .warn(
                        "request_rejected: remote={}, user_agent={}, request_url={}",
                        request.getRemoteHost(),  
                        request.getHeader(HttpHeaders.USER_AGENT),
                        request.getRequestURL(), 
                        e
                );

            response.sendError(HttpServletResponse.SC_NOT_FOUND);
        }
    }
}

Это очень недооцененный ответ. Спасибо за простое решение!

ArmedChef 17.01.2019 23:17

Самое разумное решение из всех.

arctica 04.02.2019 13:23

Это работает, но мы получаем журналы ошибок, а не просто журналы предупреждений.

Sabareesh Kkanan 17.04.2019 23:02

Просто, эффективно. Спасибо, что поделился!

Oozeerally 20.02.2020 15:08

Это сработало для меня, за исключением того, что мне пришлось изменить метод журнала, иначе трассировка стека все еще была записана. Изменено на: log.warn ("request_rejected: remote = {}, user_agent = {}, request_url = {} reason = {}", request.getRemoteHost (), request.getHeader (HttpHeaders.USER_AGENT), request.getRequestURL () , e.getMessage ());

Locutus 24.09.2020 20:21

Другой способ справиться с этим - использовать Spring АОП. Мы можем создать совет по методу FilterChainProxy.doFilter (), который улавливает любые RequestRejectedException, генерируемые HttpFirewall, и преобразует их в 400 BAD_REQUEST

@Aspect
@Component
public class FilterChainProxyAdvice {

    @Around("execution(public void org.springframework.security.web.FilterChainProxy.doFilter(..))")
    public void handleRequestRejectedException (ProceedingJoinPoint pjp) throws Throwable {
        try {
            pjp.proceed();
        } catch (RequestRejectedException exception) {
            HttpServletResponse response = (HttpServletResponse) pjp.getArgs()[1]);
            response.sendError(HttpServletResponse.SC_BAD_REQUEST);
        }
    }
}

Это вызывается дважды

Sabareesh Kkanan 17.04.2019 20:48

@SabareeshKkanan, это связано с вызовом рекомендаций AspectJ по умолчанию. Верно, что метод handleRequestRejectedException() вызывается дважды, но фактический вызов метода FilterChainProxy.doFilter() вызывается только один раз. Взгляните на здесь, чтобы понять, как работает перехват @Around.

Ahmed Sayed 18.04.2019 15:08

похоже, что не работает в Spring 5.2.9, NPE выбрасывается из метода GenericFilterBean#init во время настройки контекста, поскольку поле Log logger имеет значение null по неизвестным причинам

MeetJoeBlack 26.11.2020 20:43

Довольно простой способ - использовать web.xml; укажите в этом файле страницу с ошибкой:

<error-page>
  <exception-type>org.springframework.security.web.firewall.RequestRejectedException</exception-type>
  <location>/request-rejected</location>
</error-page>

Для указанного пути (местоположения) добавьте сопоставление в аннотированный класс @Controller:

@RequestMapping(value = "/request-rejected")
@ResponseStatus(HttpStatus.BAD_REQUEST)
public @ResponseBody String handleRequestRejected(
        @RequestAttribute(RequestDispatcher.ERROR_EXCEPTION) RequestRejectedException ex,
        @RequestAttribute(RequestDispatcher.ERROR_REQUEST_URI) String uri) {

    String msg = ex.getMessage();

    // optionally log the message and requested URI (slf4j)
    logger.warn("Request with URI [{}] rejected. {}", uri, msg);

    return msg;
}

Для версий безопасности Spring 5.4 и выше вы можете просто создать bean-компонент типа RequestRejectedHandler, который будет внедрен в цепочку фильтров безопасности Spring.

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

@Bean
RequestRejectedHandler requestRejectedHandler() {
   // sends an error response with a configurable status code (default is 400 BAD_REQUEST)
   // we can pass a different value in the constructor
   return new HttpStatusRequestRejectedHandler();
}

Похоже, RequestRejectedHandler добавлен только в Spring Security 5.4: github.com/spring-projects/spring-security/blob/master/web/s‌ rc /… @since 5.4

ARK 11.05.2020 13:58

@ARK Да, ты прав, я адаптирую. Спасибо!

Ahmed Sayed 12.05.2020 10:33

Я вижу какое-то рабочее решение в недавнем изменении github в это коммит

Он должен работать, если вы зарегистрируете bean-компонент типа RequestRejectedHandler или, как я понимаю, также будет интеграция через WebSecurity в WebSecurityConfigurerAdapter. К сожалению, это изменение, вероятно, не будет включено в 2.3.3.RELEASE с использованием управления зависимостями. Он должен присутствовать в Spring Security Config 5.4.0-M1. Для управления зависимостями это версия 2.4.0-M1.

Рано или поздно люди, столкнувшиеся с этим ответом, должны увидеть это изменение в стандартной версии.

Спасибо за обновление. Я воздержусь от перехода на Spring 5, пока он не будет включен в выпуск. Между тем, моя вышеуказанная стратегия (см. Принятое решение) безупречно работает уже более 2 лет.

vallismortis 27.08.2020 18:06

Другие вопросы по теме