web-dev-qa-db-fra.com

Comment intercepter une exception RequestRejectedException au printemps?

Je vois une tonne de RequestRejectedException entrées dans mon journal Tomcat (exemple collé ci-dessous). Ceux-ci ont commencé à apparaître dans mon fichier journal après une mise à niveau de version mineure (Spring Security 4.2.4, IIRC) il y a quelques mois, il s'agit donc clairement d'une nouvelle fonctionnalité de sécurité dans Spring qui est activée par défaut. Un problème similaire est signalé ici , mais ma question concerne spécifiquement comment intercepter ces exceptions dans un contrôleur. Il existe un bogue Spring Security documenté pour ce problème ( Fournir un moyen de gérer RequestRejectedException ). Cependant, ils ne ciblent pas de solution à ce problème avant le printemps 5.1.

Je comprends pourquoi ces exceptions sont levées , et je ne veux pas désactiver cette fonction de sécurité .

Je veux prendre un certain contrôle sur cette fonctionnalité afin que:

  1. Je sais que je ne bloque pas les utilisateurs légitimes de mon site.
  2. Je peux voir quelles demandes déclenchent cela (s'agit-il d'attaques par injection SQL?)
  3. Je peux ajuster la réponse du serveur. Le pare-feu Spring Security transfère une trace de pile complète vers le client Web (divulgation d'informations), avec un 500 Internal Server Error (ce qui est extrêmement incorrect, cela devrait être un 400 Bad Request).

Je veux trouver un moyen de consigner l'URL qui a été demandée, mais aussi supprimer la trace de pile spécifiquement pour ces exceptions car elles polluent mes fichiers journaux sans me donner d'informations utiles. Idéalement, j'aimerais intercepter ces exceptions et les gérer dans ma couche application au lieu de les signaler dans le journal Tomcat.

Par exemple, il s'agit de l'une des milliers de ces entrées de journal qui apparaissent chaque jour dans mon 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)

J'en vois plus de 3 200 en deux jours, et il est rapidement devenu le plus gros contributeur à mon catalina.out fichier journal, au point qu'il m'empêche de voir d'autres problèmes légitimes. Essentiellement, cette nouvelle fonctionnalité Spring Security est une forme de déni de service intégré, et elle a perdu des heures de mon temps depuis avril. Je ne dis pas que ce n'est pas une fonctionnalité importante, simplement que l'implémentation par défaut est complètement bâclée, et je veux trouver un moyen de prendre le contrôle, à la fois en tant que développeur et en tant qu'administrateur système.

J'utilise un contrôleur d'erreur personnalisé pour intercepter de nombreux autres types d'exceptions (y compris IOException) au printemps. Cependant, RequestRejectedException semble échouer pour une raison quelconque.

Ceci est la partie pertinente de mon ErrorController.Java, pour donner une idée de ce que j'essaie d'accomplir:

@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";
    }
}

Ce contrôleur d'erreur fonctionne pour de nombreuses exceptions. Par exemple, il a réussi à intercepter ce 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)
...

Toutefois, cela n'intercepte pas RequestRejectedException (comme indiqué par l'absence d '"erreur de serveur" dans le premier exemple de journal ci-dessus).

Comment puis-je intercepter RequestRejectedException dans un contrôleur d'erreur?

21
vallismortis

Il s'avère que bien que HttpFirewall et StrictHttpFirewall contiennent plusieurs erreurs de conception (documentées dans le code ci-dessous), il est à peine possible d'échapper à Spring Security One True Firewall = et tunneler les informations HttpFirewall via un attribut de requête vers un HandlerInterceptor qui peut transmettre ces requêtes marquées à un pare-feu réel (persistant) sans sacrifier l'entreprise d'origine logique qui les a marqués en premier lieu. La méthode documentée ici devrait être assez évolutive, car elle est conforme à un simple contrat de l'interface HttpFirewall, et le reste est simplement le noyau Spring Framework et Java API Servlet .

C'est essentiellement une alternative plus compliquée mais plus complète à ma réponse précédente . Dans cette réponse, j'ai implémenté une nouvelle sous-classe de StrictHttpFirewall qui intercepte et enregistre les demandes rejetées à un niveau de journalisation spécifique, mais ajoute également un attribut à la demande HTTP qui la signale pour les filtres (ou contrôleurs) en aval à gérer. De plus, cette AnnotatingHttpFirewall fournit une méthode inspect() qui permet aux sous-classes d'ajouter des règles personnalisées pour bloquer les requêtes.

