서버 측 CSP 정책 적용을 통한 XSS 방어
Lukas Schneider
DevOps Engineer · Leapcell

소개
진화하는 웹 보안 환경에서 교차 사이트 스크립팅(XSS)은 여전히 가장 흔하고 위험한 취약점 중 하나입니다. 공격자는 XSS 취약점을 악용하여 신뢰할 수 있는 웹사이트에 악성 스크립트를 삽입하며, 이는 결국 예상치 못한 사용자들에 의해 실행됩니다. 이로 인해 세션 하이재킹, 데이터 탈취, 웹사이트 변조, 심지어 전체 웹사이트의 파괴까지 초래될 수 있습니다. 클라이언트 측 보안 처리와 강력한 입력 유효성 검사는 중요하지만, 복잡한 XSS 시나리오를 완전히 완화하지는 못하는 경우가 많습니다. 바로 이때 Content Security Policy(CSP)가 강력하고 선언적인 보안 계층으로 등장합니다. CSP는 브라우저에게 어떤 리소스를 로드할 수 있는지 지시함으로써, 무단 스크립트 실행을 사전에 차단하는 강력한 방어 메커니즘 역할을 합니다. 이 글에서는 백엔드 프레임워크가 CSP 헤더를 효과적으로 설정하여 XSS 공격에 대한 웹 애플리케이션 보안을 대폭 강화하는 방법을 탐구할 것입니다.
핵심 개념 이해
구현에 들어가기 전에 CSP 이해에 필수적인 몇 가지 핵심 용어를 명확히 정리해 보겠습니다.
- 교차 사이트 스크립팅 (XSS, Cross-Site Scripting): 공격자가 다른 사용자가 보는 웹 페이지에 클라이언트 측 스크립트를 삽입할 수 있게 하는 보안 취약점 종류입니다.
- 콘텐츠 보안 정책 (CSP, Content Security Policy): 웹 애플리케이션 개발자가 사용자 에이전트가 특정 페이지에 대해 로드할 수 있는 리소스(스크립트, 스타일시트, 이미지 등)를 제어할 수 있게 해주는 W3C 표준입니다. XSS를 포함한 특정 유형의 공격을 완화하는 데 도움이 되는 추가 보안 계층입니다.
- HTTP 헤더 (HTTP Header): 메시지에 대한 메타데이터를 전달하는 HTTP 요청 또는 응답의 일부입니다. CSP는 일반적으로
Content-Security-PolicyHTTP 응답 헤더를 통해 전달됩니다. - 지시문 (Directives): CSP 내에서 다른 유형의 리소스에 대해 허용되는 소스를 지정하는 규칙입니다. 예로는
script-src(스크립트용),style-src(스타일시트용),img-src(이미지용),default-src(명시적으로 나열되지 않은 모든 리소스 유형에 대한 대체값) 등이 있습니다. - 소스 목록 (Source List): 특정 지시문에 대해 허용되는 출처 또는 키워드 목록입니다. 예로는
'self'(현재 출처),'unsafe-inline'(인라인 스크립트/스타일 허용, 일반적으로 권장되지 않음),'unsafe-eval'(eval() 및 유사 함수 허용, 이것 또한 일반적으로 권장되지 않음),https://example.com과 같은 특정 도메인 등이 있습니다. - Nonce: "한 번 사용되는 숫자"를 의미합니다. CSP에서는 암호화된 nonce를 사용하여 특정 인라인 스크립트 또는 스타일 블록을 명시적으로 허용할 수 있으며, 이는 HTTP 응답과 연결되어 공격자가 무단 코드를 삽입하는 것을 더 어렵게 만듭니다.
- 해시 (Hash): nonce와 유사하지만, 인라인 스크립트 또는 스타일 콘텐츠의 암호화 해시를 기반으로 합니다. 이를 통해 브라우저는 콘텐츠의 무결성을 확인할 수 있습니다.
백엔드에서 CSP 헤더 구현
CSP의 강점은 서버 측 적용에 있습니다. 백엔드는 각 관련 응답과 함께 Content-Security-Policy HTTP 헤더를 생성하고 보내는 책임을 집니다. 이를 통해 브라우저가 HTML 또는 스크립트 콘텐츠를 처리하기 전에 정책이 적용됩니다.
인기 있는 백엔드 프레임워크를 사용한 예제를 통해 설명해 보겠습니다.
Python (Flask)
Flask에서는 응답 헤더를 직접 설정할 수 있습니다. 일반적인 접근 방식은 데코레이터 또는 미들웨어를 사용하는 것입니다.
from flask import Flask, make_response, render_template app = Flask(__name__) @app.route('/') def index(): response = make_response(render_template('index.html')) # 기본 CSP 예제: 동일 출처에서만 스크립트와 스타일을 허용하고, # 명시적으로 nonce로 허용되지 않는 한 인라인 스크립트/스타일을 방지합니다. # 참고: 'unsafe-inline'은 개발 중에 임시로 사용되는 경우가 많지만 제거해야 합니다. nonce = generate_nonce() # 실제 애플리케이션에서는 암호학적으로 강력해야 합니다. csp_policy = ( f"default-src 'self';" f"script-src 'self' 'nonce-{nonce}';" f"style-src 'self' 'nonce-{nonce}';" f"img-src 'self' data:;" # 자체 및 데이터 URI에서 이미지 허용 "font-src 'self';" "connect-src 'self';" "frame-ancestors 'none';" # 사이트 프레이밍 방지 "form-action 'self';" # 양식이 제출될 수 있는 장소 제한 ) response.headers['Content-Security-Policy'] = csp_policy return response @app.route('/login') def login(): response = make_response(render_template('login.html')) # 페이지마다 다른 CSP가 필요할 수 있습니다. nonce = generate_nonce() csp_policy = ( f"default-src 'self';" f"script-src 'self' 'nonce-{nonce}' https://cdnjs.cloudflare.com;" f"style-src 'self' 'nonce-{nonce}';" f"img-src 'self';" "report-uri /csp-report-endpoint;" # 제약 위반 보고 예제 ) response.headers['Content-Security-Policy'] = csp_policy return response def generate_nonce(): import os import base64 return base64.b64encode(os.urandom(16)).decode('utf-8') if __name__ == '__main__': app.run(debug=True)
그리고 index.html (또는 nonce를 사용하는 모든 템플릿)에서:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>CSP 보호 페이지</title> <style nonce="{{ nonce }}"> body { font-family: sans-serif; } </style> </head> <body> <h1>환영합니다!</h1> <script nonce="{{ nonce }}"> // 이 스크립트는 올바른 nonce 덕분에 실행됩니다. console.log("인라인 스크립트가 nonce로 안전하게 실행되었습니다."); </script> <script src="/static/app.js"></script> <!-- 'self'에 의해 허용됨 --> <script> // 이 인라인 스크립트는 'unsafe-inline' 또는 일치하는 nonce가 없으면 차단됩니다. console.log("이 신뢰할 수 없는 인라인 스크립트는 CSP에 의해 차단되어야 합니다."); </script> </body> </html>
Node.js (Express)
Express 애플리케이션은 미들웨어를 사용하여 모든 또는 특정 라우트에 CSP 헤더를 설정할 수 있습니다. helmet 패키지는 CSP를 포함한 보안 헤더에 대해 강력히 권장됩니다.
const express = require('express'); const helmet = require('helmet'); const app = express(); const port = 3000; // 암호학적으로 강력한 nonce 생성 함수 function generateNonce() { return require('crypto').randomBytes(16).toString('base64'); } app.use((req, res, next) => { // 각 요청마다 새로운 nonce 생성 res.locals.nonce = generateNonce(); next(); }); app.use(helmet({ contentSecurityPolicy: { directives: { defaultSrc: ["'self'"], scriptSrc: ["'self'", (req, res) => `'nonce-${res.locals.nonce}'`], // nonce를 동적으로 사용 styleSrc: ["'self'", (req, res) => `'nonce-${res.locals.nonce}'`], imgSrc: ["'self'", "data:"] , fontSrc: ["'self'"], connectSrc: ["'self'"], frameAncestors: ["'none'"], formAction: ["'self'"], objectSrc: ["'none'"], // Flash와 같은 플러그인 비활성화 // reportUri: "/csp-report-endpoint", // 선택 사항: 제약 위반 보고용 }, }, })); app.get('/', (req, res) => { // nonce를 템플릿에 전달하여 HTML 템플릿 렌더링 res.send(` <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content= "width=device-width, initial-scale=1.0"> <title>CSP 보호 Node.js 페이지</title> <style nonce="${res.locals.nonce}"> body { background-color: #f0f0f0; } </style> </head> <body> <h1>CSP가 적용된 Node.js에서 인사드립니다!</h1> <script nonce="${res.locals.nonce}"> console.log("Node.js에서 인라인 스크립트가 nonce로 안전하게 실행되었습니다."); </script> <script src="/static/app.js"></script> </body> </html> `); }); app.listen(port, () => { console.log(`서버가 http://localhost:${port}에서 실행 중입니다.`); });
CSP 구현을 위한 주요 고려 사항
- 보고 전용 모드 (Report-Only Mode)로 시작:
Content-Security-Policy-Report-Only헤더를 사용하여 CSP를 "보고 전용" 모드로 배포하는 것으로 시작합니다. 이렇게 하면 제약 위반이 차단되지 않고 지정된 URL로 보고되어 정책을 미세 조정할 수 있습니다. - 세분화 (Granularity): 가능한 한 지시문을 구체적으로 작성합니다. 절대적으로 필요한 경우(그리고 극도의 주의를 기울여)가 아니라면
'*'또는'unsafe-inline'과 같은 광범위한 소스는 피하십시오. - Nonce 대 Hash: 인라인 스크립트 및 스타일에 대해 nonce는 스크립트/스타일 콘텐츠의 사소한 변경으로 인해 깨지기 쉽기 때문에 일반적으로 해시보다 선호됩니다. 그러나 해시는 콘텐츠 무결성을 보장합니다.
- 외부 리소스: CDN 또는 타사 위젯을 사용하는 경우 해당 도메인이 해당 지시문에 명시적으로 나열되도록 하십시오.
default-src: 이 지시문은 대체값으로 작동합니다. 리소스 유형이 다른 지시문(예:script-src)에 의해 명시적으로 다루어지지 않으면default-src의 규칙이 적용됩니다.- 사용자 생성 콘텐츠: 애플리케이션이 사용자 생성 콘텐츠를 처리하는 경우 CSP는 더욱 중요해집니다. 더 엄격한 정책 또는 특정 보안 처리 루틴이 필요할 수 있습니다.
- 반복적인 개선: CSP 구현은 종종 반복적인 프로세스입니다. 엄격한 정책으로 시작하고, 보고를 관찰하고, 필요한 경우에만 지시문을 완화하십시오.
결론
백엔드에서 Content Security Policy를 구현하는 것은 XSS 공격을 방어하는 기본적이면서도 매우 효과적인 전략입니다. 허용되는 콘텐츠 소스를 정확하게 지정하고 nonce를 동적으로 생성함으로써 백엔드 프레임워크는 애플리케이션이 강력한 최전선 방어를 구축할 수 있도록 지원합니다.
CSP가 만능 해결책은 아니지만, 다른 보안 모범 사례와 결합될 때 공격 표면을 크게 줄이고 악성 스크립트 삽입에 대해 웹 애플리케이션을 강화하여 더 안전한 사용자 경험을 제공합니다. 궁극적으로 잘 구성된 CSP는 사용자의 브라우저 내에서 신뢰할 수 있는 코드만 실행되도록 보장하는 강력한 게이트키퍼 역할을 합니다.