블로그 게시물

mXSS: 코드 속에 숨어 있는 취약점

Yaniv Nizry photo

Yaniv Nizry

취약점 연구원

Date

  • Code Security

크로스 사이트 스크립팅(XSS)은 공격자가 취약한 페이지에 자바스크립트 코드를 삽입할 수 있을 때 발생하는 잘 알려진 취약점 유형입니다. 무심코 해당 페이지를 방문한 피해자의 세션에서 삽입된 코드가 실행됩니다. 이 공격의 영향은 애플리케이션에 따라 달라질 수 있으며, 계정 탈취(ATO), 데이터 유출, 심지어 원격 코드 실행(RCE)에 이르기까지 비즈니스에 영향을 미칠 수 있습니다.


XSS에는 반사형, 저장형, 범용형 등 다양한 유형이 존재합니다. 그러나 최근 몇 년간 DOMPurify, Mozilla bleach, Google Caja 등 다양한 정화기를 우회하는 변종 XSS가 등장하며 Google 검색을 비롯한 수많은 애플리케이션에 영향을 미쳐 위협이 되고 있습니다. 오늘날에도 이러한 유형의 공격에 취약한 애플리케이션이 다수 존재합니다.

그렇다면 mXSS란 무엇일까요?


(이 주제는 Insomnihack 2024 발표에서도 다뤘습니다: 샌티라이저를 이기는 법: mXSS를 도구함에 추가해야 하는 이유.)

배경

웹 개발자라면 애플리케이션을 XSS 공격으로부터 보호하기 위해 어떤 형태의 샌티라이저를 통합하거나 구현해본 경험이 있을 것입니다. 하지만 제대로 된 HTML 샌티라이저를 만드는 것이 얼마나 어려운지에 대해서는 잘 알려져 있지 않습니다. HTML 세니타이저의 목표는 텍스트 입력이나 외부 소스에서 얻은 데이터와 같은 사용자 생성 콘텐츠가 보안 위험을 초래하거나 웹사이트 또는 애플리케이션의 의도된 기능을 방해하지 않도록 보장하는 것입니다.

HTML 세니타이저 구현의 주요 과제 중 하나는 HTML 자체의 복잡한 특성에 있습니다. HTML은 웹페이지의 구조와 동작에 영향을 미칠 수 있는 다양한 요소, 속성 및 잠재적 조합을 가진 다목적 언어입니다. 의도된 기능을 유지하면서 HTML 코드를 정확하게 파싱하고 분석하는 것은 어려운 작업이 될 수 있습니다.

HTML

mXSS(변이형 크로스 사이트 스크립팅) 공격에 대해 알아보기 전에, 웹 페이지의 기초를 이루는 마크업 언어인 HTML을 먼저 살펴보겠습니다. mXSS 공격은 HTML의 특이점과 복잡성을 이용하기 때문에 HTML의 구조와 작동 방식을 이해하는 것이 중요합니다.

HTML은 오류나 예상치 못한 코드를 만났을 때 관대한 특성을 지녀 관용적인 언어로 간주됩니다. 일부 엄격한 프로그래밍 언어와 달리 HTML은 코드가 완벽하게 작성되지 않았더라도 콘텐츠 표시를 우선시합니다. 이러한 관용성은 다음과 같이 나타납니다:

잘못된 마크업이 렌더링될 때, 브라우저는 충돌하거나 오류 메시지를 표시하는 대신 사소한 구문 오류나 누락된 요소가 포함되어 있더라도 가능한 한 HTML을 해석하고 수정하려고 시도합니다. 예를 들어, 브라우저에서 다음 마크업을 열면 <p>test 닫는 p 태그가 누락되었음에도 예상대로 실행됩니다. 최종 페이지의 HTML 코드를 살펴보면 파서가 깨진 마크업을 수정하여 p 요소를 자체적으로 닫은 것을 확인할 수 있습니다: <p>test</p>.

