리눅스·서버 학습

내 게시판에서 찾은 저장형 XSS와 수정 과정

에디터 HTML 정화가 꺼져 있던 문제를 직접 공격 문자열로 확인하고 고친 기록입니다.

게시판 에디터 라이브러리를 업데이트한 뒤 정상 동작을 확인하려고 일부러 위험한 내용을 글로 써 봤습니다. 업데이트 검증이 목적이었는데, 뜻밖에 원래부터 있던 취약점을 찾았습니다.

시험 방법

본문에 이벤트 속성이 있는 이미지, script 태그, javascript 주소 링크를 넣고 글을 저장한 뒤 읽기 화면의 DOM을 확인했습니다.

<img src=x onerror="alert(1)">
<script>alert(2)</script>
[링크](javascript:alert(3))

세 가지 모두 지워지지 않고 그대로 페이지에 들어갔습니다.

왜 위험한가

저장형 XSS는 글을 읽는 모든 사람의 브라우저에서 공격 코드가 실행됩니다. 로그인 토큰을 localStorage에 두는 구조라면 스크립트가 토큰을 읽어 외부로 보낼 수 있고, 관리자가 글을 열면 관리자 권한까지 넘어갈 수 있습니다.

원인

코드를 찾아보니 뷰어에 정화 기능을 사실상 끄는 옵션이 있었습니다. 유튜브 임베드가 지워지는 문제를 해결하려던 흔적으로 보입니다.

customHTMLSanitizer={(html) => html}

수정

정화를 끄는 대신 필요한 것만 허용했습니다. DOMPurify로 스크립트와 이벤트 속성을 지우고, iframe은 유튜브 임베드 주소일 때만 남깁니다.

const YOUTUBE = /^https:\/\/www\.youtube(-nocookie)?\.com\/embed\/[\w-]+/;

purify.addHook('uponSanitizeElement', (node, data) => {
  if (data.tagName === 'iframe' && !YOUTUBE.test(node.getAttribute('src') || '')) {
    node.remove();
  }
});

정규식 끝을 열어두면 주소 뒤에 다른 값이 붙을 수 있지만, 도메인과 경로 앞부분을 고정했기 때문에 다른 사이트의 삽입은 막힙니다.

수정 후 확인

  • 같은 공격 문자열이 모두 사라지는지 DOM으로 확인합니다.
  • 유튜브 임베드, 표, 코드 블록이 그대로 나오는지 확인합니다.
  • 이미 저장된 글에도 악성 코드가 없는지 검사합니다. 읽을 때 정화하므로 예전 글도 보호됩니다.

배운 점

기능이 안 될 때 안전장치를 끄는 해결은 나중에 가장 비싸게 돌아옵니다. 허용 목록을 좁게 만드는 쪽이 조금 번거로워도 맞습니다. 그리고 보안 기능은 켜져 있다고 믿지 말고 실제 공격 문자열로 시험해야 합니다.

JWT 해석기