XSS

Cross-site Scripting

개념

남이 심어 둔 스크립트가 우리 페이지에서 우리 권한으로 실행되는 취약점입니다.

등장 배경

사용자가 쓴 글을 화면에 보여 주는 일은 단순해 보입니다. 받은 글자를 그 자리에 붙이면 됩니다. 여기에는 사용자가 보낸 것이 글자라는 전제가 깔려 있습니다.

페이지덱 댓글에서 그 전제가 깨진 일이 있습니다. 누군가 댓글 칸에 글 대신 스크립트 조각을 적어 넣었고, 화면에는 아무것도 보이지 않았습니다. 그런데 그 문서를 연 사람의 브라우저에서 그 조각이 실행됐고, 로그인 상태를 증명하는 값이 외부 주소로 넘어갔습니다.

서버는 멀쩡했습니다. 침입도 없었고 데이터베이스도 그대로였습니다. 피해는 그 문서를 연 다른 사용자들의 브라우저에서 일어났습니다.

원인은 브라우저가 받은 것이 읽을 글자인지 실행할 코드인지 스스로 구분하지 못한다는 데 있습니다. 화면을 만드는 쪽이 이것은 글자라고 표시해 주지 않으면 브라우저는 코드로 읽습니다. 그 표시를 빼먹은 자리가 곧 이 취약점입니다.

작동 방식

사용자가 넣은 값이 화면에 그대로 붙고, 브라우저가 그것을 코드로 해석할 때 일어납니다. 들어오는 경로에 따라 세 가지로 나뉩니다.

  1. 저장된 값에서 오는 것. 댓글이나 문서 제목처럼 서버에 저장된 값에 스크립트가 섞여 있고, 그 화면을 여는 모든 사람의 브라우저에서 실행됩니다. 한 번 심으면 계속 퍼지므로 피해가 가장 큽니다.
  2. 주소에서 오는 것. 주소에 넣은 값을 화면에 그대로 찍는 자리가 있으면, 그 값에 스크립트를 넣은 주소를 만들어 남에게 보냅니다. 링크를 눌러야 실행되므로 한 명씩 걸리지만, 우리 도메인의 주소라 받는 사람이 의심하지 않습니다.
  3. 브라우저 안에서 오는 것. 서버를 거치지 않고 화면 쪽 코드가 값을 받아 그대로 꽂는 경우입니다. 서버 기록에는 아무것도 남지 않아서 발견이 늦습니다.

세 경우 모두 위험의 크기가 같은 이유에서 나옵니다. 그 스크립트는 우리 페이지 안에서 돌기 때문에 우리 페이지의 권한을 그대로 씁니다. 로그인 상태를 증명하는 값을 읽어 보낼 수 있고, 화면에 가짜 로그인 칸을 그려 비밀번호를 받을 수 있고, 그 사용자 대신 요청을 보내 문서를 지울 수도 있습니다. 사용자는 우리 서비스를 쓰고 있다고 믿는 상태이므로 의심할 계기가 없습니다.

막는 방법은 네 겹입니다.

  1. 화면에 내보낼 때 글자로 표시합니다. 이것이 근본이고 나머지는 보조입니다. 요즘 화면 프레임워크는 값을 넣으면 기본으로 이 처리를 해 주므로, 위험한 자리는 서식 있는 문자열을 직접 꽂는 함수를 쓴 곳뿐입니다. 그 함수 이름을 검색해 나온 목록이 곧 점검할 자리입니다.
  2. 들어올 때 걸러 냅니다. 보조 수단으로 둡니다. 같은 값이 여러 화면에 쓰이기 때문에 입력에서만 막으면 놓치는 자리가 생깁니다.
  3. 실행할 수 있는 스크립트의 출처를 제한합니다. 우리 도메인에서 온 것만 실행하도록 브라우저에 알려 두면, 표시를 빼먹은 자리가 남아 있어도 외부 스크립트는 돌지 않습니다.
  4. 로그인 값을 스크립트가 못 읽게 둡니다. 브라우저에만 보관하고 코드에서는 읽을 수 없게 설정하면, 스크립트가 실행돼도 그 값을 훔쳐 갈 수 없습니다.

쓰이는 곳