관용적인 이유:

  • 접근성: 웹은 모든 사람이 접근할 수 있어야 하며, HTML의 사소한 오류가 사용자가 콘텐츠를 보는 것을 방해해서는 안 됩니다. 관용성은 더 다양한 사용자 및 개발자가 웹과 상호작용할 수 있도록 합니다.
  • 유연성: HTML은 코딩 경험 수준이 다양한 사람들이 자주 사용합니다. 관용성은 페이지 기능을 완전히 망가뜨리지 않으면서 어느 정도의 부주의나 실수를 허용합니다.
  • 하위 호환성: 웹은 지속적으로 진화하지만, 많은 기존 웹사이트는 오래된 HTML 표준으로 구축되었습니다. 관용성은 최신 사양을 따르지 않더라도 이러한 오래된 사이트가 현대 브라우저에서 여전히 표시될 수 있도록 보장합니다.

하지만 우리 HTML 파서는 어떻게 깨진 마크업을 “고치는” 방법을 알까요? <a><b><a></a><b></b>가 되어야 할까요, 아니면 <a><b></b></a>가 되어야 할까요?

이 질문에 답하기 위해 잘 문서화된 HTML 사양이 존재하지만, 안타깝게도 여전히 일부 모호성이 존재하여 오늘날 주요 브라우저 간에도 서로 다른 HTML 파싱 동작이 발생합니다.

변형

좋아요, HTML이 깨진 마크업을 허용할 수 있다는 건 알겠는데, 이게 무슨 관련이 있나요?

mXSS의 M은 “변형(mutation)”을 의미하며, HTML에서의 변형이란 어떤 이유로든 마크업에 가해진 모든 종류의 변경을 말합니다.

  • 파서가 깨진 마크업을 수정할 때(<p>test<p>test</p>), 이는 변형입니다.
  • 속성 따옴표를 정규화할 때(<a alt=test><a alt=”test”>), 이는 변형입니다.
  • 요소 순서를 재배열할 때(<table><a><a></a><table></table>), 이는 변형입니다.
  • 등등…

mXSS는 이 동작을 이용해 위생 처리를 우회합니다. 기술적 세부사항에서 예시를 보여드리겠습니다.

HTML 파싱 배경

1500페이지가 넘는 표준인 HTML 파싱을 한 섹션으로 요약하는 것은 현실적이지 않습니다. 그러나 mXSS와 페이로드 작동 방식을 깊이 이해하는 데 중요하므로 주요 주제 몇 가지는 다루어야 합니다. 이해를 돕기 위해, 연구자와 개발자를 위한 관리 가능한 자료로 방대한 표준을 압축한 mXSS 치트시트(본 블로그 후반부에 제공)를 개발했습니다.

다양한 콘텐츠 파싱 유형

HTML은 일률적인 파싱 환경이 아닙니다. 요소마다 콘텐츠를 다르게 처리하며, 7가지 뚜렷한 파싱 모드가 적용됩니다. mXSS 취약점에 미치는 영향을 이해하기 위해 이러한 모드들을 살펴보겠습니다:

다음 예시를 통해 파싱 유형 간의 차이를 상당히 쉽게 보여줄 수 있습니다:

  1. 첫 번째 입력은 div 요소로, “일반 요소”입니다:
  2. <div><a alt="</div><img src=x onerror=alert(1)>">
  3. 반면 두 번째 입력은 style 요소를 사용한 유사한 마크업입니다(이는 “원시 텍스트”입니다):
  4. <style><a alt="</style><img src=x onerror=alert(1)>">

파싱된 마크업을 보면 파싱 방식의 차이를 명확히 확인할 수 있습니다:

div 요소의 내용은 HTML로 렌더링되며, a 요소가 생성됩니다. 닫는 div 태그와 img 태그로 보이는 것은 실제로 a 요소의 속성 값이므로, HTML 마크업이 아닌 a 요소의 alt 텍스트로 렌더링됩니다. style 예시에서 style 요소의 내용은 원시 텍스트로 렌더링되므로 a 요소가 생성되지 않으며, 해당 속성은 이제 일반 HTML 마크업입니다.

외부 콘텐츠 요소

