테마
02. 입력 검증과 컨텍스트 출력 인코딩
많은 웹 취약점은 "사용자 입력을 어떻게 받을 것인가"와 "받은 값을 어디에 어떻게 다시 보여줄 것인가"를 구분하지 못할 때 생긴다.
학습 목표
- 입력 검증, 정규화, 출력 인코딩, Sanitization의 차이를 설명할 수 있다.
- Allow-list 검증과 타입 강제의 중요성을 이해한다.
- HTML, 속성, JavaScript, URL 컨텍스트마다 인코딩 방식이 다르다는 점을 설명할 수 있다.
- 저장 시점 치환과 같은 오래된 대응 방식의 한계를 이해한다.
1. 먼저 용어를 분리해야 한다
| 개념 | 목적 | 예시 |
|---|---|---|
| 입력 검증 | 허용할 값인지 판정 | 숫자만, 이메일 형식, 길이 제한 |
| 정규화 | 비교 전에 형식을 통일 | 공백 제거, Unicode 정규화 |
| 출력 인코딩 | 브라우저가 코드를 실행하지 못하게 함 | HTML 엔티티 인코딩 |
| Sanitization | 제한된 HTML을 안전하게 정리 | 에디터 본문 허용 시 위험 태그 제거 |
이 네 가지는 비슷해 보이지만 역할이 다르다.
- 입력 검증은 들어올 수 있는 값의 범위를 제한한다
- 출력 인코딩은 브라우저 해석 방식을 제어한다
- Sanitization은 HTML을 일부 허용해야 할 때만 사용한다
2. 안전한 입력 처리 흐름
핵심은 두 가지다.
- 저장하기 전에 검증한다
- 화면에 보여주기 직전에 컨텍스트에 맞게 인코딩한다
이 두 단계가 합쳐져야 한다.
3. 입력 검증의 기본 원칙
3.1 Allow-list가 기본
허용하지 않을 값을 끝없이 나열하는 블랙리스트보다, 허용할 형식을 명확히 정하는 편이 안전하다.
예를 들어 다음과 같은 기준이 좋다.
- 게시글 ID: 정수만 허용
- 국가 코드: 미리 정한 코드 집합만 허용
- 정렬 방향:
asc,desc둘 중 하나만 허용 - 이미지 확장자:
jpg,png,webp같은 고정 목록만 허용
3.2 타입을 빨리 강제한다
문자열을 오래 끌고 가지 말고 가능한 빨리 의미 있는 타입으로 바꾼다.
- 숫자는 정수나 소수 타입으로 변환
- 날짜는 날짜 객체로 변환
- 열거형은 enum 또는 상수 집합으로 변환
이렇게 해야 뒤 단계에서 문자열 연결 실수를 줄일 수 있다.
3.3 길이와 범위를 같이 본다
형식만 맞는다고 안전한 것이 아니다.
- 사용자 이름: 최소/최대 길이
- 검색어: 최대 길이
- 업로드 파일명: 최대 길이
- 페이지 번호: 1 이상
길이 제한은 보안과 성능을 동시에 지켜 준다.
4. 컨텍스트별 출력 인코딩
같은 문자열이라도 어디에 넣느냐에 따라 필요한 방어가 달라진다.
| 출력 위치 | 필요한 대응 | 설명 |
|---|---|---|
| HTML 본문 | HTML 인코딩 | 태그 시작 문자 해석 방지 |
| HTML 속성 | 속성 컨텍스트 인코딩 | 따옴표 탈출 방지 |
| JavaScript 문자열 | JS 컨텍스트 인코딩 | 문자열 종료 방지 |
| URL 파라미터 | URL 인코딩 | 쿼리 분리 문자 처리 |
| CSS 값 | 가능하면 직접 삽입 금지 | CSS 컨텍스트는 실수가 많음 |
가장 흔한 실수는 화면에 출력하기 전에 한 번 치환했으니 끝이라고 생각하는 것이다.
출력 인코딩은 저장 시점이 아니라 출력 시점에, 현재 컨텍스트에 맞게 해야 한다.
5. HTML을 허용해야 하는 경우
리치 텍스트 에디터처럼 HTML을 일부 허용해야 하는 기능이 있다.
이때는 단순 치환이 아니라 Sanitizer가 필요하다.
권장 방향은 다음과 같다.
- HTML이 필요 없으면 태그를 허용하지 않는다
- HTML이 필요하면 허용 태그와 허용 속성을 매우 좁게 정한다
- 신뢰할 수 있는 Sanitizer를 사용한다
실무에서 많이 쓰는 선택지는 다음과 같다.
| 환경 | 대표 도구 |
|---|---|
| 브라우저 | DOMPurify |
| PHP | HTML Purifier |
| Java | OWASP Java HTML Sanitizer |
반대로 오래된 보안 강의에서 자주 보이는 "저장할 때 위험 문자를 한꺼번에 바꿔서 넣자"는 방식은 현재 기준으로 권장하기 어렵다.
6. 정규식은 만능이 아니다
정규식은 유용하지만, 그것만으로 보안을 끝내면 안 된다.
좋은 사용처
- 숫자 형식 확인
- 슬러그 형식 확인
- 국가 코드, 정렬 키 같은 작은 문자열 집합 확인
위험한 사용처
- 복잡한 HTML 정리
- 모든 이메일 형식을 완벽히 판정
- 파일 형식 전체 판정
정규식은 입력 검증의 일부일 뿐이고, 파일 형식이나 HTML 구조 같은 문제는 전용 파서나 전용 라이브러리가 더 적합하다.
7. 흔한 잘못된 대응
| 잘못된 대응 | 왜 문제인가 | 더 나은 방식 |
|---|---|---|
| 저장 시점에 전체 문자열 치환 | 출력 맥락이 바뀌면 이중 인코딩이나 누락 발생 | 출력 시점 인코딩 |
| 블랙리스트로 위험 문자만 막기 | 우회 문자가 계속 생김 | Allow-list와 타입 강제 |
| 프론트엔드 검증만 믿기 | 브라우저 우회가 가능함 | 서버 측 검증 필수 |
trim() 한 번으로 충분하다고 생각 | Unicode, 경로, URL 문제는 더 복잡함 | 정규화 + 타입 변환 + 정책 검증 |
8. 실무 예시
안전한 방향
- 정렬 키는
createdAt,name같은 허용 목록에서만 선택 - 페이지 번호는 정수로 파싱하고 범위를 제한
- 게시글 제목은 길이와 문자 집합을 검증
- 댓글 본문은 저장하되, 화면에 넣을 때 HTML 본문 컨텍스트로 인코딩
위험한 방향
- 사용자가 보낸 문자열을 그대로
innerHTML에 넣기 sortBy값을 그대로 SQLORDER BY나 ORM Raw query에 넣기- 파일명이나 URL을 문자열 덧붙이기로 조립하기
9. PR 리뷰 체크리스트
- 서버 측에서 입력 검증을 다시 하고 있는가
- 허용 값 집합을 코드나 enum으로 고정했는가
- 숫자, 날짜, 불리언을 문자열로 오래 끌고 가지 않는가
- HTML을 허용하는 기능에 Sanitizer가 있는가
- 출력 시점에 현재 컨텍스트에 맞는 인코딩을 적용하는가
innerHTML,dangerouslySetInnerHTML,v-html같은 위험 API를 정말 필요한 곳에서만 쓰는가
핵심 정리
- 입력 검증과 출력 인코딩은 다른 문제다
- 저장 전에 검증하고, 출력할 때 컨텍스트별 인코딩을 적용해야 한다
- Allow-list, 타입 강제, 길이 제한은 가장 기본적이면서 효과적인 방어다
- HTML을 일부 허용해야 한다면 치환이 아니라 Sanitizer를 사용해야 한다