정보처리안전확보지원사 2023년 가을(레이와 5년) 오후 문제1 해설 ── 16건의 리뷰가 2건만 표시되는 저장형 XSS
· 업데이트: · Go Komura · 정보처리안전확보지원사, SC 시험, XSS, 크로스사이트 스크립팅, 웹 애플리케이션, 정보보안, 취약점, IPA, 세션 관리
수정 이력(5건, 최종 수정 2026년 09월 03일)
이 글에 적용한 변경 사항의 기록입니다. 보관해 둔 수정 전 버전은 DOI가 부여된 고정 URL에서 읽을 수 있습니다.
- Codex 리뷰에 따라 상담·문의 링크에 /ko/ 로케일 접두를 붙였습니다. 본문의 기술적인 주장은 바꾸지 않았습니다.
- 관련 기사 링크를 한국어 permalink에 맞추는 등 CI가 지적한 표시용 수정을 반영했습니다. 본문의 기술적인 주장은 바꾸지 않았습니다.
- 이 글의 지식 맵에 포함된 관계를 다시 검토했습니다. 본문보다 넓은 주장이 되어 있던 것(조건부에서만 성립하는 관계, 「막는다」가 아니라 「완화한다」에 해당하는 관계, 전제가 아니라 권고에 해당하는 관계)을 조건부로 고치거나 더 정확한 술어로 바꾸었습니다. 본문의 주장은 바꾸지 않았습니다.
- 글 맨 앞에 「이 글의 지식 맵」을 추가했습니다. 본문에서 다루는 개념 사이의 관계를 한 장의 그림으로 조감할 수 있습니다. 관계의 전체 목록(근거 URL·확신도·확인일 포함)과 주요 개념의 정의는 지식 맵 상세 페이지에 모았고, 기계 가독 데이터를 JSON-LD와 Turtle로 공개하고 있습니다.
- 어느 문제가 어느 섹션에 대응하는지를 나타낸 표를, 문제 책자를 열어 두라는 안내 바로 뒤에 두었습니다. 더불어 장 제목을 내용이 드러나는 표현으로 맞추고, 문제 책자의 기술을 이 글의 샘플로 어떻게 바꿔 썼는지의 대응표도 추가했습니다.
- 최초 공개
이 글을 인용하기(DOI(등록된 아카이브): 10.5281/zenodo.22175210)
아래 DOI는 이전에 등록된 아카이브를 가리키며 현재 본문과 다를 수 있습니다. 현재 본문을 참조할 때는 이 페이지의 URL을 사용하세요.
Go Komura (2026). 「정보처리안전확보지원사 2023년 가을(레이와 5년) 오후 문제1 해설 ── 16건의 리뷰가 2건만 표시되는 저장형 XSS」. 합동회사 코무라소프트. https://comcomponent.com/ko/blog/sc-exam-r5a-pm-q1-stored-xss/
- DOI(등록된 아카이브)
- 10.5281/zenodo.22175210
- DOI(마지막 등록 버전)
- 10.5281/zenodo.22175211
「리뷰가 16건 있어야 하는데, 2건만 표시된다」
정보처리안전확보지원사 시험 2023년도 가을 오후 문제1은, 이 이용자의 문의에서 시작합니다1. 화면 전환에 이상한 곳은 없고, 오류도 나오지 않습니다. 다만 표시 건수가 맞지 않습니다.
원인은 저장형 크로스사이트 스크립팅(XSS)이었습니다. 그러나 이 문제가 흥미로운 지점은 XSS라는 답 자체가 아닙니다. Q사(문제문에 등장하는 EC 사업자)가 이미 갖고 있던 「그럴듯한 대책」이 모조리 빠져나가고 있다는 점입니다. 리뷰 타이틀의 50자 제한은 깨졌고, 업로드에 필요한 토큰은 정규 절차로 입수되었으며, 훔친 세션ID는 외부 서버로 보내지지 않고 반출되었습니다.
이 글에서는 이 대문제를 한 문제씩 따라가며, 효과가 없었던 대책과, 효과가 있었어야 할 대책이 어디서 갈리는지를 정리합니다.
이 글에서 얻는 것은 시험의 해법(각 문제의 답안 예와 그 근거)에 더해, 실무에서 쓸 수 있는 XSS 대책의 전체 모습입니다. 시험 대책으로 읽는 분은 문제별 섹션을, 실무의 코드 리뷰 관점만 필요한 분은 9장과 10장을 먼저 읽어도 의미가 통하도록 썼습니다.
참고로, 웹사이트 전체 보안을 무엇을 기준으로 확인하면 되는지에 대한 조감은 IPA 「안전한 웹사이트를 만드는 방법」을 체크리스트로 쓰기에서 다룹니다. 이 글은 거기서 든 11가지 취약점 중 XSS 하나를 실례로 깊게 파는 글입니다.
1.먼저 결론
- 취약점의 종류는 저장형 XSS. 공격자의 문자열이 서버에 저장되어, 이후 페이지를 연 전원에게 HTML로 출력되고 있었다. 삽입된 스크립트가 DOM API를 쓰는 것과 DOM Based XSS인 것은 별개의 이야기
- 입력 문자 수 제한은 XSS 대책이 되지 않았다. 공격자는 게시를 15회로 나누고, 게시 사이에 끼는 HTML을 JavaScript 주석으로 건너뛰게 해 하나의 스크립트로 이었다
- 업로드용 토큰(CSRF 대책)도 XSS 대책이 되지 않았다. 공격 스크립트는 정상 화면과 같은 절차로 토큰을 취득한다. 동일 오리진에서 동작하는 이상, 정규 이용자가 할 수 있는 일은 모두 할 수 있다
- 훔친 세션ID는 외부로 보내지지 않았다. cookie 내용을 「a.png」라는 이미지 파일로 사이트 자신의 아이콘 업로드 기능에 올려 두고, 공격자는 그것을 평범하게 열람해 회수했다. 출구 대책으로는 탐지할 수 없다
- 문제문이 Q사에 빠져 있었다고 명시하는 것은 세 가지. 출력 시 이스케이프(근본적 해결)와 cookie의 HttpOnly 속성·업로드 파일의 형식 확인(이 둘은 보험적 대책). 어느 하나라도 있었다면 이 공격의 연쇄는 끊어져 있었다. 다만 형식 확인은 수법을 바꾸면 통과되므로 재인코딩까지 필요해진다
- 문제문에는 나오지 않지만, CSP의
script-src도 같은 연쇄를 끊을 수 있다.'unsafe-inline'을 허용하지 않는 구성이라면, 삽입된 인라인 스크립트는 애초에 실행되지 않는다
그림의 실선은 항상 성립하는 관계, 점선은 조건이 붙는 관계입니다(성립 조건은 상세 페이지의 관계별 설명에 적혀 있습니다). 관계 전체 목록(총 25건, 근거와 확신도 포함)과 주요 개념의 정의는 지식 맵 상세 페이지에 정리되어 있습니다(일본어). 데이터: JSON-LD / Turtle
2.소재에 대하여 ── 출처와, 이 글에서의 취급
다루는 것은 다음 문제입니다.
출처:2023년도 가을 정보처리안전확보지원사 시험 오후 문제1
IPA는 공표한 과거의 시험 문제에 대해, 법령에 특별한 정함이 있는 경우를 제외하고 허락이나 사용료는 필요 없다고 합니다. 다만 저작권을 포기한 것은 아니며, 출처를 「년도, 시기, 시험 구분, 시간 구분, 문제 번호 등」의 형식으로 명기할 것, 문제의 일부를 개변한 경우에는 그 취지도 명기할 것을 요구합니다2.
이 글에서는 문제 책자에 실린 HTML이나 스크립트를 그대로 전재하지 않습니다. 구조 설명에 필요한 범위에서, 당사가 다시 쓴 등가의 샘플 코드로 바꿔 두었습니다. 문제문과 답안 예도 요약해 다룹니다. 문제 책자·답안 예·채점 강평의 원문은 IPA 페이지에서 무상으로 다운로드할 수 있으므로, 곁에 열어 두고 읽기를 권합니다1 3 4.
문제와 이 글의 대응
문제 책자를 열어 두고 읽는 분을 위해, 문제와 이 글 섹션의 대응을 보입니다. 풀고 싶은 문제부터 읽기 시작해도 됩니다.
| 문제 | 묻는 것(자수) | 이 글의 대응 섹션 |
|---|---|---|
| 문제1(1) | 악용된 XSS 취약점의 종류(3지선다) | 4장 |
| 문제1(2) | Web 앱 Q에서의 대책(30자 이내) | 4장 「문제1(2):대책」 |
| 문제2 | 입력 문자 수 제한을 넘는 길이의 스크립트를 실행시킨 방법(50자 이내) | 5장 |
| 문제3(1) | 공격 스크립트 6〜20행째의 처리 내용(60자 이내) | 6장 |
| 문제3(2) | 공격자가 업로드된 정보를 취득하는 방법(50자 이내) | 7장 |
| 문제3(3) | 취득한 정보로 할 수 있는 일(40자 이내) | 7장 「문제3(3):세션ID로 무엇을 할 수 있는가」 |
| 문제4 | 공격자의 도메인에서는 공격이 성공하지 않는, 브라우저의 구조(40자 이내) | 8장 |
문제를 떠나 실무 이야기만 필요한 분은 9장(효과가 있었던 대책과 없었던 대책의 목록)과 10장(코드 리뷰 관점)부터 읽으십시오. 공격의 연쇄 자체를 한 장으로 보고 싶다면, 7장의 시퀀스 그림이 게시부터 회수까지의 전체 모습입니다.
문제 책자의 기술과, 이 글 샘플의 대응
원문과 대조할 수 있도록, 어디를 어떻게 바꿔 썼는지를 정리합니다.
| 문제 책자의 기술 | 이 글에서의 취급 | 게재 위치 |
|---|---|---|
| 페이지 V의 HTML(분할 게시된 리뷰 타이틀을 포함) | 그대로 전재하지 않고, 같은 구조를 3건의 게시로 줄인 등가 샘플을 당사에서 다시 씀 | 5장 |
| 추출된 공격 스크립트(약 20행) | 그대로 전재하지 않고, 같은 처리를 하는 등가 JavaScript를 당사에서 다시 씀. 코드 안의 주석은 이 글의 해설용 | 6장 |
| 각 문제의 문제문 | 요지를 유지한 요약(자수 제한 등의 조건은 원문의 값) | 4〜8장의 각 모두 |
| 답안 예 | IPA가 공표한 답안 예3 | 4〜8장의 각처 |
| 채점 강평 | IPA가 공표한 채점 강평의 해당 부분4 | 4장·5장·7장 |
| 문제문의 사양(Q사, 페이지 V, 회원 A/B, 기능과 자수 제한 등) | 원문의 기술에 따라 요약 | 2장 「문제의 무대」 |
문제의 무대
등장하는 것은 의류 EC 사업을 하는 직원 100명의 Q사입니다. 자체 개발한 「Web 앱 Q」로 EC 사이트를 운영하며, 이용자는 HTTPS로 접속합니다. 이번에 회원의 상품 리뷰 기능을 추가했다는 설정입니다.
짚어 둘 사양은 다음 다섯 가지입니다.
| 기능 | 사양 |
|---|---|
| 로그인 | 회원 ID와 비밀번호로 인증하고, 세션ID를 cookie로 발급한다 |
| 상품 리뷰 | 로그인된 회원만 게시할 수 있다. 리뷰 타이틀은 50자, 리뷰 상세는 300자의 입력 문자 수 제한이 있으며, 모두 자유 기술 |
| 회원 프로필 | 아이콘 이미지를 업로드하는 페이지와 신용카드 정보를 등록하는 페이지를 제공한다. 둘 다 로그인된 회원만 이용할 수 있다 |
| 아이콘 이미지 업로드 | /user/upload 에 대해 이미지 파일과 토큰을 파라미터로 전송한다. 토큰이 /user/profile 에서 발급된 것과 일치할 때만 성공한다 |
| 아이콘 이미지 표시 | 업로드된 아이콘 이미지는 회원 프로필 설정 페이지나 리뷰 페이지에 표시된다 |
마지막 두 행이 나중에 효력을 발휘합니다.
3.증상 ── 왜 16건이 2건이 되었는가
회원으로부터 「무지 T셔츠의 리뷰 페이지(페이지 V)에 16건 표시되어야 할 리뷰가 2건만 표시된다」는 문의가 들어옵니다. 개발부의 N 씨가 페이지 V를 열자, 화면상은 다음과 같이 보였습니다.
- 헤더에는 「16건의 리뷰」
- 회원 A의 리뷰가 1건(타이틀 「Good」, 본문 「Nice shirt!」)
- 회원 B의 리뷰가 1건
- 끝에 「이상, 전 16건의 리뷰」
건수 표시는 16건, 실제로 늘어선 것은 2건. 여기서 HTML을 확인하면, 회원 A의 게시가 15건 존재하고, 거기에 긴 스크립트가 삽입되어 있음이 드러납니다.
즉 리뷰 자체는 16건분이 HTML로 출력되고 있습니다. 그런데도 2건만 보이는 것은, 그 대부분이 <script> 요소의 내용이 되어 버려, 브라우저가 「표시해야 할 콘텐츠」로 다루지 않게 되었기 때문입니다.
삼켜지는 범위는 정확히는 「15건 통째」가 아닙니다. 시작 태그 <script> 는 1건째 리뷰 타이틀의 중간(「Good」 직후)에 나타나고, 종료 태그 </script> 는 15건째 리뷰 타이틀의 끝에 있습니다. 따라서,
- 1건째의 카드(아이콘·표시명·날짜·★·타이틀의 「Good」까지)는
<script>보다 앞에 있으므로 표시된다 - 거기서부터 15건째 타이틀 끝까지가 script 요소의 내용으로 삼켜진다
- 15건째의 나머지(본문의 「Nice shirt!」)는
</script>보다 뒤에 있으므로 표시된다
결과적으로 1건째의 헤더 부분과 15건째의 본문이 이어져, 화면상은 「타이틀이 Good이고 본문이 Nice shirt!인, 회원 A의 리뷰 1건」으로 보입니다. 여기에 회원 B의 1건을 더해 2건입니다. 공격자가 1건째 첫머리에 「Good」, 15건째 본문에 「Nice shirt!」라는 자연스러운 문자열을 둔 것은, 표시 붕괴를 부자연스럽게 보이지 않기 위해서일 것입니다.
이 「건수는 맞는데 표시가 맞지 않는다」는 증상의 읽기는 실무에서도 도움이 됩니다. 표시 건수 로직의 버그가 아니라, 출력된 HTML 구조가 깨져 있을 가능성을 의심하는 입구가 되기 때문입니다.
4.왜 「저장형」 XSS인가 ── 문제1
문제1에서는 이 공격에 쓰인 XSS 취약점의 종류를 DOM Based XSS·저장형 XSS·반사형 XSS의 3지선다에서 고르게 합니다. 정답은 저장형 XSS입니다.
그리고 이 문제에 대해 IPA의 채점 강평은 이렇게 적습니다.
정답률은 평균적이었으나, 스크립트에서 DOM을 사용하고 있었기 때문인지, “DOM Based XSS”라고 잘못 답한 수험자가 종종 보였다.
문제문에 나오는 공격 스크립트는 XMLHttpRequest 를 쓰고, 응답을 DOM으로 받아, getElementById 로 요소를 꺼냅니다. 분명히 DOM을 만지고 있습니다. 그러나 그것은 취약점의 종류와는 관계가 없습니다.
갈림길은 「공격 문자열이 어디서 스크립트가 되는가」
세 유형을 나누는 것은, 공격 문자열이 어디서 실행 가능한 코드로 바뀌는가, 즉 취약한 출력 지점(싱크)이 서버 측에 있는가 브라우저 측에 있는가입니다.
| 종류 | 싱크(공격 문자열이 실행 가능해지는 장소) | 공격 문자열은 어디서 오는가 |
|---|---|---|
| 반사형 XSS | 서버가 HTML을 조립하는 처리 | 공격자가 준비한 URL의 파라미터 등, 그 요청 자체 |
| 저장형 XSS | 서버가 HTML을 조립하는 처리 | 서버 측에 저장된 데이터 |
| DOM Based XSS | 브라우저상의 JavaScript(innerHTML 에의 대입, eval, document.write 등) |
URL의 프래그먼트, postMessage, 서버에서 받은 값 등 |
이 문제에서는 공격자가 게시한 리뷰 타이틀을 서버가 그대로 HTML에 삽입해 반환하고 있었습니다. 싱크는 서버 측 출력 처리에 있고, 그 문자열은 데이터베이스에 저장되어 이후 페이지 V를 연 전원에게 배포됩니다. 따라서 저장형입니다.
flowchart TD
A["공격자의 문자열이<br/>스크립트로 실행되었다"] --> B{"실행 가능해진 것은<br/>어디인가"}
B -->|"브라우저상의 JS가<br/>innerHTML 등에<br/>넘겼을 때"| C["DOM Based XSS"]
B -->|"서버가 조립한<br/>HTML에 들어 있었다"| D{"그 문자열은<br/>서버에 저장되어<br/>있는가"}
D -->|"저장되어 있다<br/>(이후, 전원에게 출력)"| E["저장형 XSS"]
D -->|"저장되어 있지 않다<br/>(그 요청에만)"| F["반사형 XSS"]
참고로, 「서버 응답에 공격 문자열이 들어 있으면 저장형/반사형」으로 단순화하는 것은 위험합니다. 서버가 값을 무해한 텍스트나 JSON으로 반환하더라도, 그것을 브라우저 측 JavaScript가 innerHTML 에 대입하면 DOM Based XSS가 됩니다(저장된 값이 원인이 되는, 이른바 stored DOM XSS입니다). 이 경우 고쳐야 할 곳은 서버의 출력 처리가 아니라 클라이언트 측 싱크이므로, 응답에 문자열이 보이는지가 아니라, 어디서 실행 가능해졌는지로 판단하십시오.
삽입된 스크립트가 무엇을 하는가(DOM을 만진다, 통신한다, cookie를 읽는다)와, 그 스크립트가 어떻게 페이지에 들어왔는가는 따로 생각해야 합니다. 혼동하기 쉬운 것은, 대책을 고를 때 곤란해서가 아니라 대책을 틀리기 때문입니다. DOM Based XSS라고 판단하면 「클라이언트 측 JavaScript를 고치면 된다」는 결론이 되지만, 실제로 고쳐야 할 곳은 서버 측 출력 처리였습니다.
문제1(2):대책
문제1(2)는 Web 앱 Q에서의 대책을 30자 이내로 답하게 합니다. 답안 예는 「리뷰 타이틀을 출력하기 전에 이스케이프 처리를 실시한다」입니다.
여기서 「출력하기 전에」라는 순서가 명시되어 있는 점이 중요합니다. 입력을 받았을 때 이스케이프하는 것이 아니라, HTML로 출력하기 직전에, 출력 대상의 문맥에 따라 이스케이프합니다. IPA 「안전한 웹사이트를 만드는 방법」에서도 XSS의 근본적 해결 맨 앞에 「웹페이지에 출력하는 모든 요소에 대해 이스케이프 처리를 실시한다」가 들어 있습니다5.
「SQL 인젝션에서는 입력 이스케이프가 중요하지 않은가?」
여기서 반드시 나오는 의문입니다. 결론부터 말하면, SQL 인젝션에서도 「입력 시 이스케이프」는 대책이 아닙니다.
IPA가 SQL 인젝션의 근본적 해결로 드는 것은 「SQL문의 조립은 모두 플레이스홀더로 구현한다」입니다. 문자열 연결로 SQL문을 조립할 수밖에 없는 경우의 대안으로 이스케이프 처리가 제시되어 있지만, 그것도 「SQL문의 조립을 문자열 연결로 하는 경우에는, 이스케이프 처리 등을 수행하는 데이터베이스 엔진의 API를 써서 SQL문의 리터럴을 올바르게 구성한다」고 적혀 있으며, 이스케이프하는 것은 SQL문을 구성하는 순간입니다6. 입력을 받은 시점이 아닙니다.
즉 HTML의 경우와 완전히 같은 구조입니다. 공통 원칙은 이렇게 됩니다.
이스케이프는, 데이터가 어떤 문법의 세계로 나갈지가 정해지는 장소에서 수행한다.
- HTML로 나간다면, HTML 이스케이프
- SQL문의 일부가 된다면, 플레이스홀더(어쩔 수 없다면 SQL 리터럴로서의 이스케이프)
- 셸 명령의 일부가 된다면, 셸 방식의 처리
왜 입력 시에는 안 되는가. 입력을 받은 시점에는, 그 데이터가 장차 어디로 나갈지 정해져 있지 않기 때문입니다. 같은 리뷰 본문이 HTML 페이지에도, CSV 내보내기에도, JSON API 응답에도, 알림 메일에도, 로그에도 나갑니다. 입력 시점에 HTML 이스케이프를 걸면 CSV에는 & 라는 문자열이 그대로 나오고, 데이터베이스 내용은 원래 입력과 달라져 검색도 집계도 어긋납니다. 이중 이스케이프의 온상이 되기도 합니다.
그렇다면 입력 측은 아무것도 하지 않아도 되는가
아닙니다. 다만 입력 측에서 하는 일은 이스케이프가 아니라 검증(밸리데이션)입니다. 역할이 다릅니다.
- 입력에서 「걸러낸다」 ── 사양상 있을 수 없는 값을 받지 않는다. 우편번호란이라면 숫자 7자리 이외를 거부하고, 수량란이라면 음수를 거부한다. 이는 데이터의 올바름을 지키기 위한, 독립적으로 필요한 처리
- 출력에서 「이스케이프한다」 ── 받아들인 데이터를, 나가는 곳의 문법으로 안전하게 표현한다. 이것이 취약점의 근본적 해결
그리고 입력 측 검증에 XSS 대책을 과도하게 기대하지 않는 것도 중요합니다. IPA는 XSS에 대해, 입력값이 애플리케이션 사양에 맞는지 확인하는 방법에 언급한 뒤, 이 대책의 유효성은 한정적이며, 애플리케이션이 요구하는 사양이 폭넓은 문자 종류 입력을 허용하는 경우에는 대책이 되지 않으므로, 이 방법에 의존하는 것은 권장되지 않는다고 명기합니다5.
이번 리뷰 타이틀과 리뷰 상세는 바로 자유 기술로 폭넓은 문자 종류를 허용하는 사양이었습니다. 50자나 300자라는 상한은 있지만, 스크립트를 돌리는 데 필요한 문자 수는 그보다 훨씬 적으므로, 상한이 있다는 것 자체는 방어가 되지 않습니다. 실제로 Q사에서 무엇이 일어났는지는, 이어지는 문제2에서 봅니다.
5.50자 제한은 어떻게 깨졌는가 ── 문제2
문제2는 이 문제의 백미입니다.
그림3에 대해, 입력 문자 수 제한을 넘는 길이의 스크립트가 실행되도록 한 방법을, 50자 이내로 답하라.
답안 예는 「HTML이 주석 처리되어 하나의 스크립트가 되도록 하는 게시를 여러 차례로 나누어 수행했다」입니다.
공격자는 1건의 게시에 들어가는 길이로 나누어 15회 게시했습니다. 핵심은 게시와 게시 사이에 반드시 끼는 HTML(</div> 나 <div class="..."> 등)을 어떻게 처리하는가입니다. 이들은 JavaScript로서는 구문 오류가 됩니다.
거기서 쓰인 것이 JavaScript의 블록 주석입니다. 다음은 구조를 설명하기 위해 당사에서 다시 쓴 간략판입니다(문제 책자의 그림을 그대로 인용한 것이 아닙니다).
3회로 나누어, 리뷰 타이틀란에 이렇게 게시했다고 합시다.
1건째: すばらしい<script>a=1;/*
2건째: */b=2;/*
3건째: */c=3;</script>
서버는 이를 각각의 리뷰 타이틀로, 다음과 같이 HTML에 출력합니다.
<div class="review-title">すばらしい<script>a=1;/*</div>
<div class="description">…</div>
<div class="review-title">*/b=2;/*</div>
<div class="description">…</div>
<div class="review-title">*/c=3;</script></div>
브라우저에서 보면, 첫 <script> 부터 마지막 </script> 까지가 하나의 script 요소입니다. 그 내용은 다음과 같이 됩니다.
a=1;/*</div>
<div class="description">…</div>
<div class="review-title">*/b=2;/*</div>
<div class="description">…</div>
<div class="review-title">*/c=3;
/* 와 */ 에 끼인 HTML은 JavaScript 주석으로 건너뛰어지고, 실제로 실행되는 것은 a=1; b=2; c=3; 뿐입니다. 그리고 주석으로 건너뛰어진 부분도 script 요소의 내용인 이상, 화면에는 표시되지 않습니다. 이것이 「16건이 2건으로 보이는」 증상의 정체입니다.
여기서 가져가야 할 것
여기서 인과를 거꾸로 잡지 마십시오. 입력 문자 수 제한이 XSS 대책이 되지 않는 것은, 게시 횟수가 무제한이었기 때문이 아닙니다. 애초에 50자면 스크립트는 실행할 수 있기 때문입니다. 이벤트 핸들러 속성을 하나 덧쓰거나, 외부 스크립트를 읽는 태그를 하나 두는 정도라면 모두 수십 자로 충분합니다. 게시를 1건으로 제한하더라도, 문자 수 제한이 방어가 되지는 않습니다.
그렇다면 이번 분할 게시는 무엇이었는가. 그것은 20행가량의 긴 스크립트를 통째로 들여오기 위한 수단입니다. 공격자가 하고 싶은 일이 길어서 횟수로 벌었다는 이야기이지, 문자 수 제한이 깨진 이유 그 자체는 아닙니다. 수법의 설명과, 대책으로서의 평가는 나누어 생각해야 합니다.
같은 말은 다른 「입력 측 제한」 전반에도 해당합니다.
- 문자 수 제한, 입력 가능 문자 종류 제한, 프론트엔드 밸리데이션 ── 이들은 사양상 필요한 것이지, XSS의 근본적 해결을 대신하지 않는다
- 공격자는 분할·인코딩·다른 경로에서의 투입과 같은 수단으로, 입력 측 제한을 우회할 여지를 항상 갖고 있다
- 지켜야 할 장소는, 데이터가 HTML로 나가는 순간이다
참고로 IPA의 채점 강평은 이 문제에 대해 「”개발자 도구로 입력 제한을 삭제한 뒤 게시했다”와 같이, 확인이 부족하다고 여겨지는 답이 일부 보였다」고도 적습니다. 확실히 프론트엔드 제한은 개발자 도구로 벗길 수 있습니다. 그러나 이 문제에서 요구된 것은, HTML에 남은 흔적에서 실제로 무엇이 행해졌는가를 읽어내는 일입니다. 그림3의 HTML에는 제한 안에 들어가는 길이의 게시가 15건으로 나뉘어 늘어서고, 각각의 첫머리와 끝에 주석 기호가 놓여 있었습니다. 이는 제한을 「벗긴」 흔적이 아니라, 제한을 「우회한」 흔적입니다. 공격자가 남긴 것을 하나씩 확인하는 자세가 요구되고 있었다고 읽어야 할 것입니다.
입력값을 어디까지 어떻게 검증할 것인가라는 설계의 사고방식은, QR 코드 판독값을 앱에서 어떻게 검증하는가에서도, 외부에서 들어오는 데이터를 신뢰하지 않는 설계로 정리하고 있습니다.
6.스크립트는 무엇을 했는가 ── 문제3(1)
N 씨가 추출한 스크립트는 약 20행입니다. 문제3(1)은 그중 6행째부터 20행째(1회째 통신이 성공한 뒤 실행되는 부분)의 처리 내용을 60자 이내로 답하게 합니다.
답안 예는 「XHR의 응답에서 취득한 토큰과 함께, 아이콘 이미지로 세션ID를 업로드한다」입니다.
이하는 이 동작을 설명하기 위해 당사에서 다시 쓴 등가 코드입니다(문제 책자의 그림을 그대로 인용한 것이 아닙니다).
// ① 먼저 프로필 설정 페이지를 취득한다
const xhr = new XMLHttpRequest();
xhr.open("get", "https://example.jp/user/profile");
xhr.responseType = "document"; // 응답을 텍스트가 아니라 DOM으로 받는다
xhr.send();
xhr.onload = function () {
// ② 정상 화면과 같은 절차로 업로드용 토큰을 읽는다
const token = xhr.response.getElementById("token").value;
// ③ cookie(세션ID를 포함)를, 그대로 PNG 파일의 내용으로 만든다
const file = new File([document.cookie], "a.png", { type: "image/png" });
// ④ 이 사이트의 아이콘 이미지 업로드 기능으로 전송한다
const form = new FormData();
form.append("uploadfile", file);
form.append("token", token);
const xhr2 = new XMLHttpRequest();
xhr2.open("post", "https://example.jp/user/upload");
xhr2.send(form);
};
전반 5행은 Q사의 CSRF 대책을 통과하기 위해서만 존재한다
이 스크립트에서 주목할 점은, 전반이 통째로 토큰 취득을 위해 쓰이고 있다는 것입니다.
Q사는 아이콘 이미지 업로드 때 「/user/profile 에서 발급된 토큰과 일치할 것」을 요구하고 있었습니다. 이는 다른 사이트에 둔 가짜 폼에서 무단으로 업로드되는 것을 막기 위한 장치, 즉 CSRF 대책입니다.
그러나 공격 스크립트는 피해자의 브라우저 안에서, 피해자와 같은 오리진에서 동작합니다. 그렇다면 정상 화면이 하는 일을 그대로 하면 됩니다. 프로필 설정 페이지를 GET하고, 그 안의 token 이라는 요소의 값을 읽습니다. 그것만으로 토큰은 손에 들어옵니다.
CSRF 대책 토큰은 XSS를 막지 못합니다. XSS가 성립한 시점에서, 공격자의 코드는 「로그인된 정규 이용자」로 행동할 수 있기 때문입니다. 토큰이 지키는 것은 「다른 오리진에서 무단으로 오는 요청」이지, 「같은 오리진에서 동작하는 악의 있는 스크립트」가 아닙니다.
이 구분은 실무에서 보안 요건을 읽을 때에도 효력이 있습니다. 「CSRF 토큰을 넣고 있으니 괜찮습니다」라는 설명은 CSRF에 대해서는 옳아도, XSS에 대해서는 아무것도 말하지 않습니다.
File 객체가 갖는 의미
③의 행도 볼 만합니다. document.cookie 의 문자열을 내용으로 하고, 파일명을 a.png, MIME 타입을 image/png 로 한 파일 객체를 만들고 있습니다.
내용은 그냥 텍스트입니다. PNG 파일이 아닙니다. 그런데도 업로드가 성공한 것은, 문제문에 적힌 대로 Web 앱 Q가 업로드된 이미지 파일의 형식을 검사하지 않았기 때문입니다.
그리고 document.cookie 를 읽을 수 있었던 것은, cookie에 HttpOnly 속성이 부여되어 있지 않았기 때문입니다. RFC 6265는 HttpOnly 속성에 대해 「cookie의 스코프를 HTTP 요청으로 한정한다. 특히, cookie를 스크립트에 공개하는 웹 브라우저 API와 같은 『비 HTTP』 API를 통해 cookie에 대한 접근을 제공할 때, 그 cookie를 생략하도록 사용자 에이전트에 지시한다」고 정합니다7. 이 속성이 붙어 있었다면, ③의 행이 잡는 문자열에서 세션ID만 빠지므로, 반출해도 가치 없는 것이 되어 있었습니다.
여기서 주의할 점은, HttpOnly가 숨기는 것은 그 속성이 붙은 cookie뿐이라는 점입니다. 같은 오리진에 HttpOnly가 아닌 cookie(표시 설정이나 계측용 ID 등)가 있으면, document.cookie 는 그것들을 계속 반환합니다. 「HttpOnly를 붙이면 document.cookie 가 비게 된다」는 뜻이 아닙니다. 지켜야 할 것은 세션ID이므로, 세션ID의 cookie에 확실히 붙어 있는지를 개별적으로 확인하십시오.
7.밖으로 나가지 않는 정보 반출 ── 문제3(2)(3)
문제3(2)는 「공격자는 업로드된 정보를 어떻게 취득할 수 있는가」를 50자 이내로 묻습니다. 답안 예는 「회원의 아이콘 이미지를 다운로드해, 거기서 세션ID 문자열을 꺼낸다」입니다.
떠올려 보십시오. 문제문의 사양에는 이렇게 적혀 있었습니다.
업로드한 아이콘 이미지는, 회원 프로필 설정 페이지나 리뷰 페이지에 표시된다.
즉 피해자의 세션ID가 쓰인 「아이콘 이미지」는, 사이트상에서 열람할 수 있는 장소에 놓입니다. 공격자는 특별한 일을 할 필요가 없습니다. 피해자의 아이콘 이미지가 표시된 페이지를 열고, 그 이미지의 URL을 얻는 것만입니다. 내용은 텍스트이므로, 열면 세션ID를 읽을 수 있습니다.
덧붙이면, 이 회수가 순순히 되는 것은 피해자의 아이콘 이미지가 공격자에게 보이는 페이지(자신이 리뷰를 게시한 상품의 리뷰 페이지 등)에 나와 있는 경우입니다. 아이콘 URL은 회원마다 정해져 있으므로, 한 번 어딘가에서 보이면 이후에는 그 URL을 직접 가져가면 됩니다. 어느 쪽이든 공격자 측 조작은 정규 열람자로서 페이지나 이미지를 GET하는 것뿐이며, 사이트에서 보면 이상 통신이 되지 않습니다.
sequenceDiagram
autonumber
participant AT as 공격자
participant Q as Web 앱 Q<br/>(example.jp)
participant V as 피해자의 브라우저
Note over AT,Q: 준비 단계
AT->>Q: 리뷰를 15회로 나누어 게시<br/>(주석 기호로 HTML을 건너뛴다)
Note over Q: 게시된 문자열을<br/>그대로 저장<br/>(저장은 이것으로 올바르다)
Note over V,Q: 피해자가 페이지 V를 연다
V->>Q: GET 페이지 V
Note over Q: ★취약점은 여기★<br/>저장된 타이틀을<br/>이스케이프하지 않고 HTML에 삽입
Q-->>V: 공격 스크립트를 포함한 HTML
Note over V: script 요소로 실행된다
V->>Q: GET /user/profile
Q-->>V: 토큰을 포함한 HTML
Note over V: document.cookie 를 읽어<br/>a.png 의 내용으로 만든다
V->>Q: POST /user/upload<br/>(uploadfile=a.png, token=…)
Note over Q: 형식을 확인하지 않고 저장<br/>아이콘 이미지로 공개
Note over AT,Q: 회수
AT->>Q: GET 피해자의 아이콘 이미지
Q-->>AT: 세션ID가 적힌 파일
출구 대책으로는 멈추지 않는다
이 경로의 까다로움은 그림으로 그리면 분명합니다. 피해자의 브라우저는 처음부터 끝까지 정규 사이트(example.jp)하고만 통신합니다.
그 결과, 통신을 감시하는 종류의 대책은 모조리 헛칩니다. 효과가 있는 것은 마지막 하나뿐입니다.
| 대책 | 이 공격에 대해 | 이유 |
|---|---|---|
| 프록시의 URL 필터링 | 효과 없음 | 접속처는 업무상 정당한 EC 사이트뿐 |
| 방화벽의 외부 발신 통신 감시 | 효과 없음 | 낯선 도메인으로의 통신이 발생하지 않음 |
Content Security Policy의 connect-src 제한('self' 등 동일 오리진을 허용하는 지정) |
효과 없음 | 두 번의 통신은 모두 동일 오리진 대상이며, 정책의 허용 범위에 들어감 |
Content Security Policy의 script-src 제한('unsafe-inline' 을 허용하지 않는 구성) |
효과 있음 | 삽입된 인라인 스크립트가 애초에 실행되지 않음 |
XSS에 의한 정보 반출이라고 하면 「공격자의 서버로 cookie가 전송된다」는 그림을 떠올리기 쉽지만, 반출처가 피해자 사이트 자신이 될 수 있다는 것을 이 문제는 분명히 보여 줍니다. 쓰기 가능하고, 또한 읽기 가능한 장소가 사이트 안에 있으면, 그곳이 주고받는 장소가 됩니다. 파일 업로드 기능, 공개 프로필란, 공개 메모란 등이 전형입니다.
CSP에 대해 덧붙이면, 실무에서 자주 쓰는 지정 중 이 공격을 확실히 멈출 수 있는 것은 connect-src(통신처 제한)가 아니라 script-src 입니다. script-src 'self' 를 지정하고 'unsafe-inline' 을 허용하지 않는 구성이라면, 리뷰에 삽입된 인라인 <script> 는 애초에 실행되지 않습니다. CSP를 「외부 통신을 줄이는 것」으로만 이해하면, 이 차이를 놓칩니다.
connect-src 쪽도 원리적으로 무력한 것은 아닙니다. connect-src 'none' 처럼 동일 오리진 통신까지 금하는 지정이나, 통신처를 특정 엔드포인트만으로 좁힌 허용 목록이라면 이 두 번의 XHR도 멈춥니다. 다만 그까지 좁힐 수 있는 사이트는 한정되므로, 현실적인 대비로는 script-src 가 핵심입니다. 「동일 오리진이므로 CSP 대상 밖」이 아니라, 「동일 오리진을 허용하는 일반적인 지정에서는 통과되어 버린다」는 이해가 정확합니다.
문제3(3):세션ID로 무엇을 할 수 있는가
문제3(3)은 취득한 정보로 무엇을 할 수 있는지를 40자 이내로 묻습니다. 답안 예는 「페이지 V에 접속한 회원으로 가장해, Web 앱 Q의 기능을 쓴다」입니다.
채점 강평에 따르면, 이 문제의 정답률은 높았다고 합니다. EC 사이트에서 cookie가 공격자에게 취득되는 영향은 잘 이해되어 있었다고 적혀 있습니다.
그리고 문제문의 사양을 다시 읽으면, 회원 프로필 기능에는 신용카드 정보를 등록하는 페이지가 있고, 로그인된 회원만 이용할 수 있다고 명기되어 있었습니다. 사칭의 앞에 무엇이 있는지를, 문제문은 처음부터 마련해 두고 있었던 셈입니다.
8.왜 공격자의 사이트에서는 성립하지 않는가 ── 문제4
문제4는 이 대문제 가운데서도 가장 본질적인 물음입니다.
가령, 공격자가 준비한 도메인의 사이트에 그림4와 같은 스크립트를 포함한 HTML을 준비하고, 그 사이트에 Web 앱 Q의 로그인된 회원이 접속하더라도, Web 브라우저의 구조에 의해 공격은 성공하지 않는다. 이 구조를 40자 이내로 답하라.
답안 예는 「스크립트에서 다른 도메인의 URL에 대해 cookie가 보내지지 않는 구조」입니다.
같은 스크립트를 evil.example 에 두고, Q사의 회원에게 밟히게 했다고 합시다. 무엇이 일어날까요.
먼저, 1회째 통신이 헛칩니다. evil.example 상의 스크립트에서 https://example.jp/user/profile 로 XMLHttpRequest를 보내도, 이는 크로스 오리진 요청입니다.
결정적인 것은 cookie가 붙지 않는다는 점입니다. 그림4의 스크립트는 withCredentials 를 지정하지 않으므로, 크로스 오리진 요청에 cookie는 첨부되지 않습니다. Q사 서버에서 보면 비로그인 접속이며, 토큰이 든 프로필 설정 페이지는 돌아오지 않습니다. 스크립트는 토큰을 얻지 못하고 거기서 멈춥니다.
또 하나, 응답을 읽지 못한다는 벽도 있습니다. Q사가 Access-Control-Allow-Origin 으로 evil.example 을 허용하지 않으면, 브라우저는 CORS 검사에 실패해 이 통신을 네트워크 오류로 다루고, onload 는 발생하지 않으며 xhr.response 에 도달하지 못합니다. 보통의 사이트는 낯선 오리진에 이 헤더를 반환하지 않으므로, 실제로는 여기서도 멈출 것입니다.
다만 Q사가 CORS를 어떻게 설정했는지는 문제문에 적혀 있지 않습니다. 가령 evil.example 에서의 접속을 허용하고 있었다 해도, 읽을 수 있는 것은 비로그인 상태의 응답이며 토큰은 들어 있지 않으므로, 역시 공격은 성립하지 않습니다. cookie가 붙지 않는다는 첫 번째 점만으로, 공격은 멈추어 있습니다.
어느 한쪽만으로도 스크립트는 멈추지만, 실제로는 양쪽이 효력을 냅니다.
게다가 cookie 자체도 읽지 못합니다. document.cookie 가 반환하는 것은, 그 스크립트가 동작하는 오리진(evil.example)의 cookie입니다. example.jp 에 대해 발급된 세션ID는 거기에 들어 있지 않습니다.
IPA의 답안 예는 전자, 즉 「다른 도메인의 URL에 대해 cookie가 보내지지 않는다」를 답으로 합니다.
여기서 이 둘을 같은 구조로 외우지 마십시오. 별개의 구조입니다.
document.cookie가example.jp의 cookie를 반환하지 않는 것은, cookie가 도메인마다 분리되어 관리되기 때문입니다.evil.example과example.jp처럼 다른 사이트(다른 등록 가능 도메인)끼리라면, 이 분리를 설정으로 느슨하게 할 수 없습니다. 다만 같은 도메인 아래의 서브도메인 사이는 별개이며, cookie의Domain속성을 쓰면sub.example.jp와example.jp사이에서 공유할 수 있습니다7- 크로스 오리진 XHR에 cookie가 붙지 않는 것은,
withCredentials가 기본값으로false이기 때문입니다. 이쪽은 조건이 갖추어지면 바뀝니다. 스크립트가withCredentials = true를 지정하고, cookie의SameSite속성이 크로스 사이트 전송을 허용하며, 서버가 송신 오리진을 명시한Access-Control-Allow-Origin과Access-Control-Allow-Credentials: true를 반환하면, cookie는 보내지고 응답도 읽을 수 있게 됩니다
여기서 실제로 실패한 이유와, 가령 바꿔 쓰면 무엇이 필요해지는가를 나누어 잡으십시오.
문제문과 그림4에서 확실히 말할 수 있는 것은 하나입니다. withCredentials 를 지정하지 않았으므로 cookie가 첨부되지 않고, 비로그인 취급이 되어 토큰을 얻지 못한다. 이것만으로 공격은 멈춥니다.
CORS 허용이 없는 것도 응답을 못 읽게 하는 벽이 되지만, Q사의 설정은 문제문에 없으므로, 이쪽은 「보통의 사이트라면 그렇게 되어 있다」는 조건부 보강으로 생각하십시오.
그렇다면 공격자가 withCredentials = true 로 바꿔 쓰면 어떻게 되는가. 여기서 처음으로 또 하나의 조건이 문제가 됩니다. cookie의 SameSite 가 크로스 사이트 전송을 허용하지 않으면, withCredentials 를 세워도 그 cookie는 역시 첨부되지 않습니다. 지금의 브라우저는 SameSite 미지정 cookie를 Lax 로 다루므로, 기본값에서는 첨부되지 않는 쪽으로 기울니다.
참고로 Q사 세션ID cookie에 SameSite 가 어떻게 설정되어 있었는지는 문제문에 적혀 있지 않습니다. 따라서 「SameSite 덕에 지켜지고 있었다」고는 말할 수 없습니다. 어디까지나, credentials를 붙여 공격해 오는 상대에 대해서는 SameSite 가 효력을 낼 수 있다는 이야기입니다.
정리하면, 다른 도메인에서 인증된 응답을 읽으려면 withCredentials = true·cookie의 SameSite 가 크로스 사이트 전송을 허용할 것·서버가 credentials 포함 CORS로 송신 오리진을 허용할 것, 이 세 가지가 갖추어져야 합니다. 도메인이 다르면 반드시 안전하다는 이야기가 아니지만, 조건이 하나 갖추어졌다고 바로 깨지는 이야기도 아닙니다. 자사 사이트를 점검할 때는 cookie의 도메인 분리에 안심하지 말고, CORS 설정(특히 credentials 포함 요청을 어느 오리진에 허용하는지)과 cookie의 SameSite 속성을 실제로 확인해야 합니다.
그래서 저장형 XSS의 가치가 높다
이 문제가 알려 주는 것은, 공격자가 「피해자 사이트 안에서 자신의 코드를 돌리는 것」에 집착하는 이유입니다.
겉모습이 똑같은 가짜 사이트를 자신의 도메인에 준비해도, 브라우저는 그것을 다른 사이트로 다룹니다. document.cookie 에서 상대 사이트의 cookie는 읽히지 않고, 이 문제의 스크립트처럼 자격 정보 없이 보내는 XHR에서는 상대 사이트에서 보면 로그인 상태조차 되지 않습니다. 공격자에게 가치는 코드가 피해자 사이트의 오리진에서 동작하는 것 그 자체에 있습니다. 저장형 XSS는 바로 그것을 실현하는 수단입니다.
다만 「다른 도메인에서는 인증 있는 요청을 전혀 보낼 수 없다」고 읽지 마십시오. cookie의 SameSite 가 크로스 사이트 전송을 허용하는 설정(SameSite=None 등)이라면, 악의 있는 페이지에서 cookie가 붙은 요청을 상대 서버에 전달하는 것 자체는 가능합니다. 폼의 POST 등이 그 전형이며, CORS가 제어하는 것은 주로 「스크립트가 응답을 읽을 수 있는가」이지, 요청이 서버에 도달하는지가 아닙니다(다만 커스텀 헤더를 붙이는 경우나 application/json 으로 보내는 경우 등은 프리플라이트가 선행하므로, 허용되지 않으면 본요청 자체는 전송되지 않습니다). 이것이 CSRF라는 공격이 성립하는 이유이며, 그래서 CSRF 대책 토큰이 필요해집니다.
즉, 다른 오리진의 스크립트가 기본값으로 못 하는 것은 「상대의 cookie를 읽는 것」과 「응답을 읽는 것」이지, 「요청을 보내는 것」이 아닙니다.
게다가 이 두 「못 읽음」은 강도가 다릅니다. document.cookie 의 도메인 분리는 서버 측 설정으로 느슨해지지 않지만, 응답을 읽을 수 있는지는 상대 서버의 CORS 설정 나름입니다. 서버가 송신 오리진을 허용하면(자격 정보 포함이라면 Access-Control-Allow-Origin 에 구체적인 오리진을 반환하고, 더불어 Access-Control-Allow-Credentials: true 를 반환하면), 다른 오리진의 스크립트라도 응답을 읽을 수 있습니다. 자사 사이트의 CORS 설정을 확인해야 하는 이유가 여기에 있습니다.
XSS가 특별한 것은, 이런 조건을 모두 뛰어넘어, 처음부터 피해자 사이트의 오리진으로 행동할 수 있다는 점에 있습니다.
바꿔 말하면, XSS는 「이상한 문자가 화면에 나오는 결함」이 아니라, 공격자를 자사 사이트의 정규 이용자와 같은 입장에 서게 하는 결함입니다. 이 인식의 차이가 우선순위 판단을 가릅니다.
9.효과가 있었던 대책과, 효과가 없었던 대책
여기까지를 한 장의 표로 정리합니다. 이 글에서 가장 가져가 주었으면 하는 부분입니다.
| Q사에 있던/없던 장치 | 이 공격에 대해 | 이유 |
|---|---|---|
| HTTPS 통신 | 효과가 없었다 | 통신로의 보호이며, 페이지에 삽입된 스크립트와는 무관 |
| 리뷰 타이틀 50자·상세 300자의 입력 문자 수 제한 | 효과가 없었다 | 15회로 나누어 게시되었고, 사이의 HTML은 JavaScript 주석으로 건너뛰어졌다 |
| 업로드용 토큰(CSRF 대책) | 효과가 없었다 | 동일 오리진의 스크립트는 정규 절차로 토큰을 취득할 수 있다 |
| 로그인 필수의 업로드 기능 | 효과가 없었다 | 실행하는 것은 로그인된 피해자 자신의 브라우저 |
| 출력 시 이스케이프(없었다) | 효과 있음(근본적 해결) | 게시된 문자열이 HTML로 해석되지 않게 되어, 애초에 스크립트가 동작하지 않는다 |
| cookie의 HttpOnly 속성(없었다) | 효과 있음(보험적 대책) | document.cookie 에서 세션ID를 읽지 못하게 된다 |
| 업로드 파일의 형식 확인·재인코딩(없었다) | 효과 있음(보험적 대책) | 생 텍스트는 PNG로 저장되지 않고, 메타데이터에 숨긴 문자열도 재인코딩으로 사라진다. 다만 이미지의 픽셀로 부호화되면 남는다 |
위의 넷은 이 공격에 대해서는 아무 역할도 하지 못했습니다. 아래 셋을 IPA 「안전한 웹사이트를 만드는 방법」의 구분에 맞추면, 첫 하나가 근본적 해결, 나머지 둘이 보험적 대책입니다5.
하나 덧붙이면, 위의 넷이 「IPA가 대책으로 인정하지 않는 것」이라는 뜻은 아닙니다. IPA의 구분은 근본적 해결과 보험적 대책 둘이며, 입력값의 내용 검사는 오히려 보험적 대책으로 들어 있습니다. 다만 거기에는 「이 대책이 유효해지는 것은 한정됩니다」라는 단서가 붙어 있고, 이번에는 바로 그 한정에 해당했다는 이야기입니다.
그리고 중요한 것은, 아래 셋은 어느 하나라도 있었다면, 이 문제에서 실행된 공격의 연쇄는 끊어져 있었다는 점입니다.
- 이스케이프했다면, 애초에 스크립트가 실행되지 않는다
- HttpOnly가 붙어 있었다면, 스크립트는 실행되지만 cookie를 읽지 못한다
- 형식을 확인했다면, cookie는 읽을 수 있지만 생 텍스트를 PNG로 업로드하지 못한다
다층 방어가 의미를 갖는 것은 바로 이런 장면입니다. 다만 보험적 대책은 근본적 해결을 대신하지 않습니다. HttpOnly를 붙여 있어도 XSS가 남아 있으면 공격자는 피해자의 브라우저에서 임의의 조작(상품 구매, 등록 정보 변경 등)을 실행할 수 있습니다. 세션ID를 훔칠 필요조차 없는 공격은 얼마든지 설계할 수 있습니다.
왜 「형식 확인」만이 아니라 「재인코딩」까지 적었는가
표의 마지막 행을 「형식 확인·재인코딩」으로 두 단으로 적은 데는 이유가 있습니다.
문제문의 Q사는 이미지 파일 형식을 전혀 확인하지 않았으므로, 형식 검사만 넣어도 이 공격은 멈춥니다. 그러나 그것은 공격자가 생 텍스트를 보낸 경우에 한정된 이야기입니다. 공격자는 정당한 PNG 파일을 조립하고, 그 메타데이터 영역 등에 문자열을 숨길 수도 있습니다. 이 경우 「형식이 올바른가」의 확인은 통과됩니다.
따라서 이 경로를 좁히려면, 업로드된 이미지를 서버 측에서 재인코딩해 배포하는(원래 바이트열을 그대로 저장하지 않는) 데까지 해야 합니다.
다만 재인코딩으로도 완전히 막을 수 있는 것은 아닙니다. 공격자가 세션ID를 이미지의 픽셀 그 자체로 그려 넣어 정당한 PNG를 만든 경우, 통상의 재인코딩은 그 겉모습 정보를 유지하므로, 공격자는 다운로드해 읽을 수 있습니다. 재인코딩이 막는 것은 생 바이트열을 보내는 변종과 메타데이터 영역에 숨기는 변종이지, 이미지 내용으로 부호화된 정보가 아닙니다.
즉 「쓸 수 있고, 또한 읽을 수 있는 장소」가 사이트 안에 있는 한, 그곳을 반출 경로로 완전히 닫을 수는 없습니다. 이는 보험적 대책의 성질을 잘 나타냅니다. 보험적 대책은 「지금 관측된 공격」을 멈추는 것만으로는 부족하고, 공격자가 수법을 조금 바꿨을 때에도 성립하는가까지 보고 설계해야 합니다. 그리고 아무리 겹쳐도, 근본적 해결인 출력 시 이스케이프를 대신하지 않습니다.
참고로, 업로드된 파일을 본체와 다른 도메인에서 배포하는 설계도 자주 권해지지만, 이는 이 반출 경로에 대한 대책이 아닙니다. 공격자는 다른 도메인에서라도 마찬가지로 이미지를 다운로드할 수 있고, 동일 오리진 정책은 공개된 바이트열을 비밀로 하는 장치가 아니기 때문입니다. 다른 도메인 배포가 효력을 내는 것은, 업로드된 파일 자체가 능동적 콘텐츠로서 피해자 사이트의 오리진에서 실행되어 버리는 위협(HTML이나 SVG를 업로드당하는 부류)에 대해서입니다. 다른 위협에 대한 대책으로, 갈라서 이해하십시오.
파일을 송신 측의 신고(확장자나 Content-Type)가 아니라 내용으로 판단한다는 사고방식 자체는, Web에 한정되지 않고 필요해지는 기본 동작입니다. 다만 「내용으로 판단한다」의 내용은 형식에 따라 다릅니다. PNG처럼 선두 바이트열에 정해진 나열이 있는 형식이라면 그것을 확인할 수 있습니다. 한편 CSV에는 표준화된 서명이 없고, 선두 BOM은 문자 인코딩을 나타낼 뿐 내용이 CSV라는 증거가 되지 않습니다. CSV 파일의 취급에서 다룬 것처럼, CSV의 경우에는 상정하는 방언으로 파싱이 통과되는지, 기대하는 스키마와 건수 상한에 들어가는지를 확인하게 됩니다.
10.자신의 코드에서 확인하는 관점
이 대문제를 코드 리뷰 체크리스트로 내리면 다음과 같습니다. 회원 기능이나 게시 기능을 가진 웹 애플리케이션이라면 그대로 쓸 수 있습니다.
- 출력 시 이스케이프가 모든 출력 지점에서 효력을 내는가. 템플릿 엔진의 자동 이스케이프를 명시적으로 끈 지점(
raw,| safe,dangerouslySetInnerHTML등)을 전부 찾아내고, 각각에 대해 꺼도 되는 이유를 설명할 수 있는지 확인한다 - 입력 측 제한을 XSS 대책으로 세지 않았는가. 문자 수 제한이나 입력 가능 문자 종류 제한은 사양상의 요건이지, XSS의 근본적 해결이 아니다
- 입력 시에 이스케이프하지 않았는가. 입력의 역할은 사양 밖의 값을 걸러내는 검증이지, 이스케이프가 아니다. 입력 시점에는 출력 대상(HTML·CSV·JSON·메일·로그)이 정해져 있지 않으므로, 입력 시 이스케이프는 이중 이스케이프와 데이터 파괴를 부른다. SQL문 조립도 같으며, 근본적 해결은 플레이스홀더이다
- cookie에
HttpOnly,Secure,SameSite가 붙어 있는가. 세션ID cookie는 특히 확인한다 - 업로드된 파일의 형식을, 확장자나 Content-Type이 아니라 내용으로 확인하고 있는가. 송신 측이 신고한 정보는 검증의 재료가 되지 않는다. 다만 형식 검사만으로는, 정당한 이미지의 메타데이터 영역에 데이터를 숨기는 수법은 통과된다
- 업로드된 이미지를 재인코딩해 배포하고 있는가. 원래 바이트열을 그대로 반환하지 않는가. 다만 재인코딩으로도 픽셀로 부호화된 정보는 남으므로, 「쓸 수 있고 읽을 수 있는 장소」는 완전히 막을 수 없다고 이해해 둔다. 참고로 다른 도메인에서의 배포는, 업로드된 파일이 자사 사이트 오리진에서 실행되는 위협에 대한 대책이지, 이 반출 경로에 대한 대책이 아니다
- CSP로 인라인 스크립트를 막을 수 있는 구성인가. 판단 기준은 「
'unsafe-inline'이 적혀 있는가」가 아니라 「인라인 스크립트가 실제로 허용되어 있는가」이다.script-src에 nonce나 hash 지정이 있으면, CSP Level 2 이후의 브라우저는'unsafe-inline'을 무시하므로,'unsafe-inline'과 nonce를 병기하는 하위 호환 정책에서는 nonce가 없는 주입 스크립트는 차단된다 - 토큰이 「무엇을」 지키는지 설명할 수 있는가. CSRF 대책 토큰은 XSS를 막지 않는다. 각각 별개의 대책이 필요하다
특히 1번은 실제 조사에서 나오는 전형 패턴입니다. 프레임워크가 자동 이스케이프해 주므로 안전하다고 생각했는데, 「HTML을 그대로 넣고 싶다」는 개별 요건 때문에 한곳만 자동 이스케이프를 껐다는 경우는 드물지 않습니다.
맺음말 ── 이 문제가 묻는 진짜 능력
여기까지 본 대책 목록은 맨 앞의 「먼저 결론」에 정리되어 있습니다. 마지막으로, 이 문제 자체에 대해 하나만 적어둡니다.
시험 문제로서 잘 되어 있는 점은, 「XSS를 아는가」가 아니라 「손에 있는 대책 가운데, 어느 것이 실제로 효력을 내는지를 가려낼 수 있는가」를 묻는다는 점입니다.
Q사는 아무것도 하지 않은 것이 아닙니다. HTTPS로 통신하고, 입력에 문자 수 제한을 두고, 업로드에는 토큰을 요구하고, 기능은 로그인된 회원으로 한정하고 있었습니다. 늘어놓으면 그럭저럭 대책이 된 구조로 보입니다. 그래도 전부 빠져나갔습니다. 반대로, 빠져 있던 셋은 모두 수수하고, 릴리스 노트에 쓸 만한 기능이 아닙니다.
보안 논의가 어려운 것은, 대책의 수와 방어의 강도가 비례하지 않기 때문입니다. 「무엇을 하고 있는가」가 아니라 「그 대책은 어느 공격의, 어느 단계를 멈추는가」를 하나씩 말할 수 있는지. 이는 자격 시험만을 위한 능력이 아니라, 보안 대책 설명을 받는 쪽에게도, 구현하는 쪽에게도, 가장 필요한 판단력입니다.
관련 상담 영역
합동회사 고무라소프트에서는, 회원 기능이나 게시 기능을 가진 웹사이트의 제작과, 기존 웹 애플리케이션에 같은 구멍이 나 있지 않은지의 설계 리뷰를 다룹니다.
참고 링크
-
IPA 독립행정법인 정보처리추진기구, 문제 책자·배점 비율·답안 예·채점 강평(2023년도, 레이와 5년도) 수록 「2023년도 가을 정보처리안전확보지원사 시험 오후 문제」. Q사의 Web 앱 Q 기능(회원 등록, 로그인과 cookie에 의한 세션ID 발급, 상품 리뷰 기능의 리뷰 타이틀 50자·리뷰 상세 300자의 입력 문자 수 제한, 회원 프로필 기능의 아이콘 이미지 업로드와 신용카드 정보 등록, 아이콘 이미지 업로드가 이미지 파일과 토큰을 파라미터로 할 것, 업로드한 아이콘 이미지가 회원 프로필 설정 페이지나 리뷰 페이지에 표시될 것), 페이지 V에서 16건의 리뷰가 2건만 표시되는 현상, 페이지 V의 HTML에 회원 A에 의한 15건의 게시와 긴 스크립트가 포함되어 있던 것, 추출된 스크립트의 내용, 그리고 Web 앱 Q에 회원이 입력한 스크립트가 실행되어 버리는 취약점이 있었던 것, cookie에 HttpOnly 속성을 부여하지 않았던 것, 업로드된 이미지 파일의 형식을 검사하지 않았던 것에 대해. 문제1부터 문제4의 문제문도 이 책자에 따른다. ↩ ↩2
-
IPA 독립행정법인 정보처리추진기구, 시험에 관한 자주 하는 질문. 당 기구에서 공표하는 과거의 시험 문제 사용에 관해, 법령에 특별한 정함이 있는 경우를 제외하고 허락이나 사용료는 필요 없다는 것, 다만 저작권은 포기하지 않았다는 것, 출처를 「년도, 시기, 시험 구분, 시간 구분, 문제 번호 등」의 형식으로 명기해야 한다는 것(예로 「출처:平成31年度 春期 기본정보기술자시험 오전 문제1」이 제시되어 있다), 및 문제의 일부를 개변한 경우에는 그 취지도 명기해야 한다는 것에 대해. ↩
-
IPA 독립행정법인 정보처리추진기구, 2023년도 가을 정보처리안전확보지원사 시험 답안 예. 문제1의 출제 취지(웹 애플리케이션 프로그램의 취약점이 악용된 것에 의한 인시던트 대응을 소재로, HTML이나 ECMAScript에서 악용된 취약점과 문제점을 읽어내고 대책을 입안하는 능력을 묻는다), 및 각 문제의 답안 예(문제1(1)은 「イ 저장형 XSS」, 문제1(2)는 「리뷰 타이틀을 출력하기 전에 이스케이프 처리를 실시한다.」, 문제2는 「HTML이 주석 처리되어 하나의 스크립트가 되도록 하는 게시를 여러 차례로 나누어 수행했다.」, 문제3(1)은 「XHR의 응답에서 취득한 토큰과 함께, 아이콘 이미지로 세션ID를 업로드한다.」, 문제3(2)는 「회원의 아이콘 이미지를 다운로드해, 거기서 세션ID 문자열을 꺼낸다.」, 문제3(3)은 「페이지 V에 접속한 회원으로 가장해, Web 앱 Q의 기능을 쓴다.」, 문제4는 「스크립트에서 다른 도메인의 URL에 대해 cookie가 보내지지 않는 구조」)에 대해. ↩ ↩2
-
IPA 독립행정법인 정보처리추진기구, 2023년도 가을 정보처리안전확보지원사 시험 채점 강평. 문제1 전체의 정답률이 평균적이었던 것, 문제1(1)에 대해 「스크립트에서 DOM을 사용하고 있었기 때문인지, “DOM Based XSS”라고 잘못 답한 수험자가 종종 보였다」는 것, 취약점은 특징이나 대책 방법까지 포함해 정확하게 이해해 주었으면 한다는 지적, 문제2에 대해 「”개발자 도구로 입력 제한을 삭제한 뒤 게시했다”와 같이, 확인이 부족하다고 여겨지는 답이 일부 보였다」는 것, 공격자가 남긴 흔적을 주의 깊게 확인해 공격 방법을 정확하게 파악하는 능력을 길러 주었으면 한다는 지적, 그리고 문제3(3)의 정답률이 높아, EC 사이트에서 cookie가 공격자에게 취득되는 영향이 잘 이해되어 있었다는 것에 대해. ↩ ↩2
-
IPA 독립행정법인 정보처리추진기구, 안전한 웹사이트를 만드는 방법. 웹사이트 취약점 11종류에 대해 위협과 대책을 「근본적 해결」(취약점의 원인 자체를 없애는 구현)과 「보험적 대책」(취약점이 남은 경우 공격의 성공률이나 피해를 낮추는 대책)으로 나누어 보인다는 것, 크로스사이트 스크립팅의 근본적 해결로서 웹페이지에 출력하는 모든 요소에 대해 이스케이프 처리를 실시하는 것이 들어 있다는 것, 및 부속의 보안 구현 체크리스트와 별책 「안전한 SQL 호출 방법」「웹 건강검진 사양」에 대해. ↩ ↩2 ↩3
-
IPA 독립행정법인 정보처리추진기구, 안전한 웹사이트를 만드는 방법 - 1.1 SQL 인젝션. SQL 인젝션의 근본적 해결로서 「SQL문의 조립은 모두 플레이스홀더로 구현한다」가 들어 있고, 정적 플레이스홀더(prepared statement) 쪽이 원리적으로 취약점이 생기기 어렵다고 되어 있다는 것, 문자열 연결로 SQL문을 조립하는 경우의 구현으로서 「SQL문의 조립을 문자열 연결로 하는 경우에는, 이스케이프 처리 등을 수행하는 데이터베이스 엔진의 API를 써서 SQL문의 리터럴을 올바르게 구성한다」고 적혀 있으며, 이스케이프가 SQL문을 구성하는 리터럴 생성에 대해 수행되는 것(입력을 받은 시점이 아니라는 것), 더불어 근본적 해결로서 「웹 애플리케이션에 전달되는 파라미터에 SQL문을 직접 지정하지 않는다」가, 보험적 대책으로서 「오류 메시지를 그대로 브라우저에 표시하지 않는다」「데이터베이스 계정에 적절한 권한을 부여한다」가 들어 있다는 것에 대해. ↩
-
IETF, RFC 6265: HTTP State Management Mechanism, Section 4.1.2.6 “The HttpOnly Attribute”. HttpOnly 속성이 cookie의 스코프를 HTTP 요청으로 한정한다는 것, 특히 cookie를 스크립트에 공개하는 웹 브라우저 API와 같은 「비 HTTP」 API를 통해 cookie에 대한 접근을 제공할 때 그 cookie를 생략하도록 사용자 에이전트에 지시한다는 것에 대해. 더불어 Secure 속성(Section 4.1.2.5)이 안전한 채널에서만 cookie를 보내도록 제한하는 것이라는 점에 대해. ↩ ↩2
관련 기사
같은 태그를 공유하는 최신 기사입니다. 더 가까운 주제로 지식을 넓힐 수 있습니다.
정보처리안전확보지원사 2024년 봄 오후 문제1 해설 ── JWT의 alg=none과 API 인가, WAF의 임시 대책
정보처리안전확보지원사 시험 2024년 봄 오후 문제1을 소재로 JWT의 alg=none, API 인가, Mass Assignment, 4자리 인증 코드 무차별 대입, WAF 임시 대책을 해설합니다.
홈페이지 발주 담당자도 알아 두면 좋은 점 ── IPA 「안전한 웹사이트 만드는 법」을 체크리스트로 활용하기
회사 홈페이지의 보안은 무엇을 기준으로 확인해야 할까요. IPA 「안전한 웹사이트 만드는 법」이 다루는 11가지 취약점과 대책을, 발주 담당자와 운영 담당자도 이해할 수 있는 말로 설명하고, 발주·검수·운영에서의 활용법을 소개합니다.
정보처리안전확보지원사 2023년 가을(레이와 5년) 오후 문제2 해설 ── 방문객용 Wi-Fi로 유출되는 파일
정보처리안전확보지원사 시험 2023년 가을(레이와 5년) 오후 문제2를 소재로, USB 메모리를 막은 회사가 방문객용 Wi-Fi를 통해 파일을 유출당하는 경로를 해설합니다. 서버 인증서 검증과 HSTS, MAC 주소 필터링의 한계, EAP-TLS...
정보보안 10대 위협 2026 ── 순위를 읽는 법과, 중소기업이 실제로 대비해야 할 것
IPA 「정보보안 10대 위협 2026」에서는 랜섬 공격이 11년 연속 1위, 공급망 공격이 2위, 처음 선정된 「AI 이용을 둘러싼 사이버 리스크」가 3위에 올랐습니다. 조직편 톱 10의 내용과, 중소기업이 어떤 위협을 자기 일로 받아들여 대비...
중소기업의 보안 대책, 무엇부터 시작할까 ── IPA 「중소기업의 정보보안 대책 가이드라인」 제4.0판 활용법
중소기업 보안 대책은 무엇부터 시작해야 할까요. IPA 「중소기업의 정보보안 대책 가이드라인」 제4.0판을 바탕으로 정보보안 6개 조항, 5분이면 끝나는 자사 진단, SECURITY ACTION까지 단계적으로 설명합니다.
관련 토픽
이 기사와 가까운 토픽 페이지입니다. 기사를 출발점 삼아 관련 서비스와 다른 기사로 이어집니다.
Windows 기술 토픽
Windows 개발, 장애 조사, 기존 자산 활용에 관한 KomuraSoft LLC 기사를 모은 토픽 허브입니다.
이 주제와 연결되는 서비스
이 기사는 다음 서비스 페이지로 이어집니다. 가까운 입구부터 확인해 주세요.
웹사이트 제작
회원 기능이나 게시 기능을 가진 웹사이트에서는, 출력 시 이스케이프나 cookie 속성 설정과 같은 이 글의 논점이 그대로 제작 품질로 직결되기 때문입니다.
기술 상담 & 설계 리뷰
기존 웹 애플리케이션의 어디에 같은 구멍이 나 있는지, 설계 리뷰의 관점에서 찾아내는 작업은 기술 상담의 범위이기 때문입니다.
자주 묻는 질문
이 기사 주제에 대해 상담 시 자주 나오는 질문을 모았습니다.
- 이 문제의 XSS는 왜 「저장형」인가요. 스크립트가 DOM을 조작하고 있는데, DOM Based XSS가 아닌가요.
- 취약점의 종류는 공격자의 문자열이 「어디서」 실행 가능한 코드로 바뀌는가, 즉 취약한 출력 지점(싱크)이 서버 측에 있는가 브라우저 측에 있는가로 결정됩니다. 이 문제에서는 공격자가 게시한 리뷰 타이틀을 서버가 그대로 HTML에 삽입해 반환하고 있었습니다. 싱크는 서버의 출력 처리에 있고, 그 문자열은 저장되어 이후 페이지를 연 전원에게 배포되므로 저장형 XSS입니다. DOM Based XSS는 브라우저상의 JavaScript가 innerHTML에의 대입 등을 통해 값을 실행 가능하게 만들어 버리는 유형을 가리킵니다. 삽입된 스크립트가 XMLHttpRequest나 getElementById 같은 DOM API를 쓰는지 여부는 취약점의 종류와는 관계가 없습니다. 참고로, 서버가 저장된 값을 무해한 텍스트나 JSON으로 반환하더라도, 그것을 브라우저 측 JavaScript가 innerHTML에 넘기면 DOM Based XSS가 됩니다. 응답에 문자열이 보이는지가 아니라, 어디서 실행 가능해졌는지로 판단하십시오. IPA의 채점 강평에서도, 스크립트에서 DOM을 사용했기 때문인지 DOM Based XSS라고 잘못 답한 수험자가 종종 보였다고 지적하고 있습니다.
- 리뷰 타이틀에 50자의 입력 문자 수 제한이 있었는데, 왜 긴 스크립트가 실행되었나요.
- 공격자는 1회 게시에 들어가는 길이로 나누어, 여러 차례에 걸쳐 게시했습니다. 1건째는 타이틀 도중에 script 태그를 열고, 타이틀 끝에서 JavaScript의 블록 주석(슬래시와 별표)을 열어 둡니다. 2건째의 첫머리에서 그 주석을 닫은 뒤 다음 명령을 쓰고, 끝에서 다시 주석을 여는 방식을 반복합니다. 이렇게 하면 게시와 게시 사이에 있는 HTML(div 태그 등)은 모두 JavaScript 주석 안에 들어가므로 무시되고, 분할된 조각만 이어져 하나의 스크립트로 실행됩니다. 입력 문자 수 제한은 1건당 길이만 제한할 뿐, 게시 횟수는 제한하지 않았다는 뜻입니다.
- 훔친 세션ID는 어디로 보내졌나요.
- 외부 서버로는 보내지지 않았습니다. 스크립트는 cookie의 내용을 그대로 파일의 내용으로 만들어, 이름을 「a.png」, MIME 타입을 「image/png」로 하여, 피해자가 이용하는 EC 사이트 자신의 회원 아이콘 이미지 업로드 기능으로 전송했습니다. 업로드된 아이콘 이미지는 리뷰 페이지 등에 표시되므로, 공격자는 그 이미지를 평범하게 다운로드해 내용의 문자열에서 세션ID를 꺼낼 수 있습니다. 피해자의 브라우저는 정규 사이트하고만 통신하며 외부로의 수상한 통신이 발생하지 않으므로, 프록시의 URL 필터링이나 방화벽의 외부 발신 통신 감시로는 알아챌 수 없는 경로가 됩니다.
- 업로드에는 토큰이 필요했을 텐데, 왜 공격이 성립했나요.
- 그 토큰은 다른 사이트로부터의 무단 요청을 걸러내기 위한 장치(CSRF 대책)이지, 같은 사이트에서 동작하는 스크립트를 상정한 대책이 아니기 때문입니다. 공격 스크립트는 먼저 프로필 설정 페이지에 XMLHttpRequest로 접속해, 정상 화면과 마찬가지로 토큰을 받은 뒤, 그 토큰을 첨부해 업로드를 실행합니다. XSS에 의해 공격자의 코드는 피해자와 같은 오리진에서 동작하므로, 정규 이용자가 할 수 있는 일은 무엇이든 할 수 있습니다. CSRF 대책 토큰은 XSS를 막지 못합니다.
- SQL 인젝션에서는 입력 이스케이프가 중요하다고 들었습니다. XSS만 출력 시인가요.
- 아닙니다. SQL 인젝션에서도 「입력 시 이스케이프」는 대책이 아닙니다. IPA가 근본적 해결로 드는 것은 「SQL문의 조립은 모두 플레이스홀더로 구현한다」는 것입니다. 문자열 연결로 SQL문을 조립하는 경우의 대안으로 이스케이프 처리가 제시되어 있지만, 그것도 「SQL문의 리터럴을 올바르게 구성하기」 위한 것, 즉 SQL문을 조립하는 순간에 하는 것이지 입력을 받은 시점이 아닙니다. 공통 원칙은 「이스케이프는 데이터가 어떤 문법의 세계로 나갈지가 정해지는 지점에서 수행한다」입니다. HTML로 나간다면 HTML 이스케이프, SQL문의 일부가 된다면 플레이스홀더, 셸 명령의 일부가 된다면 셸 방식, 이런 식입니다. 입력 시점에 이스케이프해서는 안 되는 이유는, 그 시점에는 출력 대상이 정해져 있지 않기 때문입니다. 같은 데이터가 HTML 페이지에도 CSV에도 JSON에도 알림 메일에도 로그에도 나가므로, 입력 시점에 HTML 이스케이프를 걸면 CSV에 실체 참조가 그대로 나오거나, 데이터베이스 내용이 원래 입력과 달라져 검색이나 집계가 어긋납니다. 참고로 입력 측에서 아무것도 하지 않는다는 뜻은 아닙니다. 입력 측의 역할은 이스케이프가 아니라 검증(밸리데이션)이며, 사양상 있을 수 없는 값을 걸러내는 것입니다.
- 이 문제에서 실무로 가져가야 할 대책은 무엇인가요.
- 근본적 해결은 리뷰 타이틀을 포함한 모든 이용자 입력에 대해, HTML로 출력하기 직전에 이스케이프 처리를 실시하는 것입니다. 그 위에 보험적 대책으로서, 세션ID의 cookie에 HttpOnly 속성을 붙여 스크립트에서 읽을 수 없게 하는 것, 업로드된 이미지를 서버 측에서 재인코딩해 원래 바이트열을 남기지 않는 것, Content Security Policy로 인라인 스크립트의 실행을 막을 수 있는 구성으로 해 두는 것을 들 수 있습니다. 이 문제의 Q사는 이 중 어느 하나라도 구현했다면 공격의 연쇄가 어딘가에서 끊어졌을 것입니다. 다만 보험적 대책은 공격자가 수법을 바꾸면 우회될 수 있으므로, 근본적 해결인 이스케이프를 대신하지는 못합니다.