XSS와 출력 이스케이프
세 가지 유형
| 유형 | 유입 경로 | 예시 |
|---|
| Stored | DB에 저장 | 댓글에 심은 스크립트가 모든 방문자에게 실행 |
| Reflected | 요청 파라미터 | 검색어를 결과 화면에 그대로 출력 |
| DOM 기반 | 브라우저 내부 | innerHTML에 URL 조각을 대입 |
막는 지점은 입력이 아니라 출력이다
| 값이 놓이는 자리 | 처리 방법 |
|---|
| HTML 본문 | 꺾쇠와 앰퍼샌드를 인코딩한다 |
| HTML 속성 | 인용부호로 감싸고 인코딩한다 |
| JS 문자열 | 유니코드 이스케이프를 쓴다 |
| URL 파라미터 | 퍼센트 인코딩을 쓴다 |
- 블랙리스트 필터링은 우회 변형을 따라가지 못한다. 컨텍스트별 인코딩이 원칙
- 자동 이스케이프를 끄는 API가 위험 지점: React
dangerouslySetInnerHTML, Vue v-html
넣는 자리마다 규칙이 다르다
같은 문자열이라도 어디에 넣느냐에 따라 위험한 문자가 다릅니다. 한 가지 방식으로 다
막지 못하는 이유입니다.
| 넣는 자리 | 위험한 것 |
|---|
| 본문 글자 | 꺾쇠와 앰퍼샌드 |
| 속성 값 | 따옴표. 안 감싸면 공백만으로도 벗어난다 |
| 스크립트 안 | 문자열을 끝내는 문자. 여기는 값을 아예 넣지 않는다 |
| 주소 자리 | 스킴. 스크립트를 실행하는 스킴이 들어갈 수 있다 |
셋째와 넷째가 자주 뚫립니다. 특히 사용자가 준 주소를 링크에 그대로 쓰는 경우입니다.
주소는 허용할 스킴을 정해 두고 그 외는 거절합니다.
원문을 그대로 저장하는 이유
들어올 때 변환해 저장하면 나중에 곤란해집니다.
저장된 것이 원문인지 변환된 것인지 알 수 없어진다
같은 값을 다른 자리에 쓸 때 규칙이 안 맞는다
두 번 변환돼 화면에 이상한 문자가 보인다
저장은 원문, 변환은 출력 자리에서 하는 것이 규칙입니다. 셋째는 사용자 눈에 보이는
결함이라 금방 발견되지만, 둘째는 조용히 구멍을 남깁니다.
실무 포인트
- HTML 입력을 허용해야 하면(에디터) 직접 필터링 대신 검증된 새니타이저를 쓴다
- CSP를 2차 방어선으로: nonce 기반
script-src, unsafe-inline 제거
- 세션 쿠키에
HttpOnly를 붙여 스크립트의 쿠키 접근을 차단한다
- 방어를 프런트에만 두지 않는다. API 응답에 그대로 저장된 값은 다른 클라이언트에서 다시 터진다
Q.게시판에 HTML 서식을 허용해야 한다면 어떻게 안전하게 구현하겠습니까?
전부 막을 수 없으니 허용 목록 방식의 검증된 라이브러리로 걸러냅니다. 직접 만들지 않습니다.
| 방법 | 판정 |
|---|
| 위험한 태그를 지우는 차단 목록 | 불가. 우회 방법이 계속 나온다 |
| 허용 태그와 속성만 남기는 검증 라이브러리 | 권장 |
| 마크다운만 허용하고 HTML 은 이스케이프 | 더 안전. 표현력은 줄어든다 |
허용 목록을 쓸 때 놓치기 쉬운 부분입니다.
| 확인할 것 | 내용 |
|---|
| 태그만 보면 안 된다 | onclick 같은 이벤트 속성도 제거해야 한다 |
| 링크의 스킴 | javascript: 와 data: 는 거부한다 |
| 스타일 속성 | CSS 로도 공격이 가능하다 |
| 저장할 때와 출력할 때 | 저장 시 정제하고 출력 시 자리에 맞게 처리한다 |
여기에 CSP 를 함께 걸어 인라인 스크립트 실행을 막으면 한 겹이 더 생깁니다.
흔한 실수: 정규식으로 script 태그만 지우는 것. 대소문자 섞기, 속성 안 실행, SVG 안 스크립트 등으로 뚫립니다.
Q.입력값 검증만으로 XSS를 막기 어려운 이유는 무엇인가요?
위험한지 아닌지는 값이 어디에 쓰이는지에 달려 있기 때문입니다. 입력 시점에는 그것을 모릅니다.
같은 값 javascript:alert(1) 이 어디에 놓이는지에 따라 달라집니다.
| 놓이는 자리 | 결과 |
|---|
| HTML 본문 | 그냥 글자다. 위험하지 않다 |
| 링크의 href | 클릭 시 실행된다. 위험하다 |
그래서 원칙은 원본을 저장하고 출력 시점에 그 자리에 맞게 처리하는 것입니다.
| 값이 놓이는 자리 | 처리 |
|---|
| HTML 본문 | 꺾쇠와 앰퍼샌드 인코딩 |
| HTML 속성 | 인용부호로 감싸고 인코딩 |
| URL 자리 | 허용 스킴만 통과 |
| 스크립트 안 | HTML 에 심지 않고 데이터로 넘긴다 |
입력 검증도 여전히 필요합니다. 길이와 형식을 제한해 공격 표면을 줄이는 역할입니다. 다만 그것만으로는 부족합니다.
흔한 실수: 입력 시점에 이스케이프해 저장하는 것. 그 값이 JSON 이나 로그로 나갈 때 이중 인코딩되어 화면이 깨집니다.
Q.CSP를 도입했는데도 XSS가 발생할 수 있는 경우는?
CSP 는 실행 경로를 제한하는 장치이고, 정책에 구멍이 있으면 통과됩니다.
| 구멍 | 왜 뚫리나 |
|---|
unsafe-inline 허용 | 인라인 스크립트가 실행된다. CSP 의 핵심 효과가 사라진다 |
unsafe-eval 허용 | 문자열을 코드로 실행할 수 있다 |
| 넓은 허용 출처 | 허용한 CDN 에 공격자가 올릴 수 있는 경로가 있으면 통과 |
| JSONP 엔드포인트 | 허용 출처가 임의 콜백을 반환해 준다 |
| 스크립트가 아닌 경로 | 데이터 유출은 이미지나 폼 전송으로도 가능하다 |
그리고 CSP 는 DOM 조작으로 생기는 XSS 를 다 막지 못합니다. 이미 로드된 우리 스크립트가 사용자 입력을 innerHTML 로 넣으면 정책 안에서 벌어집니다.
흔한 실수: CSP 를 도입했으니 이스케이프를 안 해도 된다고 보는 것. CSP 는 마지막 방어선이고, 출력 이스케이프가 1차 방어입니다. 순서를 바꿀 수 없습니다.
Q.저장형 XSS와 반사형 XSS의 대응 우선순위를 어떻게 정하겠습니까?
저장형을 먼저 막습니다. 피해 범위와 지속성이 다릅니다.
| 항목 | 저장형 | 반사형 |
|---|
| 어디에 있나 | DB 에 남는다 | 요청 파라미터에만 있다 |
| 피해 범위 | 그 페이지를 보는 모든 사용자 | 조작된 링크를 누른 사용자 |
| 공격 조건 | 한 번 심으면 계속 | 사용자를 유인해야 한다 |
| 정리 비용 | 이미 저장된 데이터를 찾아 지워야 한다 | 코드만 고치면 끝 |
저장형은 이미 심어진 것을 찾아 지우는 작업이 따라옵니다. 그래서 발견 즉시 유입을 막고, 기존 데이터를 훑어 정리하고, 그 사이 노출된 세션을 무효화할지 판단합니다.
대응 순서
1. 저장 경로를 막는다 (유입 차단)
2. 저장된 데이터를 조사하고 정리한다
3. 출력 이스케이프를 전 경로에 적용한다
4. 반사형 경로를 정리한다
흔한 실수: 코드만 고치고 끝내는 것. 저장형은 데이터에 남아 있어 코드 수정만으로 사라지지 않습니다.
먼저 스스로 답해보고 아래 답변과 견줘보세요. 막히는 부분은 문제로 확인할 수 있어요.
읽었으면 문제로 확인해보세요
보안 문제를 풀면 틀린 문제가 자동으로 노트에 쌓입니다. 가입 없이 5문제를 먼저 풀어볼 수도 있어요.