XSS 방어 (Preventing XSS)

XSS 방어 (Preventing XSS)

자바스크립트를 다룰 때 XSS(Cross-Site Scripting)는 피할 수 없는 보안 문제 중 하나다. 공격자가 악성 스크립트를 삽입해서 사용자의 브라우저에서 실행되게 만드는 이 공격을 막으려면 철저한 방어가 필요하다. 이번에는 XSS의 기본 개념부터 방어 방법까지 코드와 함께 하나씩 풀어보려고 한다.


XSS를 잘 이해하고 막아내면 웹 애플리케이션의 안전성이 한층 높아진다. 단계별로 차근차근 알아보자.


XSS란 무엇인가

XSS는 사용자가 입력한 데이터를 제대로 처리하지 않아 악성 코드가 웹 페이지에 삽입되는 공격이다. 예를 들어, 사용자가 입력란에 <script>alert('해킹!')</script>를 넣으면 그 코드가 그대로 실행될 수 있다.


간단한 예를 들어보자:

const userInput = "<script>alert('해킹!')</script>";
document.body.innerHTML = userInput;
// 경고창 "해킹!"이 뜬다

이렇게 innerHTML로 바로 넣으면 스크립트가 실행된다. XSS는 이런 식으로 시작된다.


1. 기본 방어: HTML 이스케이프

가장 기본적인 방법은 사용자가 입력한 데이터를 HTML 엔티티로 변환하는 것이다. 그러면 스크립트 태그가 실행되지 않고 텍스트로 표시된다:

function escapeHTML(str) {
    const htmlEntities = {
        '&': '&amp;',
        '<': '&lt;',
        '>': '&gt;',
        '"': '&quot;',
        "'": '&#39;'
    };
    return str.replace(/[&<>"']/g, match => htmlEntities[match]);
}

const userInput = "<script>alert('해킹!')</script>";
const safeInput = escapeHTML(userInput);
document.body.innerHTML = safeInput;
// "<script>alert('해킹!')</script>"로 표시됨

<>가 각각 &lt;&gt;로 바뀌면서 코드가 실행되지 않았다.


2. innerHTML 대신 textContent 사용

innerHTML은 HTML로 해석되지만, textContent는 순수 텍스트로 처리한다:

const userInput = "<script>alert('해킹!')</script>";
document.body.textContent = userInput;
// "<script>alert('해킹!')</script>"로 표시됨

textContent를 사용하면 태그가 해석되지 않고 그대로 텍스트로 출력된다.


3. DOMPurify로 강력한 방어

직접 이스케이프 함수를 만드는 것도 좋지만, DOMPurify 같은 라이브러리를 활용하면 더 안전하다:

// DOMPurify 라이브러리 추가 후
const userInput = "<script>alert('해킹!')</script><p>안녕</p>";
const clean = DOMPurify.sanitize(userInput);
document.body.innerHTML = clean;
// "<p>안녕</p>"만 남고 스크립트는 제거됨

DOMPurify는 위험한 태그를 제거하면서 허용된 HTML은 유지한다.


4. 이벤트 핸들러에서의 XSS

사용자 입력이 이벤트 핸들러에 들어갈 때도 주의가 필요하다:

const userInput = "alert('해킹!')";
document.body.innerHTML = `<button onclick="${userInput}">클릭</button>`;
// 버튼 클릭 시 "해킹!" 경고창 실행

이를 방어하려면 이벤트 핸들러를 직접 추가해야 한다:

const button = document.createElement('button');
button.textContent = '클릭';
button.addEventListener('click', () => {
    console.log('안전한 클릭');
});
document.body.appendChild(button);

이렇게 하면 사용자 입력이 이벤트 속성에 직접 들어가지 않는다.


5. JSON 데이터 처리

서버에서 받은 JSON 데이터도 XSS 위험이 있을 수 있다:

const unsafeJSON = {
    content: "<script>alert('해킹!')</script>"
};
document.body.innerHTML = unsafeJSON.content;
// 경고창 실행

이를 막으려면 JSON 데이터를 정화해야 한다:

const unsafeJSON = {
    content: "<script>alert('해킹!')</script>"
};
const safeContent = escapeHTML(unsafeJSON.content);
document.body.innerHTML = safeContent;
// 텍스트로 표시됨

JSON 데이터를 화면에 뿌리기 전에 항상 확인하자.


6. URL 파라미터와 XSS

URL 쿼리에서 받은 데이터도 위험할 수 있다:

const params = new URLSearchParams(window.location.search);
const input = params.get('data'); // ?data=<script>alert('해킹!')</script>
document.body.innerHTML = input;
// 경고창 실행

URL 데이터도 이스케이프 처리해야 한다:

const params = new URLSearchParams(window.location.search);
const input = params.get('data');
const safeInput = escapeHTML(input);
document.body.innerHTML = safeInput;

쿼리 파라미터도 안전하게 다루는 습관을 들이자.


7. CSP(Content Security Policy) 활용

CSP는 브라우저 수준에서 XSS를 막아준다. 헤더에 정책을 추가하면 된다:

<meta http-equiv="Content-Security-Policy" content="script-src 'self';">

이렇게 하면 외부 스크립트가 로드되지 않는다:

document.body.innerHTML = "<script src='https://evil.com/hack.js'></script>";
// CSP에 의해 차단됨

CSP는 추가적인 방어선 역할을 한다.


8. 심화: 속성 값에서의 XSS

HTML 속성에 삽입된 스크립트도 위험하다:

const userInput = "\" onclick=\"alert('해킹!')";
document.body.innerHTML = `<input value=${userInput}>`;
// 클릭 시 경고창 실행

이를 막으려면 속성을 동적으로 설정할 때도 안전하게 처리해야 한다:

const input = document.createElement('input');
input.setAttribute('value', escapeHTML("입력값"));
document.body.appendChild(input);

속성 값도 항상 확인하고 설정하자.


9. 프레임워크에서의 XSS 방어

React 같은 프레임워크는 기본적으로 XSS를 방어한다:

function Comment(props) {
    const userInput = "<script>alert('해킹!')</script>";
    return <div>{userInput}</div>;
}
// "<script>alert('해킹!')</script>"로 표시됨

하지만 dangerouslySetInnerHTML을 사용할 때는 주의해야 한다:

function DangerousComponent() {
    const userInput = "<script>alert('해킹!')</script>";
    return <div dangerouslySetInnerHTML={{__html: userInput}} />;
}
// 경고창 실행

프레임워크를 믿더라도 위험한 기능은 피하자.


10. 종합적인 방어 전략

XSS를 완벽히 막으려면 여러 방법을 조합해야 한다:

function renderSafeContent(userInput) {
    const escaped = escapeHTML(userInput);
    const cleaned = DOMPurify.sanitize(escaped);
    const div = document.createElement('div');
    div.textContent = cleaned;
    document.body.appendChild(div);
}

renderSafeContent("<script>alert('해킹!')</script><p>안녕</p>");
// 안전하게 텍스트로 표시됨

이스케이프, 정화, DOM 조작을 모두 활용하면 더 튼튼한 방어가 된다.


마무리

XSS는 자바스크립트 개발에서 늘 마주치는 위협이다. HTML 이스케이프부터 CSP, 프레임워크 활용까지 다양한 방법으로 방어할 수 있다. 사용자가 입력하는 모든 데이터를 의심하고, 여러 층의 보호를 적용하면 안전한 웹을 만들 수 있다.


+ Recent posts