사용자 입력 데이터의 HTML 태그 필터링 우회(XSS) 가능성

컴퓨터 화면에 깨진 메뉴와 불규칙한 팝업 창이 깜빡이며 오류를 보여주는 모습이다.

증상 확인: 웹사이트가 예상치 못한 동작을 하나요?

사용자가 입력한 데이터가 웹페이지에 그대로 출력될 때, 스크립트가 실행되거나 HTML 구조가 깨지는 현상을 목격했다면 XSS(Cross-Site Scripting) 취약점이 존재할 가능성이 높습니다. 공격자는 게시판 댓글, 프로필 이름, 검색어 입력창과 같은 곳에 악성 코드를 삽입하여 다른 사용자의 세션을 탈취하거나, 악의적인 사이트로 리다이렉트 시킬 수 있습니다, 이는 단순한 웹 오류가 아니라, 심각한 보안 위협입니다.

컴퓨터 화면에 깨진 메뉴와 불규칙한 팝업 창이 깜빡이며 오류를 보여주는 모습이다.

원인 분석: 신뢰할 수 없는 데이터의 무차별 출력

근본적인 원인은 개발자가 사용자로부터 입력받은 모든 데이터를 ‘악의적일 수 있다’는 전제 없이 처리하기 때문입니다. 웹 애플리케이션이 사용자 입력을 충분히 검증하거나 필터링하지 않고, 브라우저가 해석 가능한 HTML, JavaScript, CSS 코드로 웹페이지에 삽입할 때 발생합니다. 특히, innerHTML 속성을 사용하거나 PHP의 echo, Node.js의 템플릿 엔진에서 변수를 그대로 출력하는 경우가 대표적입니다.

주의사항: 본 가이드에서 제시하는 필터링 및 인코딩 방법은 일반적인 공격 벡터를 방어하기 위한 것입니다. 실제 운영 환경에 적용하기 전에는 반드시 개발 서버에서 충분히 테스트하고, 보안 전문가의 코드 리뷰를 받는 것이 필수적입니다, 잘못된 구현은 오히려 보안 허점을 만들거나 서비스 기능을 저해할 수 있습니다.

파란색과 녹색의 데이터 흐름이 깨진 핵심에서 쏟아져 나와 주변을 어지럽게 뒤덮고 있습니다.

해결 방법 1: 출력 시 필수 인코딩 적용 (가장 기본적이고 효과적인 방어)

사용자 데이터를 웹페이지의 본문(Content)으로 출력할 때는 반드시 HTML 엔티티 인코딩을 적용해야 합니다, 이는 <, >, &, ", '와 같은 특수 문자를 안전한 형태(&lt;, &gt; 등)로 변환하여 브라우저가 이를 코드가 아닌 ‘텍스트 데이터’로 해석하게 만듭니다.

  1. 서버 사이드에서 처리 (권장): 출력 직전에 인코딩 함수를 사용하세요.
    • PHP: htmlspecialchars($user_input, ENT_QUOTES, 'UTF-8'); 사용. ENT_QUOTES는 작은따옴표와 큰따옴표 모두를 인코딩합니다.
    • Node.js (EJS): <%= userInput %> (자동 이스케이프)를 사용. 비이스케이프 출력은 절대 <%- %>를 사용하지 마세요.
    • Python (Django): 템플릿에서 {{ user_input }} 구문은 자동으로 이스케이프됩니다.
    • Java (JSP): JSTL의 <c:out value="${userInput}"/> 사용.
  2. 클라이언트 사이드(JavaScript)에서 처리: DOM API를 사용해 텍스트 노드로 삽입하세요.
    • 절대 사용 금지: element.innerHTML = userInput;
    • 안전한 방법: element.textContent = userInput; 또는 document.createTextNode(userInput) 사용.
    • jQuery를 사용한다면, .text() 메서드는 안전그러나 .html() 메서드는 위험합니다.

해결 방법 2: 입력 검증과 허용 목록(Whitelist) 기반 필터링

인코딩만으로 해결되지 않는 경우가 있습니다. 예를 들어, 사용자에게 일부 HTML 태그(예: <b>, <i>, <a>) 사용을 허용하는 리치 텍스트 에디터를 제공해야 할 때입니다. 이때는 ‘허용 목록(Whitelist)’ 방식을 사용해야 합니다. ‘블랙리스트’ 방식(특정 태그만 금지)은 우회 방법이 무궁무진하기 때문에 신뢰할 수 없습니다.

  1. 신뢰할 수 있는 라이브러리 도입: 직접 정규식으로 모든 경우의 수를 필터링하는 것은 거의 불가능합니다.
    • PHP: HTMLPurifier 라이브러리를 사용. 허용할 태그와 속성을 정확히 정의할 수 있습니다.
    • JavaScript/Node.js: DOMPurify, sanitize-html 같은 라이브러리를 사용. DOMPurify.sanitize(dirtyHtml); 형태로 호출합니다.
  2. 구체적인 허용 규칙 설정: 라이브러리를 사용할 때도 최소 권한 원칙을 적용하세요.
    • <a> 태그는 허용하되, href 속성 값이 javascript:data: 프로토콜로 시작하지 않는지 검증.
    • <img> 태그의 src 속성을 허용 도메인으로만 제한.
    • 모든 태그에서 onclick, onmouseover 등 이벤트 핸들러 속성은 기본적으로 차단.