HTML5는 웹 페이지 내에 특수한 콘텐츠를 통합하는 새로운 방식을 도입했습니다. 두 가지 주요 예시는 <svg><math> 요소입니다. 이 요소들은 별개의 네임스페이스를 활용하므로 표준 HTML과 다른 파싱 규칙을 따릅니다. 이러한 상이한 파싱 규칙을 이해하는 것은 mXSS 공격과 관련된 잠재적 보안 위험을 완화하는 데 중요합니다.

이전과 동일한 예시를 살펴보되, 이번에는 svg 요소 안에 캡슐화해 보겠습니다:

<svg><style><a alt="</style><img src=x onerror=alert(1)>">

이 경우 a 요소가 생성되는 것을 확인할 수 있습니다. style 요소는 다른 네임스페이스 내에 위치하므로 “원시 텍스트” 파싱 규칙을 따르지 않습니다. SVG 또는 MathML 네임스페이스 내에 존재할 경우 파싱 규칙이 변경되어 더 이상 HTML 언어를 따르지 않습니다.

네임스페이스 혼동 기법(예: DOMPurify 2.0.0 우회)을 사용하는 공격자는 브라우저가 최종적으로 렌더링하는 방식과는 다른 방식으로 콘텐츠를 파싱하도록 샌티라이저를 조작하여 악성 요소의 탐지를 회피할 수 있습니다.

변형에서 취약점으로

종종 mXSS라는 용어는 다양한 샌티라이저 우회 방법을 포괄하는 광범위한 의미로 사용됩니다. 더 나은 이해를 위해 일반적인 용어 “mXSS”를 4가지 하위 범주로 구분하겠습니다.

파서 차이점

파서 차이점은 일반적인 샌티나이저 우회로 지칭될 수 있지만, 때로는 mXSS로 불리기도 합니다. 어느 쪽이든 공격자는 샌티나이저 알고리즘과 렌더러(예: 브라우저) 간의 파서 불일치를 악용할 수 있습니다. HTML 파싱의 복잡성으로 인해 파싱 차이가 존재한다고 해서 반드시 한 파서가 틀리고 다른 파서가 옳다는 의미는 아닙니다.

예를 들어 noscript 요소를 살펴보면, 해당 요소에 대한 파싱 규칙은 다음과 같습니다: “스크립팅 플래그가 활성화된 경우 토큰화기를 RAWTEXT 상태로 전환한다. 그렇지 않으면 토큰화기를 데이터 상태로 유지한다.” (링크) 즉, JavaScript가 비활성화되었는지 활성화되었는지에 따라 noscript 요소의 본문이 다르게 렌더링됩니다. JavaScript가 샌티라이저 단계에서는 활성화되지 않지만 렌더러에서는 활성화된다는 것은 논리적입니다. 이 동작은 정의상 잘못된 것은 아니지만 다음과 같은 우회를 유발할 수 있습니다: <noscript><style></noscript><img src=x onerror=”alert(1)”>

JS 비활성화 시:

자바스크립트 활성화됨:

HTML 버전 차이, 콘텐츠 유형 불일치 등 다른 많은 파서 차이점도 발생할 수 있습니다.

파싱 라운드트립

파싱 라운드트립은 잘 알려져 있고 문서화된 현상으로, 다음과 같이 설명합니다: “이 알고리즘의 출력을 HTML 파서로 파싱할 경우 원래의 트리 구조를 반환하지 않을 수 있습니다. 직렬화 및 재파싱 단계를 거친 후 원본과 일치하지 않는 트리 구조는 HTML 파서 자체에서도 생성될 수 있으나, 이러한 경우는 일반적으로 규격에 부합하지 않습니다.”

이는 HTML 마크업을 파싱하는 횟수에 따라 결과 DOM 트리가 변경될 수 있음을 의미합니다.


사양서에 제시된 공식 예시를 살펴보겠습니다:

하지만 먼저, form 요소 내부에는 다른 form 요소가 중첩될 수 없다는 점을 이해해야 합니다: “콘텐츠 모델: 흐름 콘텐츠이지만, 하위 요소에 form 요소가 포함될 수 없음.” (사양서에 명시된 내용)

