CSP의 개념과 XSS 공격을 줄이는 원리, 주요 지시어의 의미, Spring Security에서 Content-Security-Policy 헤더를 안전하게 설정하는 방법을 알아봅니다.
웹페이지는 HTML만으로 구성되지 않는다. 화면을 움직이는 JavaScript, 디자인을 담당하는 CSS, 이미지와 글꼴 등 여러 자원을 함께 불러온다. 이 과정에서 브라우저가 모든 출처의 스크립트를 제한 없이 실행한다면 XSS 취약점이 발생했을 때 피해가 커질 수 있다.
CSP는 웹사이트가 사용할 수 있는 스크립트와 이미지 등의 출처를 서버가 미리 지정하는 보안 정책이다. 허용되지 않은 자원이 페이지에 삽입되면 브라우저가 해당 자원의 로드나 실행을 차단한다.
CSP란?
CSP는 ‘Content Security Policy’의 약자로, 우리말로는 콘텐츠 보안 정책이라고 한다. 웹 서버가 HTTP 응답 헤더를 통해 허용할 콘텐츠 출처와 실행 조건을 브라우저에 전달하는 방식이다.
예를 들어 다음과 같은 헤더를 적용할 수 있다.
Content-Security-Policy: default-src 'self'; script-src 'self'
'self'는 현재 웹페이지와 출처가 같은 자원만 허용한다는 의미다. 이 정책이 적용된 페이지에서는 기본적으로 같은 출처의 자원만 로드할 수 있고, JavaScript도 같은 출처에서 제공되는 파일만 허용된다.
CSP는 JavaScript뿐 아니라 CSS, 이미지, 글꼴, iframe, 네트워크 연결과 폼 전송 대상까지 제한할 수 있다. 브라우저는 서버가 전달한 정책과 페이지에서 요청한 자원을 비교한 뒤 허용 여부를 결정한다. MDN의 CSP 안내에 따르면 CSP는 페이지가 로드할 수 있는 자원을 통제해 XSS에 대한 심층 방어 수단으로 사용된다.
CSP의 핵심은 웹페이지에서 어떤 콘텐츠를 불러오고 실행할 수 있는지 서버가 브라우저에 명확하게 알려주는 것이다.
CSP가 XSS 공격을 줄이는 원리
XSS는 공격자가 입력한 스크립트가 웹페이지에 포함되고 방문자의 브라우저에서 실행될 때 발생한다. 게시글이나 검색어처럼 사용자가 입력한 값을 안전하게 출력하지 않으면 악성 <script> 태그나 이벤트 처리 코드가 페이지에 들어갈 수 있다.
CSP가 적용돼 있지 않다면 브라우저는 삽입된 스크립트를 정상적인 페이지 코드로 판단해 실행할 수 있다. 반면 안전한 CSP가 설정돼 있으면 허용되지 않은 외부 스크립트, 인라인 스크립트와 eval() 같은 위험한 실행 방식이 차단된다.
예를 들어 공격자가 다음과 같은 외부 스크립트를 삽입했다고 가정해 보자.
<script src="https://malicious.example/attack.js"></script>
script-src 'self'가 설정돼 있다면 브라우저는 현재 사이트와 출처가 다른 malicious.example의 JavaScript를 차단한다.
CSP는 인라인 <script>와 onclick 같은 이벤트 속성도 제한할 수 있다. 또한 'unsafe-eval'을 허용하지 않으면 문자열을 JavaScript 코드로 실행하는 eval()과 Function() 생성자 같은 기능도 차단할 수 있다. 이러한 동작은 공식 MDN CSP 설명과 OWASP CSP 지침에서도 주요 XSS 완화 방식으로 설명한다.
다만 CSP가 XSS 취약점 자체를 제거하는 것은 아니다. 사용자 입력에 대한 출력 인코딩이나 HTML 정화에 실패했더라도 악성 코드가 실행될 가능성을 낮추는 추가 방어선이다.
CSP에서 자주 사용하는 지시어
CSP는 여러 지시어를 세미콜론으로 구분해 작성한다. 각 지시어는 특정한 종류의 자원이나 브라우저 동작을 통제한다.
default-src
별도의 지시어가 설정되지 않은 자원에 적용되는 기본 정책이다.
default-src 'self'
같은 출처의 자원만 허용한다. 하지만 default-src 하나만 설정하고 끝내기보다 중요한 자원은 전용 지시어로 구체적으로 제한하는 것이 좋다.
script-src
JavaScript를 불러오거나 실행할 수 있는 출처를 정한다.
script-src 'self'
같은 출처에서 제공되는 JavaScript 파일만 허용하고, 별도로 승인하지 않은 인라인 스크립트는 차단한다.
style-src
CSS 파일과 스타일 적용 범위를 설정한다.
style-src 'self'
같은 출처의 CSS만 허용한다. 기존 페이지에 인라인 스타일이 많다는 이유로 'unsafe-inline'을 추가하면 정책이 약해질 수 있으므로 가능한 한 스타일 파일로 분리하는 것이 좋다.
img-src
이미지를 불러올 수 있는 출처를 지정한다.
img-src 'self' data:
같은 출처의 이미지와 data: 형식의 이미지를 허용한다. 외부 이미지 서버를 사용한다면 정확한 출처만 추가해야 한다.
connect-src
JavaScript의 fetch(), XMLHttpRequest, WebSocket 등으로 연결할 수 있는 주소를 제한한다.
connect-src 'self' https://api.example.com
같은 출처와 지정된 API 주소로만 연결할 수 있다.
object-src
object, embed 등 플러그인 콘텐츠의 출처를 제한한다. 특별히 필요한 경우가 아니라면 다음과 같이 모두 차단하는 것이 안전하다.
object-src 'none'
base-uri
HTML의 <base> 요소가 문서의 기준 URL을 변경하지 못하도록 제한한다.
base-uri 'self'
더 엄격하게 제한하려면 'none'을 사용할 수 있다.
frame-ancestors
현재 페이지를 iframe 등으로 삽입할 수 있는 상위 페이지를 지정한다.
frame-ancestors 'none'
어떤 사이트에서도 현재 페이지를 프레임으로 불러올 수 없게 해 클릭재킹 위험을 줄인다.
form-action
HTML 폼 데이터를 전송할 수 있는 목적지를 제한한다.
form-action 'self'
공격자가 페이지에 피싱 폼을 삽입하더라도 외부 사이트로 계정 정보를 전송하지 못하도록 막는 데 도움이 된다.
피해야 할 CSP 설정
CSP를 설정했다고 해서 항상 안전한 것은 아니다. 다음처럼 모든 출처와 인라인 스크립트 실행을 폭넓게 허용하면 CSP의 효과가 크게 떨어진다.
Content-Security-Policy:
default-src *;
script-src * 'unsafe-inline' 'unsafe-eval'
*는 광범위한 출처를 허용한다. 'unsafe-inline'은 인라인 스크립트 실행을 허용하고, 'unsafe-eval'은 문자열을 코드로 평가하는 기능을 허용한다.
특히 'unsafe-inline'을 추가하면 공격자가 삽입한 인라인 스크립트도 실행될 가능성이 커진다. MDN 역시 'unsafe-inline'과 'unsafe-eval'은 CSP의 목적을 약화하므로 피하도록 안내한다.
개발 과정에서 화면이 깨지거나 버튼이 작동하지 않는다는 이유로 두 값을 추가하는 경우가 있다. 당장은 오류가 사라지지만 XSS 방어 효과도 함께 낮아진다. 기존의 인라인 스크립트와 이벤트 속성을 외부 JavaScript 파일이나 addEventListener() 방식으로 정리하는 것이 바람직하다.
Spring Security에서 CSP 설정하기
Spring Security는 애플리케이션마다 필요한 자원 출처가 다르기 때문에 CSP를 기본으로 추가하지 않는다. 개발자가 서비스 구조에 맞는 정책을 직접 선언해야 한다. 이는 Spring Security 공식 문서에서도 확인할 수 있다.
다음은 같은 출처의 기본 자원만 허용하고 위험한 플러그인과 외부 프레임, 외부 폼 전송을 차단하는 예시다.
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
SecurityFilterChain securityFilterChain(
HttpSecurity http) throws Exception {
String policy =
"default-src 'self'; " +
"script-src 'self'; " +
"style-src 'self'; " +
"img-src 'self' data:; " +
"font-src 'self'; " +
"connect-src 'self'; " +
"object-src 'none'; " +
"base-uri 'self'; " +
"frame-ancestors 'none'; " +
"form-action 'self'";
http.headers(headers ->
headers.contentSecurityPolicy(csp ->
csp.policyDirectives(policy)
)
);
return http.build();
}
}
이 설정에서는 현재 사이트가 제공하는 JavaScript와 CSS만 사용할 수 있다. 인라인 스크립트와 eval() 실행은 별도로 허용하지 않았으므로 기본적으로 차단된다.
그러나 모든 사이트에 이 정책을 그대로 적용할 수 있는 것은 아니다. 외부 CDN, 지도, 결제 모듈이나 분석 도구를 사용한다면 필요한 출처를 지시어별로 추가해야 한다. 편의를 위해 외부 도메인을 넓게 허용하기보다 실제로 요청되는 주소만 확인해 등록하는 것이 좋다.
nonce를 이용한 엄격한 CSP
페이지에 꼭 필요한 인라인 스크립트가 있다면 'unsafe-inline' 대신 nonce를 사용할 수 있다. nonce는 한 번만 사용하는 임의의 값이다.
서버는 HTTP 응답마다 예측하기 어려운 새로운 nonce를 생성해 CSP 헤더에 포함한다.
Content-Security-Policy:
script-src 'nonce-RANDOM_VALUE';
object-src 'none';
base-uri 'none'
HTML의 허용할 스크립트에도 같은 값을 넣는다.
<script nonce="RANDOM_VALUE">
initializePage();
</script>
브라우저는 CSP 헤더의 nonce와 <script>의 nonce가 일치할 때만 코드를 실행한다. 공격자가 스크립트를 삽입하더라도 현재 응답에 사용된 nonce를 알지 못하면 실행되기 어렵다.
nonce는 페이지를 요청할 때마다 암호학적으로 안전한 난수로 새로 생성해야 한다. 설정 파일에 고정된 값을 적어 모든 응답에서 반복해서 사용하면 안 된다. 또한 렌더링이 끝난 HTML의 모든 <script> 태그에 nonce를 자동으로 붙이는 방식은 공격자가 삽입한 스크립트까지 허용할 수 있으므로 피해야 한다.
정적 페이지처럼 응답마다 nonce를 넣기 어려운 경우에는 허용할 스크립트 내용의 해시를 CSP에 등록하는 방법도 있다. 현재 권장되는 엄격한 CSP는 nonce 또는 해시를 중심으로 구성된다. MDN의 Strict CSP 안내에서도 이러한 방식을 권장한다.
Report-Only로 먼저 점검하기
운영 중인 사이트에 엄격한 CSP를 바로 적용하면 기존 스크립트와 이미지가 차단돼 일부 기능이 작동하지 않을 수 있다. 이때 Content-Security-Policy-Report-Only 헤더를 사용하면 실제로 차단하지 않고 위반 내역만 확인할 수 있다.
Spring Security에서는 다음과 같이 설정할 수 있다.
http.headers(headers ->
headers.contentSecurityPolicy(csp ->
csp.policyDirectives(
"default-src 'self'; " +
"script-src 'self'; " +
"object-src 'none'; " +
"base-uri 'self'"
).reportOnly()
)
);
Report-Only 상태에서 브라우저 개발자 도구와 수집된 위반 보고서를 확인하면 어떤 자원이 정책에 걸리는지 파악할 수 있다. 정상 기능에 필요한 출처만 선별해 정책에 추가한 후 실제 Content-Security-Policy 헤더로 전환하면 된다.
다만 Report-Only는 위반 자원을 차단하지 않는다. 점검이 끝난 정책은 반드시 실제 적용 모드로 전환해야 한다.
CSP만으로 XSS를 완전히 막을 수 있을까?
CSP는 강력한 보안 기능이지만 XSS 대응의 전부는 아니다. 정책에 잘못된 출처가 등록돼 있거나 'unsafe-inline'이 허용돼 있으면 공격 코드가 실행될 수 있다. 같은 출처에 공격자가 원하는 JavaScript 파일을 업로드할 수 있는 기능이 있다면 'self' 정책도 충분하지 않을 수 있다.
사용자 입력값은 출력되는 위치에 맞게 HTML, 속성, JavaScript와 URL 문맥별로 인코딩해야 한다. 사용자가 HTML을 작성해야 하는 기능이라면 검증된 정화 라이브러리로 위험한 태그와 속성을 제거해야 한다.
DOM 기반 XSS를 막기 위해 innerHTML, document.write()와 문자열 기반 eval() 사용을 줄이고, textContent처럼 입력값을 문자로 처리하는 안전한 API를 사용하는 것도 중요하다.
즉, CSP는 취약한 코드를 그대로 두고 헤더 하나로 해결하는 방법이 아니다. 안전한 코딩과 입력값 처리에 브라우저 차단 정책을 더하는 심층 방어 수단이다. OWASP도 CSP를 XSS의 유일한 방어책이 아닌 추가 보호 계층으로 사용할 것을 권고한다.
CSP 점검 항목
CSP를 점검할 때는 먼저 모든 HTML 응답에 Content-Security-Policy 헤더가 포함되는지 확인한다. 첫 화면에만 적용되고 오류 페이지나 로그인 페이지에는 누락되지 않았는지도 살펴봐야 한다.
default-src, script-src, object-src, base-uri, frame-ancestors와 form-action이 서비스 구조에 맞게 지정돼 있는지 확인한다. 스크립트 정책에 *, 'unsafe-inline' 또는 'unsafe-eval'이 불필요하게 포함돼 있지 않은지도 점검해야 한다.
외부 도메인을 허용했다면 해당 도메인이 정말 필요한지, 사용자가 파일을 올리거나 임의의 콘텐츠를 생성할 수 있는 출처는 아닌지 확인한다. nonce를 사용한다면 요청마다 새로운 값이 생성되고 CSP 헤더와 실제 <script> 태그에 동일하게 적용되는지 살펴본다.
마지막으로 브라우저 개발자 도구의 CSP 오류와 위반 보고서를 확인한다. 정책을 완화해 오류만 없애기보다 인라인 코드와 불필요한 외부 자원을 정리하는 방향으로 개선하는 것이 좋다.
자주 묻는 질문
CSP를 설정하면 XSS 취약점이 사라지나요?
아니다. CSP는 공격 스크립트의 실행 가능성을 낮추는 추가 방어 수단이다. 출력 인코딩, HTML 정화와 안전한 DOM API 사용이 함께 적용돼야 한다.
'self'는 무엇을 의미하나요?
현재 문서와 프로토콜, 호스트 및 포트가 같은 출처를 의미한다. 단순히 도메인 이름이 비슷한 사이트를 허용한다는 뜻은 아니다.
'unsafe-inline'을 사용해도 괜찮나요?
가능하면 사용하지 않는 것이 좋다. 인라인 스크립트를 허용하면 CSP의 XSS 방어 효과가 크게 낮아질 수 있다. 필요한 인라인 스크립트에는 nonce나 해시를 적용하는 것이 안전하다.
CSP는 HTML의 meta 태그로 설정해도 되나요?
일부 CSP 기능은 meta 태그로도 적용할 수 있지만 지원되지 않는 지시어와 기능이 있다. 가능한 경우 HTTP 응답 헤더로 설정하는 것이 좋다. OWASP는 응답 헤더 방식이 전체 CSP 기능을 지원하는 우선적인 전달 방법이라고 설명한다.
Report-Only 정책도 공격을 차단하나요?
차단하지 않는다. 정책 위반을 기록하고 보고하지만 자원 로드와 스크립트 실행은 계속 허용한다. 운영 전 점검 단계에서 사용하고 검증 후 실제 CSP 헤더로 전환해야 한다.
마무리
CSP는 웹페이지가 불러오거나 실행할 수 있는 콘텐츠의 출처를 제한하는 브라우저 보안 정책이다. 안전하게 설정하면 허용되지 않은 외부 스크립트와 인라인 코드의 실행을 차단해 XSS 공격의 피해 가능성을 줄일 수 있다.
처음에는 default-src 'self', script-src 'self', object-src 'none'처럼 단순한 정책에서 시작하고 서비스에 필요한 자원만 추가하는 것이 좋다. 인라인 스크립트가 필요하다면 'unsafe-inline' 대신 요청마다 새로 생성한 nonce나 검증된 해시를 사용해야 한다.
CSP 설정의 핵심은 모든 콘텐츠를 허용한 뒤 일부를 차단하는 것이 아니라, 기본적으로 제한하고 필요한 자원만 명확하게 허용하는 것이다. 안전한 출력 처리와 CSP를 함께 적용해야 XSS에 대한 실질적인 방어 효과를 얻을 수 있다.
핵심 키워드: CSP, Content-Security-Policy, CSP 설정 방법, XSS 방어, script-src, CSP nonce, CSP 헤더, 콘텐츠 보안 정책