티스토리 뷰

발생일: 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에 스크립트를 추가하는 것보다는 컨텐트 스크립트에 정의하는 것이 좋다.





반응형
댓글
공지사항