그러나 문서에서 제공하는 예시를 계속 살펴보면, 다음과 같은 마크업으로 form 요소가 중첩될 수 있음을 보여줍니다:

<form id="outer"><div></form><form id="inner"><input>

html
├── head
└── body
    └── form id="outer"
        └── div
            └── form id="inner"
                └── input

</form>는 닫히지 않은 div 때문에 무시되며, input 요소는 내부 form 요소와 연관됩니다. 이제 이 트리 구조가 직렬화되고 재파싱되면, <form id="inner"> 시작 태그는 무시되어 input 요소가 대신 외부 form 요소와 연관됩니다.

<html><head></head><body><form id="outer"><div><form id="inner"><input></form></div></form></body></html>

html
├── head
└── body
    └── form id="outer"
        └── div
            └── input

공격자는 이 동작을 이용해 정화기와 렌더러 간 네임스페이스 혼란을 유발하여 다음과 같은 우회 공격을 수행할 수 있습니다:

<form><math><mtext></form><form><mglyph><style></math><img src onerror=alert(1)>

출처: @SecurityMB, 상세 내용은 여기 참조.

디샌티라이제이션

디샌티라이제이션은 애플리케이션이 클라이언트에 전송하기 전에 샌티라이저의 출력을 변경함으로써 발생하는 중대한 오류로, 본질적으로 샌티라이저의 작업을 무효화합니다. 마크업의 사소한 변경도 최종 DOM 트리에 큰 영향을 미쳐 샌티라이제이션 우회로 이어질 수 있습니다. 이 문제는 Insomni'Hack 발표와 여러 블로그 글에서 다룬 바 있으며, 다양한 애플리케이션의 취약점을 확인했습니다:

다음은 탈정화(desanitization)의 예시입니다. 애플리케이션이 정화기(sanitizer) 출력을 받아 svg 요소의 이름을 custom-svg로 변경하는 경우입니다. 이는 요소의 네임스페이스를 변경하여 재렌더링 시 XSS를 유발할 수 있습니다.

문맥에 따라 달라짐

HTML 파싱은 복잡하며 문맥에 따라 달라질 수 있습니다. 예를 들어, 전체 문서 파싱은 Firefox에서 프래그먼트 파싱과 다릅니다(치트시트의 브라우저별 섹션 참조). 브라우저에서 정화(sanitizing)에서 렌더링으로 전환되는 과정을 처리할 때, 개발자는 데이터가 렌더링되는 문맥을 실수로 변경하여 파싱 차이를 유발하고 결국 정화기를 우회할 수 있습니다. 타사 정화기는 결과가 배치될 컨텍스트를 인식하지 못하므로 이 문제를 해결할 수 없습니다. 이는 브라우저가 내장 정화기(Sanitizer API 노력)를 구현할 때 해결될 예정입니다.

예를 들어, 애플리케이션이 입력을 정화하지만 페이지에 삽입할 때 SVG로 캡슐화하여 컨텍스트를 SVG 네임스페이스로 변경하는 경우가 있습니다.

mXSS 사례 연구

과거에 Reply to calc: The Attack Chain to Compromise Mailspring과 같은 mXSS 취약점을 다룬 블로그 게시물을 공개한 바 있지만, mganss/HtmlSanitizer (CVE-2023-44390), Typo3 (CVE-2023-38500), OWASP/java-html-sanitizer 등도 보고했습니다.

이번에는 전자기기로 작성된 메모 앱인 Joplin (CVE-2023-33726)이라는 소프트웨어의 간단한 사례 연구를 살펴보겠습니다. 안전하지 않은 Electron 구성으로 인해 Joplin의 JS 코드는 Node 내부 기능을 사용할 수 있어 공격자가 시스템에서 임의 명령을 실행할 수 있습니다.

이 취약점은 htmlparser2 npm 패키지를 통해 신뢰할 수 없는 HTML 입력을 파싱하는 sanitizer의 파서에서 비롯됩니다. 해당 패키지 자체는 사양을 따르지 않으며 정확성보다 속도를 우선시한다고 주장합니다: “엄격한 HTML 사양 준수가 필요하다면 parse5를 살펴보세요.”