Cette solution est divisée en deux parties: (1) Spring Security et (2) Spring Framework (Core), car c'est la fracture à l'origine de ce problème en premier lieu, et cela montre comment le combler.

Pour référence, cela est testé sur Spring 4.3.17 et Spring Security 4.2.6. Il peut y avoir des changements importants lors de la sortie de Spring 5.1.


Partie 1: Sécurité Spring

Il s'agit de la moitié de la solution qui effectue la journalisation et le signalement dans Spring Security.


AnnotatingHttpFirewall.Java

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 class AnnotatingHttpFirewall extends StrictHttpFirewall
{
    /**
     * The name of the HTTP header representing a request that has been rejected by this firewall.
     */
    public static final String HTTP_HEADER_REQUEST_REJECTED_FLAG = "X-HttpFirewall-RequestRejectedFlag";

    /**
     * The name of the HTTP header representing the reason a request has been rejected by this firewall.
     */
    public static final String HTTP_HEADER_REQUEST_REJECTED_REASON = "X-HttpFirewall-RequestRejectedReason";

    /**
     * Logger.
     */
    private static final Logger LOGGER = Logger.getLogger(AnnotatingHttpFirewall.class.getName());

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

    /**
     * Provides the request object which will be passed through the filter chain.
     *
     * @param request The original HttpServletRequest.
     * @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.
     */
    @Override
    public FirewalledRequest getFirewalledRequest(final HttpServletRequest request)
    {
        try
        {
            this.inspect(request); // Perform any additional checks that the naive "StrictHttpFirewall" misses.
            return super.getFirewalledRequest(request);
        } catch (RequestRejectedException ex) {
            final String requestUrl = request.getRequestURL().toString();

            // Override some of the default behavior because some requests are
            // legitimate.
            if (requestUrl.contains(";jsessionid="))
            {
                // Do not block non-cookie serialized sessions. Google's crawler does this often.
            } else {
                // Log anything that is blocked so we can find these in the catalina.out log.
                // This will give us any information we need to make
                // adjustments to these special cases and see potentially
                // malicious activity.
                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());
                }

                // Mark this request as rejected.
                request.setAttribute(HTTP_HEADER_REQUEST_REJECTED, Boolean.TRUE);
                request.setAttribute(HTTP_HEADER_REQUEST_REJECTED_REASON, ex.getMessage());
            }

            // Suppress the RequestBlockedException and pass the request through
            // with the additional attribute.
            return new FirewalledRequest(request)
            {
                @Override
                public void reset()
                {
                    return;
                }
            };
        }
    }

    /**
     * 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);
    }

    /**
     * Perform any custom checks on the request.
     * This method may be overridden by a subclass in order to supplement or replace these tests.
     *
     * @param request The original HttpServletRequest.
     * @throws RequestRejectedException if the request should be rejected immediately.
     */
    public void inspect(final HttpServletRequest request) throws RequestRejectedException
    {
        final String requestUri = request.getRequestURI(); // path without parameters
//        final String requestUrl = request.getRequestURL().toString(); // full path with parameters

        if (requestUri.endsWith("/wp-login.php"))
        {
            throw new RequestRejectedException("The request was rejected because it is a vulnerability scan.");
        }

        if (requestUri.endsWith(".php"))
        {
            throw new RequestRejectedException("The request was rejected because it is a likely vulnerability scan.");
        }

        return; // The request passed all custom tests.
    }
}

WebSecurityConfig.Java

Dans WebSecurityConfig, définissez le pare-feu HTTP sur AnnotatingHttpFirewall.

@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 AnnotatingHttpFirewall()); // Set the custom firewall.
        return;
    }
}

Partie 2: Spring Framework

La deuxième partie de cette solution pourrait être implémentée en tant que ServletFilter ou HandlerInterceptor. Je vais sur le chemin d'un HandlerInterceptor car il semble donner le plus de flexibilité et fonctionne directement dans le Spring Framework.


RequestBlockedException.Java

Cette exception personnalisée peut être gérée par un contrôleur d'erreur. Cela peut être étendu pour ajouter des en-têtes de demande, des paramètres ou des propriétés disponibles à partir de la demande brute (même la demande complète elle-même) qui peuvent être pertinents pour la logique métier de l'application (par exemple, un pare-feu persistant).

/**
 * A custom exception for situations where a request is blocked or rejected.
 */