점검할 자리는 사용자가 넣은 값이 화면에 보이는 모든 곳입니다. 댓글과 문서 제목이 먼저 떠오르지만 놓치기 쉬운 자리가 더 많습니다. 프로필 이름, 올린 파일의 이름, 팀 이름, 검색한 말을 결과 화면에 다시 보여 주는 자리가 모두 해당합니다. 페이지덱에서 실제로 걸린 곳은 파일 이름이었습니다.

두 번째는 합치기 전에 동료가 코드를 읽는 코드 리뷰입니다. 서식 있는 문자열을 직접 넣는 함수가 새로 들어왔는지가 확인 항목으로 들어갑니다. 이 함수를 아예 쓰지 못하게 막고 예외만 승인받는 규칙을 두는 팀도 있습니다.

세 번째는 보안 점검입니다. 웹에서 가장 많이 터지는 취약점을 모아 둔 OWASP Top 10에 오래 올라 있는 항목이고, 외부 전문가가 실제로 두드려 보는 모의 침투 테스트에서도 기본 점검 대상입니다.

네 번째는 글에 서식을 넣을 수 있는 편집기입니다. 굵은 글씨와 링크를 허용해야 하므로 전부 글자로 바꿔 버릴 수 없습니다. 이때는 허용할 태그와 속성을 목록으로 정해 두고 그 밖을 제거하는 방식을 씁니다. 금지 목록으로 만들면 빠뜨린 것이 통과합니다.

다섯 번째는 메일과 알림 문구입니다. 사용자 이름이나 문서 제목이 그 안에 들어가므로 같은 문제가 화면 밖에서도 생깁니다.

실무 용례

"여기 사용자 입력을 그대로 꽂고 있어요. XSS 납니다."

출력할 때 글자로 표시하는 처리가 빠졌다는 지적입니다.

"파일 이름도 이스케이프해야 합니다. 거기서 걸렸어요."

점검 자리를 댓글과 제목으로만 좁히면 안 된다는 말입니다.

"편집기는 허용 목록으로 갑시다. 금지 목록은 새는 구멍이 생겨요."

서식을 허용해야 하는 자리의 처리 방식을 정하는 대화입니다.

페이지덱에서는 태오가 이 항목을 점검표에 고정으로 넣어 두었습니다. 화면을 새로 만들 때 사용자 값이 들어가는 자리를 세고, 그중 서식을 허용하는 자리가 있으면 그 자리만 따로 표시해 둡니다. 표시된 자리는 기능이 바뀔 때마다 다시 봅니다.

흔한 오해

서버가 안전하면 괜찮다고 여기는 경우가 많습니다. 침입도 없고 데이터도 그대로이니 그렇게 읽힙니다. 하지만 이 취약점의 피해는 우리 서버가 아니라 다른 사용자의 브라우저에서 일어납니다. 서버 기록에는 댓글 하나가 저장된 흔적만 남고, 실제로 무엇이 넘어갔는지는 그 문서를 연 사람 쪽에서만 벌어집니다. 그래서 서버 쪽 지표만 보고 있으면 사고가 진행되는 동안 아무 신호도 받지 못합니다.

입력에서 막으면 끝난다고 보는 것도 흔합니다. 들어오는 문을 하나만 지키면 되니 간단해 보입니다. 그런데 같은 값이 여러 화면에 쓰입니다. 문서 제목은 목록에도 보이고 알림 메일에도 들어가고 주소창 제목에도 나가는데, 각 자리에서 안전한 형태가 서로 다릅니다. 입력에서 미리 바꿔 두면 어떤 자리에서는 깨져 보이고 다른 자리에서는 여전히 위험합니다. 내보내는 자리마다 그 자리에 맞게 처리하는 것이 근본인 이유입니다.

자주 헷갈리는 용어

용어무엇을 이용하는가XSS와의 차이
CSRF이미 로그인해 둔 상태사용자의 브라우저가 자동으로 붙여 보내는 로그인 정보를 빌려 씁니다. XSS는 우리 페이지 안에서 코드를 실행하므로 할 수 있는 일의 범위가 더 넓습니다

같이 보면 좋은 페이지

마지막 업데이트 2026-09-16