우리는 이 파서가 사양을 따르지 않는 방식을 매우 빠르게 발견했습니다. 다음 입력에서 파서가 서로 다른 네임스페이스를 인식하지 못함을 확인할 수 있습니다.

Sanitizer의 파서는 img 요소를 렌더링하지 않지만, 렌더러는 렌더링합니다. 이는 파서 차등(Parser Differential)의 예시입니다. 공격자는 단순히 onerror 이벤트 핸들러를 추가하면 피해자가 악성 노트를 열 때 임의의 코드가 실행됩니다.

이 특정 발견 사항은 @maple3142에 의해 독립적으로도 확인되었습니다.

완화 방안

안타깝게도 단일한 완화 방안은 존재하지 않습니다. 개발자 여러분께서는 이 버그 유형을 깊이 이해하여 각자의 애플리케이션에 맞춰 이 문제를 완화하는 최선의 결정을 내리시길 권장합니다.

연구 과정에서 mXSS 문제를 해결하기 위해 개발자들이 채택한 여러 완화 접근법과 보안 조치들을 확인했습니다(치트시트에서도 확인 가능):

클라이언트 측 정화

  • 이는 아마도 가장 중요한 준수 규칙일 것입니다. DOMPurify와 같은 클라이언트 측에서 실행되는 정화 도구를 사용하면 파서 차이 위험을 피할 수 있습니다. 파싱의 복잡성과 콘텐츠가 서로 다른 파서(Firefox vs Chrome vs Safari 등)에 제공될 가능성이 높기 때문에, HTML이 최종적으로 렌더링되는 위치와 다른 곳에서 파싱될 때 차이를 피하는 것은 불가능합니다. 이러한 이유로 서버 측 정화 도구는 실패하기 쉽습니다.
  • 클라이언트 측 JS 프레임워크와 함께 서버 측 렌더링(SSR)을 사용할 때, isomorphic-dompurify와 같은 라이브러리를 쉽게 도입할 수 있습니다. 이 라이브러리는 DOMPurify와 같은 클라이언트 측 정화 도구가 SSR 모드에서 “그냥 작동”하도록 합니다. 그러나 이를 달성하기 위해 jsdom과 같은 서버 측 HTML 파서를 도입하게 되며, 이는 파서 차이 위험을 초래합니다. SSR을 사용하는 웹 애플리케이션에 가장 안전한 옵션은 사용자가 제어하는 HTML에 대해 SSR을 비활성화하고, 정화 및 렌더링을 클라이언트 측으로만 연기하는 것입니다.

재파싱 금지

  • “왕복형 mXSS(Round trip mXSS)”를 방지하기 위해 애플리케이션은 콘텐츠를 직렬화하고 재렌더링하는 대신, 정화된 DOM 트리를 문서에 직접 삽입할 수 있습니다.
  • 참고: 이 접근법은 클라이언트 측에서만 구현된 정화 도구를 사용할 때만 가능하며, 예상치 못한 동작(예: 페이지 컨텍스트에 적응하지 못해 콘텐츠가 다르게 렌더링되는 경우)을 유발할 수 있습니다.

원시 콘텐츠는 항상 인코딩하거나 삭제

  • mXSS의 핵심은 악성 문자열이 검증 단계에서는 원시 텍스트로 처리되지만 이후 HTML로 파싱되도록 하는 것이므로, 검증 단계에서 원시 텍스트를 허용하지 않거나 인코딩하면 HTML로 재렌더링 자체가 불가능해집니다.
  • 참고: 이로 인해 CSS 코드 등이 깨질 수 있습니다.

외부 콘텐츠 요소 미지원

  • 샌티라이저에서 외부 콘텐츠 요소(SVG/수학 요소 및 그 내용을 삭제하고 이름을 변경하지 않음)를 지원하지 않으면 복잡성이 크게 줄어듭니다.
  • 참고: 이는 mXSS를 완화하지는 않지만 예방 조치를 제공합니다.