public class RequestBlockedException extends RuntimeException
{
    private static final long serialVersionUID = 1L;

    /**
     * The requested URL.
     */
    private String requestUrl;

    /**
     * The remote address of the client making the request.
     */
    private String remoteAddress;

    /**
     * A message or reason for blocking the request.
     */
    private String reason;

    /**
     * The user agent supplied by the client the request.
     */
    private String userAgent;

    /**
     * Creates a new Request Blocked Exception.
     *
     * @param reqUrl The requested URL.
     * @param remoteAddr The remote address of the client making the request.
     * @param userAgent The user agent supplied by the client making the request.
     * @param message A message or reason for blocking the request.
     */
    public RequestBlockedException(final String reqUrl, final String remoteAddr, final String userAgent, final String message)
    {
        this.requestUrl = reqUrl;
        this.remoteAddress = remoteAddr;
        this.userAgent = userAgent;
        this.reason = message;
        return;
    }

    /**
     * Gets the requested URL.
     *
     * @return A URL.
     */
    public String getRequestUrl()
    {
        return this.requestUrl;
    }

    /**
     * Gets the remote address of the client making the request.
     *
     * @return A remote address.
     */
    public String getRemoteAddress()
    {
        return this.remoteAddress;
    }

    /**
     * Gets the user agent supplied by the client making the request.
     *
     * @return  A user agent string.
     */
    public String getUserAgent()
    {
        return this.userAgent;
    }

    /**
     * Gets the reason for blocking the request.
     *
     * @return  A message or reason for blocking the request.
     */
    public String getReason()
    {
        return this.reason;
    }
}

FirewallInterceptor.Java

Cet intercepteur est appelé après l'exécution des filtres Spring Security (c'est-à-dire après que AnnotatingHttpFirewall a signalé les demandes qui doivent être rejetées. Cet intercepteur détecte ces indicateurs (attributs) sur la demande et déclenche une exception personnalisée que notre contrôleur d'erreurs peut manipuler.

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

import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

/**
 * Intercepts requests that were flagged as rejected by the firewall.
 */
public final class FirewallInterceptor implements HandlerInterceptor
{
    /**
     * Default constructor.
     */
    public FirewallInterceptor()
    {
        return;
    }

    @Override
    public boolean preHandle(final HttpServletRequest request, final HttpServletResponse response, final Object handler) throws Exception
    {
        if (Boolean.TRUE.equals(request.getAttribute(AnnotatingHttpFirewall.HTTP_HEADER_REQUEST_REJECTED)))
        {
            // Throw a custom exception that can be handled by a custom error controller.
            final String reason = (String) request.getAttribute(AnnotatingHttpFirewall.HTTP_HEADER_REQUEST_REJECTED_REASON);
            throw new RequestRejectedByFirewallException(request.getRequestURL().toString(), request.getRemoteAddr(), request.getHeader(HttpHeaders.USER_AGENT), reason);
        }

        return true; // Allow the request to proceed normally.
    }

    @Override
    public void postHandle(final HttpServletRequest request, final HttpServletResponse response, final Object handler, final ModelAndView modelAndView) throws Exception
    {
        return;
    }

    @Override
    public void afterCompletion(final HttpServletRequest request, final HttpServletResponse response, final Object handler, final Exception ex) throws Exception
    {
        return;
    }
}

WebConfig.Java

Dans WebConfig, ajoutez le FirewallInterceptor au registre.

@EnableWebMvc
public class WebConfig extends WebMvcConfigurerAdapter
{
    /**
     * Among your other methods in this class, make sure you register
     * your Interceptor.
     */
    @Override
    public void addInterceptors(final InterceptorRegistry registry)
    {
        // Register firewall interceptor for all URLs in webapp.
        registry.addInterceptor(new FirewallInterceptor()).addPathPatterns("/**");
        return;
    }
}

ErrorController.Java

Cela gère spécifiquement l'exception personnalisée ci-dessus et produit une page d'erreur propre pour le client tout en enregistrant toutes les informations pertinentes et en invoquant toute logique métier spéciale pour un pare-feu d'application personnalisé.

import Java.io.IOException;
import Java.util.logging.Level;
import Java.util.logging.Logger;

import org.springframework.web.servlet.NoHandlerFoundException;

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;

import RequestBlockedException;

