본문 바로가기

Daylogs/Javascript

크롬 익스텐션의 컨텐트 보안 정책 (CSP)

발생일: 2014.12.28

키워드: Content Security Policy, CSP, Chrome Extension, 크롬 확장, 크롬 익스텐션, angular, google analytics, 구글 애널리틱스


문제:
크롬 익스텐션에서 앵귤러를 사용하려고 추가했는데, 아래와 같은 오류가 나면서 실행되지 않는다.

Refused to evaluate a string as JavaScript because 'unsafe-eval' is not an allowed source of script in the following Content Security Policy directive: "script-src 'self' https://ssl.google-analytics.com".

오류 구문을 보니 앵귤러의 코드에서 내부적으로 `eval()`을 사용하고 있고,
익스텐션의 컨텐트 보안 정책 때문에 `eval()`의 실행이 제한된 것 같다.

지금도 구글 애널리틱스 스크립트를 로드하려고 오류 구문에 보이는 것처럼 익스텐션의 보안 설정을 따로 설정해두긴 했다.

"script-src 'self' https://ssl.google-analytics.com"

여기에 `unsafe-eval`에 대한 구문을 추가해야 할 것 같다.

이참에 크롬 익스텐션의 보안 정책에 대해 좀 정리해둬야겠다.



해결책:

개요

XSS 공격을 막기 위해서 크롬 익스텐션은 Content Security Policy (이하 CSP)라는 보안 정책을 갖고 있다.
익스텐션에서 로드되거나 실행되는 컨텐츠의 범위를 제한하는 방법으로 동작하며,
블랙리스트 방식과 화이트리스트 방식을 모두 적용할 수 있다. 

크롬 익스텐션엔 기본적으로 크롬 API와 호스트 등을 제한하는 Permissions 정책이 있긴 하지만,
CSP는 이보다 더 상위에, 별도로 존재하는 보안 정책이다.

CSP는 manifest.json 파일에 아래와 같은 형태로 정의할 수 있다.

{
  ...,
  "content_security_policy": "[POLICY STRING GOES HERE]"
  ...
}


기본값

매니페스트 파일에 `content_security_policy`를 정의하지 않으면, 기본적으로 아래 값이 할당된다.

script-src 'self'; object-src 'self'

위 구문은 다음과 같이 스크립트의 실행을 제한한다

(A) eval() 이나 문자열로 실행하는 함수, 함수 생성자를 제한한다.
(B) 페이지 내의 인라인 자바스크립트를 제한한다.
(C) 로컬 경로의 스크립트만 로드한다. 익스텐션 이외의 스크립트는 로드할 수 없다.

예를 들면, 아래 스크립트는 기본으로 할당된 정책에 의해 실행할 수 없다.

eval(‘alert(1)’); // (A)
new Function(‘return ‘foo’); // (A)
setTimeout(‘alert(1)’, 100); // (A)

<button onclick=“alert(1);”> … // (B) 인라인 이벤트 핸들러
<script>alert(‘1’);</script> // (B) 페이지 내의 <script> 태그

<script src=“http://ajax.googleapi.com/…/jquery.js”></script> // (C) 외부 경로의 스크립트는 실행되지 않는다.


제한 허용하기

인라인 스크립트

인라인 스크립트의 실행 제한을 허용하는 방법은 제공하지 않는다.
정책 문구에 `unsafe-inline`을 적더라도 인라인 스크립트의 실행을 허용하지 않는다.


외부의 스크립트

jquery 등의 외부 라이브러리의 사용이 필요하거나, 구글 애널리틱스처럼 동적으로 외부의 스크립트를 불러오는 코드를 사용해야 한다면, 화이트 리스트 방식으로 소스의 오리진을 추가할 수 있다.

그렇지만, man-in-the-middle attack 과 같은 네트워크 단의 공격을 회피하기 위해,
SSL이 적용되지 않은 HTTP 리소스는 허용하지 않는다.

현재는, 아래 스킴의 리소스만 화이트리스트로 추가할 수 있다.

    blob, filesystem, https, chrome-extension, chrome-extension-resouce 

또한, https 와 chrome-extension 스킴에서는 리소스의 호스트 부분은 반드시 명시되어 있어야한다.
`https:`, `https://*`, `https://*.com`과 같이 호스트 부분에 와일드카드를 사용해 정의하는 것은 허용하지 않는다.
다만, `https://*.example.com`과 같이 서브 도메인 영역에 사용하는 와일드카드는 허용한다.