미래

간단한 해결책이 없는 이처럼 복잡한 주제에 밝은 미래가 있을까요?

답은 '예'입니다. 다행히도 이 버그 유형을 종식시키거나 최소한 공식적으로 해결하기 위한 여러 제안과 조치가 이루어지고 있습니다.

현재 가장 큰 문제는 신뢰할 수 없는 HTML 입력값을 정화하는 책임이 애플리케이션 개발자든 정화기 개발자든 제3자 개발자에게 있다는 점입니다. 작업의 복잡성과 다양한 렌더러 파서(사용자마다 다른 브라우저 사용)를 처리해야 하며 진화하는 HTML 사양을 따라잡아야 한다는 사실 때문에 이는 비현실적입니다. 이 문제를 해결하는 더 올바른 접근 방식은 마크업에 악성 콘텐츠가 없도록 하는 책임을 렌더러 측에 부여하는 것입니다. 예를 들어 브라우저에 내장된 정화기를 도입하면 지금까지 목격된 대부분의 우회 방법을 근절할 수 있을 것입니다.

Sanitizer API 이니셔티브가 바로 이를 위한 것입니다. 현재 웹 플랫폼 인큐베이터 커뮤니티 그룹(WICG)에서 개발 중이며, 브라우저 자체에서 작성한 통합적이고 견고하며 컨텍스트 인식 가능한 정화기를 개발자에게 제공하기 위한 것입니다(더 이상 파서 차이도, 재파싱도 필요 없음). Sanitizer API의 브라우저 채택이 확대되면 개발자들이 더 안전한 HTML 조작을 위해 이를 더 많이 사용할 가능성이 높습니다.

이 문제를 해결하기 위한 또 다른 노력은 사양 업데이트입니다. 예를 들어, Chrome은 이제 속성에서 <> 문자를 인코딩합니다.

<svg><style><a alt="</style>"><svg><style><a alt="&lt;/style&gt;">

HTML 정의의 기본을 더 안전한 미래로 진화시키고 있습니다.

mXSS 치트시트 🧬🔬

mXSS 세계에서 학습, 연구, 혁신에 관심 있는 모든 이를 위한 원스톱 정보원인 mXSS 치트시트를 제작했습니다. 1500페이지에 달하는 문서를 읽는 대신 간결한 목록으로 예상치 못한 HTML 동작을 파악할 수 있도록 돕습니다. 사용자들이 기여하여 함께 이 노력을 추진해 나가길 권장합니다.

요약

mXSS(변형 크로스 사이트 스크립팅)는 HTML 처리 방식에서 발생하는 보안 취약점입니다. 웹 애플리케이션이 기존 XSS 공격을 차단하기 위한 강력한 필터를 갖추고 있더라도 mXSS는 여전히 침투할 수 있습니다. 이는 mXSS가 HTML 동작의 특이점을 악용하여 악성 요소를 감지하지 못하게 하기 때문입니다.

본 블로그는 mXSS를 심층 분석하여 예시를 제공하고, 'mXSS'라는 광범위한 개념을 하위 항목으로 세분화하며, 개발자 대응 전략을 다루었습니다. 이 지식을 바탕으로 개발자와 연구자들이 향후 이 문제를 자신 있게 해결할 수 있기를 바랍니다.

새 블로그를 이메일로 바로 받아보세요!

최신 소나 콘텐츠를 놓치지 마세요. 지금 구독하시면 최신 블로그 글을 받아보실 수 있습니다.

I do not wish to receive promotional emails about upcoming SonarQube updates, new releases, news and events.

This site is protected by reCAPTCHA and the Google Privacy Policy and Terms of Service apply.

  • Follow SonarSource on Twitter
  • Follow SonarSource on Linkedin
language switcher
한국인 (Korean)
  • 법적 문서
  • 신뢰 센터

© 2008-2024 SonarSource SA. All rights reserved. SONAR, SONARSOURCE, SonarQube for IDE, SonarQube Server, SonarQube Cloud, and CLEAN AS YOU CODE are trademarks of SonarSource SA.