@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 AnnotatingHttpFirewall.
     *
     * @param request The original HTTP request.
     * @param ex A RequestBlockedException exception.
     * @return The tile definition name for the page.
     */
    @ExceptionHandler(RequestBlockedException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public String handleRequestBlockedException(final RequestBlockedException ex)
    {
        if (LOGGER.isLoggable(Level.WARNING))
        {
            LOGGER.log(Level.WARNING, "Rejected request from " + ex.getRemoteAddress() + " for [" + ex.getRequestUrl() + "]. Reason: " + ex.getReason());
        }

        // Note: Perform any additional business logic or logging here.

        return "errorPage"; // Returns a Nice error page with the specified status code.
    }

    /**
     * Generates a Page Not Found page.
     *
     * @param ex A NoHandlerFound exception.
     * @return The tile definition name for the page.
     */
    @ExceptionHandler(NoHandlerFoundException.class)
    @ResponseStatus(HttpStatus.NOT_FOUND)
    public String handleException(final NoHandlerFoundException ex)
    {
        return "notFoundPage";
    }
}

FirewallController.Java

Un contrôleur avec un mappage par défaut qui lance un NoHandlerFoundException. Cela contourne la stratégie de poulet et d'oeuf dans DispatcherServlet.noHandlerFound , permettant à cette méthode de toujours trouver un mappage de sorte que FirewallInterceptor.preHandle est toujours invoqué. Cela donne à RequestRejectedByFirewallException la priorité sur NoHandlerFoundException.

Pourquoi cela est nécessaire:

Comme mentionné ici , lorsqu'un NoHandlerFoundException est lancé à partir de DispatcherServlet (c'est-à-dire lorsqu'une URL demandée n'a pas de mappage correspondant), il n'y a aucun moyen de gérer les exceptions générées à partir du pare-feu ci-dessus (NoHandlerFoundException est jeté avant d'invoquer preHandle () ), donc ces demandes passeront à votre vue 404 (ce qui n'est pas le comportement souhaité dans mon cas - vous verra beaucoup de messages "Aucun mappage trouvé pour la requête HTTP avec URI ..."). Cela pourrait être corrigé en déplaçant la vérification de l'en-tête spécial dans la méthode noHandlerFound. Malheureusement, il n'y a aucun moyen de le faire sans écrire un nouveau servlet Dispatcher à partir de zéro, et vous pouvez également jeter l'ensemble du framework Spring. Il est impossible d'étendre DispatcherServlet en raison du mélange de méthodes protégées, privées et finales, et du fait que ses propriétés sont inaccessibles (pas de getters ou setters). Il est également impossible d'envelopper la classe car il n'y a pas d'interface commune pouvant être implémentée. Le mappage par défaut dans cette classe fournit un moyen élégant de contourner toute cette logique.

Avertissement important : le RequestMapping ci-dessous empêchera la résolution des ressources statiques car il a priorité sur tous les ResourceHandlers enregistrés. Je suis toujours à la recherche d'une solution de contournement pour cela, mais une possibilité pourrait être d'essayer l'une des méthodes de gestion des ressources statiques suggérées dans cette réponse .

import org.springframework.http.HttpHeaders;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.NoHandlerFoundException;

@Controller
public final class FirewallController
{
    /**
     * The name of the model attribute (or request parameter for advertisement click tracking) that contains the request URL.
     */
    protected static final String REQUEST_URL = "requestUrl";

    /**
     * The name of the model attribute that contains the request method.
     */
    protected static final String REQUEST_METHOD = "requestMethod";

    /**
     * The name of the model attribute that contains all HTTP headers.
     */
    protected static final String REQUEST_HEADERS = "requestHeaders";

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

    /**
     * Populates the request URL model attribute from the HTTP request.
     *
     * @param request The HTTP request.
     * @return The request URL.
     */
    @ModelAttribute(REQUEST_URL)
    public final String getRequestURL(final HttpServletRequest request)
    {
        return request.getRequestURL().toString();
    }

    /**
     * Populates the request method from the HTTP request.
     *
     * @param request The HTTP request.
     * @return The request method (GET, POST, HEAD, etc.).
     */
    @ModelAttribute(REQUEST_METHOD)
    public final String getRequestMethod(final HttpServletRequest request)
    {
        return request.getMethod();
    }

    /**
     * Gets all headers from the HTTP request.
     *
     * @param request The HTTP request.
     * @return The request headers.
     */
    @ModelAttribute(REQUEST_HEADERS)
    public final HttpHeaders getRequestHeaders(final HttpServletRequest request)
    {
        return FirewallController.headers(request);
    }

    /**
     * A catch-all default mapping that throws a NoHandlerFoundException.
     * This will be intercepted by the ErrorController, which allows preHandle to work normally.
     *
     * @param requestMethod The request method.
     * @param requestUrl The request URL.
     * @param requestHeaders The request headers.
     * @throws NoHandlerFoundException every time this method is invoked.
     */
    @RequestMapping(value = "/**") // NOTE: This prevents resolution of static resources. Still looking for a workaround for this.
    public void getNotFoundPage(@ModelAttribute(REQUEST_METHOD) final String requestMethod, @ModelAttribute(REQUEST_URL) final String requestUrl, @ModelAttribute(REQUEST_HEADERS) final HttpHeaders requestHeaders) throws NoHandlerFoundException
    {
        throw new NoHandlerFoundException(requestMethod, requestUrl, requestHeaders);
    }

    /**
     * Gets all headers from a HTTP request.
     *
     * @param request The HTTP request.
     * @return The request headers.
     */
    public static HttpHeaders headers(final HttpServletRequest request)
    {
        final HttpHeaders headers = new HttpHeaders();

        for (Enumeration<?> names = request.getHeaderNames(); names.hasMoreElements();)
        {
            final String headerName = (String) names.nextElement();

            for (Enumeration<?> headerValues = request.getHeaders(headerName); headerValues.hasMoreElements();)
            {
                headers.add(headerName, (String) headerValues.nextElement());
            }
        }

        return headers;
    }
}

Résultats

Lorsque les deux parties fonctionnent, vous verrez les deux avertissements suivants enregistrés (le premier est dans Spring Security, le second est Spring Framework (Core) ErrorController). Vous avez maintenant un contrôle total sur la journalisation et un pare-feu d'application extensible que vous pouvez ajuster selon vos besoins.

Sep 12, 2018 10:24:37 AM com.mycompany.spring.security.AnnotatingHttpFirewall getFirewalledRequest
WARNING: Intercepted org.springframework.security.web.firewall.RequestRejectedException: Remote Host: 0:0:0:0:0:0:0:1 User Agent: Mozilla/5.0 (Windows NT 6.3; Win64; x64; rv:62.0) Gecko/20100101 Firefox/62.0 Request URL: http://localhost:8080/webapp-www-mycompany-com/login.php
Sep 12, 2018 10:24:37 AM com.mycompany.spring.controller.ErrorController handleException
WARNING: Rejected request from 0:0:0:0:0:0:0:1 for [http://localhost:8080/webapp-www-mycompany-com/login.php]. Reason: The request was rejected because it is a likely vulnerability scan.
4
vallismortis

Il peut également être géré par un simple filtre, ce qui entraînera une réponse d'erreur 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);
        }
    }
}
20
lanwen

