Esigenza Reale
Proteggere le chiamate asincrone effettuate via fetch() o XMLHttpRequest contro attacchi di falsificazione della richiesta.
Analisi Tecnica
Problema: Le richieste asincrone falliscono con errore 403 perché non includono il segreto crittografico richiesto da Spring Security.
Perché: Esposizione sicura del token. Ho scelto di usare i meta-tag per centralizzare il segreto CSRF, rendendolo accessibile in modo trasparente a tutti i moduli JavaScript della pagina.
Esempio Implementativo
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
<!-- Inserisco i meta-tag CSRF nel layout base: disponibili in tutte le pagine.
Thymeleaf inietta i valori correnti del token generato da Spring Security
per questa sessione. --> <head> <meta name="_csrf" th:content="${
_csrf.token
}
"> <meta name="_csrf_header" th:content="${
_csrf.headerName
}
"> <!-- Oppure uso th:attr per sicurezza se il nome dell'attributo è dinamico:
--> <meta th:attr="name=${
_csrf.parameterName
}
,content=${
_csrf.token
}
"> </head>
/* Modulo JavaScript centralizzato (csrf.js): leggo i meta-tag una sola volta e
li rendo disponibili a tutti i moduli fetch della pagina. */
const CsrfUtils = (() => {
// Leggo i valori dal DOM una sola volta al caricamento della pagina const
tokenElement = document.querySelector('meta[name="_csrf"]')
;
const headerElement = document.querySelector('meta[name="_csrf_header"]');
if (!tokenElement || !headerElement) {
console.error('Meta-tag CSRF non trovati: le richieste POST falliranno
con 403.');
}
const token = tokenElement?.content;
const headerName = headerElement?.content;
/* Wrapper fetch con CSRF automatico: sostituzione drop-in di window.fetch
per tutte le richieste mutanti. */
function secureFetch(url, options = {
}
) {
const method = (options.method || 'GET').toUpperCase();
// GET e HEAD non necessitano di CSRF (idempotenti per design) if
(['GET', 'HEAD', 'OPTIONS'].includes(method))
{
return fetch(url, options);
}
// Aggiungo il token CSRF a tutte le richieste mutanti const headers =
new Headers(options.headers ||
{
}
);
headers.set(headerName, token);
return fetch(url, {
...options, headers
}
);
}
/* Funzione helper per form AJAX: serializza il FormData e aggiunge il CSRF.
*/
function submitForm(form) {
const formData = new FormData(form);
// Spring Security accetta il token anche come parametro di form formDat
a.append(document.querySelector('meta[name="_csrf"]')?.getAttribute(
'name') || '_csrf', token)
;
return fetch(form.action, {
method: form.method || 'POST', body: formData
// NON imposto Content-Type: il browser lo fa automaticamente con il
boundary multipart
}
);
}
return {
token, headerName, secureFetch, submitForm
}
;
}
)();
/* Esempio d'uso: */
async function deleteProduct(productId) {
const response = await CsrfUtils.secureFetch(`/api/products/${
productId
}
`, {
method: 'DELETE'
}
);
if (response.ok) {
document.getElementById(`product-${
productId
}
`).remove();
}
else if (response.status === 403) {
console.error('CSRF token non valido o scaduto: ricarica la pagina.');
}
}
/* Per HTMX, configuro il token CSRF globalmente: */
document.addEventListener('htmx:configRequest', (event) => {
// HTMX legge i meta-tag automaticamente se configurato correttamente
event.detail.headers[CsrfUtils.headerName] = CsrfUtils.token
;
}
);
/* Configurazione Spring Security per accettare il token sia come header che
come parametro: */
@Configuration public class SecurityConfig {
@Bean public SecurityFilterChain filterChain(HttpSecurity http) throws
Exception {
http .csrf(csrf -> csrf
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
// Permette la lettura del token via JS per SPA
.csrfTokenRequestHandler(new CsrfTokenRequestAttributeHandler()) )
// ... resto della configurazione
return http.build();
}
}