Content Security Policy (CSP)엄격하게 설정하는 방법

2025. 6. 4. 22:43프로그램/PHP 초급

Content Security Policy (CSP)를 엄격하게 설정하는 것은 웹 애플리케이션의 크로스 사이트 스크립팅(XSS) 및 기타 콘텐츠 주입 공격을 방어하는 데 매우 효과적인 방법입니다. 엄격한 CSP는 기본적으로 모든 리소스를 차단하고, 필요한 리소스만 명시적으로 허용하는 "화이트리스트(Whitelist)" 방식을 취합니다.

CSP의 기본 원리

CSP는 웹 페이지가 로드할 수 있는 리소스(스크립트, 스타일시트, 이미지, 폰트, 미디어 등)의 출처(Source)를 웹 서버가 브라우저에게 알려주는 메커니즘입니다. 브라우저는 이 정책에 따라 허용되지 않은 출처의 리소스 로드를 차단합니다.

CSP는 주로 HTTP 응답 헤더를 통해 설정하거나, HTML <meta> 태그를 통해 설정할 수 있습니다. HTTP 헤더를 통해 설정하는 것이 더 강력하고 권장됩니다.

엄격한 CSP 설정을 위한 핵심 원칙

  1. default-src 'none'; 또는 default-src 'self';로 시작:
    • 'none': 가장 엄격한 시작점입니다. 명시적으로 허용하지 않으면 아무것도 로드할 수 없습니다. (권장)
    • 'self': 같은 도메인에서 로드되는 리소스만 허용합니다. 이후 다른 모든 지시어(directives)를 개별적으로 명시해야 합니다.
  2. 'unsafe-inline' 및 'unsafe-eval' 금지:
    • script-src나 style-src에서 'unsafe-inline'(인라인 스크립트/스타일 허용)이나 'unsafe-eval'(eval() 함수 허용)을 사용하는 것은 XSS 취약점을 크게 증가시키므로, 엄격한 CSP에서는 절대 사용하지 않아야 합니다.
    • 인라인 스크립트/스타일이 필요하다면 nonce (넘버 원스) 또는 hash 방식을 사용해야 합니다.
  3. nonce (Number Once) 사용 (권장):
    • 인라인 스크립트(script-src)나 인라인 스타일(style-src)을 허용하는 가장 안전한 방법입니다.
    • 각 HTTP 응답마다 서버에서 **고유하고 예측 불가능한 무작위 문자열(nonce)**을 생성하여 CSP 헤더와 해당 <script> 또는 <style> 태그에 동시에 추가합니다.
    • 예: Content-Security-Policy: script-src 'nonce-randomstring'; <script nonce="randomstring">...</script>
  4. hash 사용:
    • 인라인 스크립트/스타일 콘텐츠의 SHA256, SHA384, 또는 SHA512 해시 값을 CSP에 포함시키는 방법입니다.
    • 예: Content-Security-Policy: script-src 'sha256-abcdef123456...';
    • 정적이고 변경되지 않는 인라인 스크립트에 적합하지만, 동적으로 생성되는 스크립트에는 관리가 어렵습니다.
  5. object-src 'none';:
    • 플래시나 자바 애플릿 같은 레거시 플러그인 콘텐츠를 완전히 차단합니다. 이는 보안 취약점의 일반적인 원인입니다.
  6. base-uri 'self'; 또는 base-uri 'none';:
    • <base> 태그 주입을 방지하여 상대 경로의 기준 URL을 조작하는 공격을 막습니다.
  7. frame-ancestors 'none';:
    • 클릭재킹(Clickjacking) 공격을 방어하는 데 매우 중요합니다. 다른 사이트가 당신의 페이지를 <iframe>, <frame>, <object>, <embed> 등으로 포함하는 것을 완전히 차단합니다. (참고: X-Frame-Options 헤더와 유사하지만, CSP가 더 강력하고 유연합니다.)
  8. form-action 'self';:
    • 폼(form) 데이터가 제출될 수 있는 URL을 제한합니다. 피싱(phishing) 공격을 방지하는 데 도움이 됩니다.
  9. report-uri 또는 report-to 사용:
    • CSP 위반이 발생했을 때 브라우저가 지정된 URL로 위반 보고서를 전송하도록 합니다. 이는 CSP를 테스트하고 배포하는 과정에서 매우 중요합니다.

엄격한 CSP 설정 예시 (HTTP 헤더)

PHP를 사용하여 HTTP 헤더를 설정하는 예시입니다.

PHP
 
<?php

// 1. 논스(Nonce) 생성
// 매 요청마다 예측 불가능한 고유한 문자열을 생성합니다.
// openssl_random_pseudo_bytes()는 안전한 의사 난수를 생성합니다.
$nonce = base64_encode(openssl_random_pseudo_bytes(16)); // 16바이트 난수 -> Base64 인코딩

// 2. CSP 정책 정의 (매우 엄격한 예시)
$csp_policy = "
    default-src 'none';
    script-src 'self' 'nonce-" . $nonce . "' https://cdn.example.com;
    style-src 'self' 'nonce-" . $nonce . "' https://fonts.googleapis.com;
    img-src 'self' data: https://cdn.example.com;
    font-src 'self' data: https://fonts.gstatic.com;
    connect-src 'self' https://api.example.com wss://realtime.example.com;
    media-src 'self';
    object-src 'none';
    frame-src 'self' https://www.youtube.com;
    frame-ancestors 'none';
    base-uri 'self';
    form-action 'self';
    // report-uri는 개발/테스트 단계에서 유용합니다.
    // production 환경에서는 report-to를 사용하는 것이 더 최신 방식입니다.
    report-uri /csp-report-endpoint;