J'ai implémenté une sous-classe de StrictHttpFirewall qui enregistre les informations de demande sur la console et lève une nouvelle exception avec une trace de pile supprimée. Cela résout partiellement mon problème (au moins, je peux voir les mauvaises demandes maintenant).

Si vous voulez simplement voir les demandes rejetées sans la trace de pile, c'est la réponse que vous recherchez.

Si vous souhaitez gérer ces exceptions dans un contrôleur, veuillez vous référer à réponse acceptée pour une solution complète (mais légèrement plus complexe).


LoggingHttpFirewall.Java

Cette classe étend StrictHttpFirewall pour intercepter RequestRejectedException et lève une nouvelle exception avec des métadonnées de la demande et une trace de pile supprimée.

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

Dans WebSecurityConfig, définissez le pare-feu HTTP sur 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;
    }
}

Résultats

Après avoir déployé cette solution en production, j'ai rapidement découvert que le comportement par défaut de StrictHttpFirewall empêchait Google d'indexer mon site!

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

Dès que j'ai découvert cela, j'ai rapidement déployé une nouvelle version (incluse dans mon autre réponse ) qui cherche ;jsessionid= et autorise le traitement de ces demandes. Il peut également y avoir d'autres demandes qui devraient passer, et maintenant j'ai un moyen de les détecter.

8
vallismortis

Une autre façon de le gérer est d'utiliser Spring AOP . Nous pouvons créer un conseil autour de la méthode FilterChainProxy.doFilter () qui intercepte toute exception RequestRejectedException levée par le HttpFirewall et la traduit en 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);
        }
    }
}
0
Ahmed Sayed