해결 방법 3: 보안 헤더를 통한 추가 방어층 구축

서버 응답에 보안 헤더를 추가하면, 클라이언트 브라우저 차원에서도 일부 XSS 공격을 차단할 수 있습니다. 이는 인코딩이나 필터링이 실패했을 때를 대비한 마지막 안전장치 역할을 합니다.

  1. Content Security Policy (CSP) 헤더 설정: 가장 강력한 현대적 방어 수단입니다. 웹사이트가 로드할 수 있는 스크립트, 스타일, 이미지 등의 출처를 명시적으로 지정합니다.

    예시 헤더: Content-Security-Policy: default-src 'self'; script-src 'self' https://trusted.cdn.com;

    • 이 정책은 'self'(동일 출처)와 명시된 CDN 외의 스크립트 실행을 모두 차단합니다. 인라인 스크립트(<script>alert(1)</script>)도 기본적으로 차단됩니다.
    • 도입 시 기존 기능이 작동하지 않을 수 있으므로, Content-Security-Policy-Report-Only 모드로 먼저 모니터링하며 시작하세요.
  2. X-XSS-Protection 헤더 (레거시 브라우저용): 최신 브라우저는 CSP를 우선시하지만, 구형 브라우저를 위해 설정할 수 있습니다. X-XSS-Protection: 1; mode=block로 설정하면 브라우저의 기본 XSS 필터가 작동하며 공격이 탐지되면 페이지 로딩을 중단합니다.
  3. HttpOnly 및 Secure 쿠키 설정: 이는 XSS로 인한 세션 탈취 피해를 최소화합니다. 세션 쿠키에 HttpOnly 플래그를 설정하면 JavaScript에서 해당 쿠키에 접근(document.cookie)이 불가능해집니다. Secure 플래그는 HTTPS 연결에서만 쿠키를 전송하도록 강제합니다.

주의사항 및 점검 리스트

다음은 코드를 검토하거나 보안 테스트를 수행할 때 확인해야 할 핵심 포인트입니다.

  • 문맥(Context) 구분: 사용자 데이터가 HTML 본문, HTML 속성, JavaScript 코드, CSS, URL 등 어디에 삽입되는지 정확히 파악하고, 각 문맥에 맞는 인코딩/검증 방식을 적용해야 합니다, 속성 값은 "'로 둘러싸고 html 인코딩해야 합니다.
  • dom 기반 xss 주의: 서버가 아닌 브라우저의 javascript 코드가 location.hash, document.referrer, window.name과 같은 소스를 통해 dom을 동적으로 조작할 때 발생하는 xss입니다. 클라이언트 사이드 코드도 신뢰할 수 없는 소스로부터의 데이터를 .innerHTML에 직접 넣지 않도록 검토 필요.
  • 정규식 필터의 함정: <script> 태그만 필터링하는 정규식은 <scr<script>ipt>나 대소문자 변형(<ScRiPt>), HTML 엔티티(&#x3C;script&#x3E;) 등으로 쉽게 우회됩니다. 정규식은 허용 목록 검증에만 제한적으로 사용.
  • URL 처리: 사용자 입력이 URL(리다이렉트, 링크)로 사용될 경우, javascript: 프로토콜이나 위장된 프로토콜을 허용하지 않도록 http://, https://로 시작하는지 검증하고, 가능하면 사전에 허용된 도메인 목록과 비교하세요.

전문가 팁: 단일 방어선에 의존하지 마세요. ‘방어적 프로그래밍’은 여러 겹의 보안층(Layered Defense)을 구성하는 것입니다. 이상적인 조합은 1) 입력에 대한 적절한 검증(형식, 길이), 2) 저장 또는 출력 시 문맥에 맞는 인코딩, 3) CSP 헤더를 통한 브라우저 수준 차단입니다. 또한, 정기적으로 OWASP ZAP, Burp Suite와 같은 동적 애플리케이션 보안 테스트(DAST) 도구를 이용해 자신의 서비스를 공격자 시각에서 점검하는 습관이 보안 사고를 미연에 방지하는 최선의 방법입니다. 모든 사용자 입력은 기본적으로 ‘악의적’이라고 가정하고 처리하는 마인드셋이 핵심입니다.