";

// 불필요한 공백과 줄바꿈 제거
$csp_policy = preg_replace('/\s+/', ' ', trim($csp_policy));


// 3. HTTP 헤더 전송
// Content-Security-Policy-Report-Only: 정책을 위반해도 차단하지 않고 보고만 합니다. (테스트/디버깅용)
// Content-Security-Policy: 정책을 위반하면 차단하고 보고합니다. (운영용)
header("Content-Security-Policy: " . $csp_policy);
// header("Content-Security-Policy-Report-Only: " . $csp_policy); // 테스트 시 사용

// 4. HTML 콘텐츠 출력 (nonce 적용 예시)
?>
<!DOCTYPE html>
<html lang="ko">
<head>
    <meta charset="UTF-8">
    <title>엄격한 CSP 적용 예시</title>
    <style nonce="<?= $nonce ?>">
        body {
            font-family: Arial, sans-serif;
            background-color: #f0f0f0;
        }
    </style>
</head>
<body>
    <h1>엄격한 CSP 테스트 페이지</h1>

    <script nonce="<?= $nonce ?>">
        // 이 스크립트는 nonce가 일치하므로 실행됩니다.
        console.log('이 스크립트는 CSP에 의해 허용되었습니다.');
        alert('CSP 적용 성공!');

        // CSP에 의해 차단될 외부 스크립트 (example.com은 허용했지만, non-allowed-domain.com은 허용하지 않음)
        // var script = document.createElement('script');
        // script.src = 'https://non-allowed-domain.com/malicious.js';
        // document.body.appendChild(script);

        // CSP에 의해 차단될 인라인 이벤트 핸들러 (엄격한 CSP에서 인라인 이벤트는 허용되지 않음)
        // document.getElementById('myButton').onclick = function() { alert('Clicked!'); };
    </script>

    <button id="myButton">클릭</button>

    <script src="https://cdn.jsdelivr.net/npm/jquery@3.6.0/dist/jquery.min.js"></script>
    <script>
        // jQuery는 CDN에서 로드되므로, script-src에 해당 CDN이 포함되어야 합니다.
        $(document).ready(function() {
            $('#myButton').click(function() {
                console.log('버튼 클릭됨!');
            });
        });
    </script>
</body>
</html>

엄격한 CSP 설정 시 고려사항

  1. 점진적 적용 (Report-Only 모드):
    • CSP를 처음 적용할 때는 Content-Security-Policy-Report-Only 헤더를 사용하여 시작하는 것이 매우 중요합니다.
    • 이 모드에서는 정책을 위반해도 브라우저가 콘텐츠를 차단하지 않고, 단지 report-uri (또는 report-to)로 보고서만 전송합니다.
    • 이를 통해 애플리케이션의 모든 리소스 로드 경로를 파악하고, 실제 차단 없이 위반 사항을 모니터링하여 정책을 완벽하게 다듬을 수 있습니다.
  2. 모든 리소스 파악:
    • JS 라이브러리 (CDN 포함), CSS 파일, 이미지, 웹 폰트, AJAX 요청 엔드포인트, WebSocket 연결, iframe 콘텐츠, 폼 전송 대상 등 모든 리소스의 출처를 정확히 파악해야 합니다.
    • 특히 타사 위젯(소셜 미디어 버튼, 광고, 분석 스크립트)은 종종 복잡한 로드 방식을 사용하므로 주의 깊게 살펴봐야 합니다.
  3. 인라인 스크립트/스타일 제거 또는 Nonce/Hash 적용:
    • 가장 큰 도전 과제입니다. 모든 onclick, onmouseover 등의 인라인 이벤트 핸들러를 제거하고 JavaScript 이벤트 리스너로 변경해야 합니다.
    • <script> 태그 내의 모든 인라인 JavaScript 코드와 <style> 태그 내의 모든 인라인 CSS 코드에도 nonce 또는 hash를 적용해야 합니다. Nonce는 동적 콘텐츠에, Hash는 정적 콘텐츠에 유리합니다.
  4. eval() 및 setTimeout(string), new Function() 사용 금지:
    • 이러한 함수는 문자열을 코드로 실행하므로 XSS에 매우 취약합니다. 엄격한 CSP는 이들의 사용을 unsafe-eval 없이 차단합니다. 필요한 경우 다른 방식으로 코드를 작성해야 합니다.
  5. 개발자 도구 활용:
    • 브라우저의 개발자 도구(Console, Network 탭)는 CSP 위반을 감지하고 디버깅하는 데 필수적입니다. 위반 사항이 콘솔에 자세히 기록됩니다.

엄격한 CSP를 설정하는 것은 복잡할 수 있지만, 웹 애플리케이션의 보안을 크게 강화하는 데 필수적인 단계입니다. 점진적인 접근 방식과 철저한 테스트를 통해 성공적으로 구현할 수 있습니다.