Protection contre les vulnérabilités XSS dans une application Spring Boot

La faille XSS (Cross-Site Scripting) demeure l'une des vulnérabilités les plus courantes dans les applications web. Dans un environnement Spring Boot, une stratégie efficace pour atténuer ce risque consiste à intercepter les requêtes entrantes afin de ntetoyer les caractères spéciaux susceptibles d'exécuter des scripts malveillants.

1. Utilitaire d'extraction du corps de la requête

Comme le flux d'entrée d'une requête HTTP ne peut être lu qu'une seule fois, nous avons besoin d'une méthode utilitaire pour extraire le contenu du corps (body) afin de le traiter ultérieurement.

import javax.servlet.ServletRequest;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;

public class RequestPayloadUtils {
    /**
     * Extrait le corps de la requête sous forme de chaîne de caractères
     */
    public static String readBody(ServletRequest request) {
        StringBuilder content = new StringBuilder();
        try (BufferedReader reader = new BufferedReader(new InputStreamReader(request.getInputStream(), StandardCharsets.UTF_8))) {
            String line;
            while ((line = reader.readLine()) != null) {
                content.append(line);
            }
        } catch (IOException e) {
            // Loguer l'erreur selon votre framework de log
        }
        return content.toString();
    }
}

2. Personnalisation du Flux d'Entrée

Pour réinjecter le contenu nettoyé dans le cycle de vie de la requête, nous devons implémenter une version personnalisée de ServletInputStream.

import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import java.io.ByteArrayInputStream;
import java.io.IOException;

public class CleanServletInputStream extends ServletInputStream {
    private final ByteArrayInputStream buffer;

    public CleanServletInputStream(byte[] data) {
        this.buffer = new ByteArrayInputStream(data);
    }

    @Override
    public boolean isFinished() {
        return buffer.available() == 0;
    }

    @Override
    public boolean isReady() {
        return true;
    }

    @Override
    public void setReadListener(ReadListener listener) { }

    @Override
    public int read() throws IOException {
        return buffer.read();
    }
}

3. Le Wrapper de Requête pour le filtrage XSS

C'est ici que réside la logique principale. Nous étendons HttpServletRequestWrapper pour surcharger l'accès aux paramètres, aux en-têtes et au corps de la requête, en appliquant un encodage de sécurité.

import org.apache.commons.lang3.StringUtils;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.IOException;
import java.util.LinkedHashMap;
import java.util.Map;

public class XssRequestWrapper extends HttpServletRequestWrapper {
    private final String sanitizedBody;

    public XssRequestWrapper(HttpServletRequest request) {
        super(request);
        String originalBody = RequestPayloadUtils.readBody(request);
        this.sanitizedBody = sanitize(originalBody);
    }

    @Override
    public String getHeader(String name) {
        String value = super.getHeader(name);
        return sanitize(value);
    }

    @Override
    public String getParameter(String name) {
        return sanitize(super.getParameter(name));
    }

    @Override
    public String[] getParameterValues(String name) {
        String[] values = super.getParameterValues(name);
        if (values == null) return null;
        
        String[] encodedValues = new String[values.length];
        for (int i = 0; i < values.length; i++) {
            encodedValues[i] = sanitize(values[i]);
        }
        return encodedValues;
    }

    @Override
    public ServletInputStream getInputStream() throws IOException {
        final byte[] bytes = sanitizedBody.getBytes(getCharacterEncoding());
        return new CleanServletInputStream(bytes);
    }

    private String sanitize(String input) {
        if (StringUtils.isBlank(input)) {
            return input;
        }
        
        StringBuilder cleaner = new StringBuilder(input.length());
        for (int i = 0; i < input.length(); i++) {
            char c = input.charAt(i);
            switch (c) {
                case '<': cleaner.append('<'); break;
                case '>': cleaner.append('>'); break;
                case '\'': cleaner.append('‘'); break;
                case '\"': cleaner.append('"'); break;
                case '&': cleaner.append('&'); break;
                case '\\': cleaner.append('\'); break;
                case '#':  cleaner.append('#'); break;
                case '%':
                    if (isEncodedChar(input, i)) {
                        cleaner.append('%'); // Ou traitement spécifique
                    } else {
                        cleaner.append(c);
                    }
                    break;
                default: cleaner.append(c); break;
            }
        }
        return cleaner.toString();
    }

    private boolean isEncodedChar(String s, int index) {
        // Logique simplifiée pour détecter les tentatives d'encodage URL comme %3c
        if (index + 2 < s.length()) {
            String hex = s.substring(index + 1, index + 3).toLowerCase();
            return hex.equals("3c") || hex.equals("3e");
        }
        return false;
    }
}

4. Implémentation du Filtre Servlet

Le filtre intercepte chaque requête HTTP pour l'envelopper dans notre XssRequestWrapper.

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;

public class XssSecurityFilter implements Filter {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        XssRequestWrapper wrapper = new XssRequestWrapper((HttpServletRequest) request);
        chain.doFilter(wrapper, response);
    }
    
    @Override
    public void init(FilterConfig filterConfig) {}
    
    @Override
    public void destroy() {}
}

5. Configuration dans Spring Boot

Enfin, nous enregistrons le filtre dans le contexte Spring pour qu'il soit appliqué à l'ensemble de l'application.

import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class SecurityWebConfig {

    @Bean
    public FilterRegistrationBean<XssSecurityFilter> xssFilterRegistration() {
        FilterRegistrationBean<XssSecurityFilter> registration = new FilterRegistrationBean<>();
        registration.setFilter(new XssSecurityFilter());
        registration.addUrlPatterns("/*");
        registration.setName("xssSecurityFilter");
        registration.setOrder(1); // priorité haute
        return registration;
    }
}

Cette approche permet de neutraliser les injections de scripts en remplaçant les caractères de contrôle HTML par des équivalents visuels en pleine largeur (Full-width), empêchant ainsi le navigateur de les interpréter comme du code exécutable tout en préservant la lisibilité des données pour l'utilisateur.

Étiquettes: Java spring-boot security XSS servlet-filter

Publié le 28 juin à 00h55