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.