Public Suffix list에 등록되어 있는 도메인은 최상위 도메인으로 취급하며,
이 경우 서브 도메인을 명시적으로 정의해야 한다.
예를 들어, 클라우드 호스팅 서비스 경로인 `https://*.cloudfront.net`은 허용하지 않지만,
`https://XXX.cloudfront.net`이나, `https://*.XXX.cloudfront.net`은 허용한다.

개발 편의를 위해 로컬 머신의 리소스에 한해 화이트리스트 방식으로 추가할 수 있다.
`http://127.0.0.1`이나 `http://localhost`의 리소스는 HTTP이지만 허용한다.


HTTPS 스킴의 리소스를 사용할 수 있게 하려면, manifest.json 파일에 아래와 같이 정책을 추가하면 된다.

"content_security_policy": "script-src 'self' https://example.com; object-src 'self'"

참고로, `script-src`와 `object-src`는 모두 정책에 의해 정의되어 있고, 재정의하더라도 기재해줘야 한다.
크롬은 자기 자신(`self`)의 값을 허용하지 않는 것을 허용하지 않는다.


자바스크립트 실행

`eval()`이나 `setTimeout(String)`, `new Function(String)`과 같은 자바스크립트를 실행하도록 한다면,
`unsafe-eval`을 추가하는 방법으로 허용할 수 있다.

위 문제점의 앵귤러가 실행되지 않은 원인에 해당하는 정책이기도 하며, 아래와 같이 정의하면 된다.

"content_security_policy": "script-src 'self’ ‘unsafe-eval'; object-src 'self'"

이 설정은 XSS 공격에 취약하기 때문에, 설정한다면 취약한 곳이 없는지 잘 살펴봐야 한다.


컨텐트 스크립트

위에서 언급한 제한은 익스텐션의 백그라운드 페이지나 이벤트 페이지에 해당하는 것이었다.
컨텐트 스크립트에서는 위에 정의한 것보다 조금 더 복잡한다.

컨텐트 스크립트는 일반적으로 익스텐션의 CSP 대상이 아니고, CSP의 룰이 적용되지도 않기 때문이다.
CSP에서 `unsafe-eval`을 허용하지 않더라도 이미 컨텐트 스크립트에서는 `eval()`을 실행할 수 있다.

또한, 컨텐트 스크립트는 페이지의 CSP에도 적용되지 않는다.
그래서, DOM에 주입되는 스크립트들 상황에 따라 동작 방식이 다를 수 있다.
예를 들어, 컨텐트 스크립트에서 아래 코드가 실행되었다고 가정해보자.

document.write("<script>alert(1);</script>");

이 컨텐트 스크립트는 `document.write()`가 실행되자마자 `alert()`을 실행한다.
페이지의 정책이 적용되기 전에 컨텐트 스크립트가 실행됐기 때문이다.

페이지의 CSP가 `script-src ‘self’`로 정의되었다고 가정하고,
컨텐트 스크립트에서 아래와 같은 코드를 실행했다고 해보자.

document.write("<button onclick='alert(1);'>click me</button>'");

사용자가 버튼을 클릭하면 onclick 핸들러가 실행되는데, 이 스크립트는 페이지의 CSP에 의해 실행이 제한된다.
클릭 이벤트 핸들러는 컨텐트 스크립트에 의해 실행된 것이 아니라,
페이지 내에서 실행됐기 때문에 페이지의 CSP 정책이 적용되었기 때문이다.

위 코드를 의도한 대로 실행하고자 한다면, 아래처럼 컨텐트 스크립트의 영역에서 실행되도록 하면 된다.

document.write("<button id='mybutton'>click me</button>'");
var button = document.getElementById('mybutton');
button.onclick = function() {
    alert(1);
};


또 다른 예제를 보면 좀 더 확실하게 이해할 수 있다.

var script = document.createElement('script');
script.innerHTML = 'alert(1);'
document.getElementById('body').appendChild(script);

위의 `alert(1)`은 컨텐트 스크립트의 환경에서 실행되기 때문에 문제 없이 잘 실행된다.
하지만, 아래 스크립트는 좀 다르다.

var script = document.createElement('script');
script.innerHTML = 'eval("alert(1);")';
document.getElementById('body').appendChild(script);

`eval()`까지는 컨텐트 스크립트의 환경에서 실행되었지만, `alert(1)`은 페이지의 CSP에 적용돼 제한된다.


DOM에 주입하는 스크립트는 익스텐션을 어떻게 작성하는지에 따라 달라진다.
컨텐트 스크립트가 페이지의 CSP에 영향을 받지 않기 때문에,
가능하다면 DOM에 스크립트를 추가하는 것보다는 컨텐트 스크립트에 정의하는 것이 좋다.