# Content Security Policy (CSP) 筆記
**Content Security Policy (CSP) 內容安全政策**
**Content Security Policy**是寫給**瀏覽器**看的
他寫在從伺服端回應給使用者瀏覽器端網頁的**HTTP Header**裡
主要用來**限制**網頁中對外部的請求來源(例如:css,js(ajax,ws),webfont,img,video,iframe等等)
還有一部份是禁止HTML行內的JS或CSS運作
以及限制`<form>`表單的指向網址
過濾XSS語法是伺服器端語言的工作(例如PHP)
如果不幸過濾失敗的話**CSP**的功能可以**阻止**惡意語法在瀏覽器端運作
算是多一道擋XSS的防線
(要注意是規則要寫對才能起作用,寫法限制太寬鬆還是會阻止不了)
另外也要注意CSP白名單列入外部JS library CDN可能造成的風險
除了要用[SRI](https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity)檢查內容完整性避免被竄改之外
還要注意這些library的安全性問題
因為這些CDN含有大量開源的library,而且新舊版同時存在,若語法有安全問題則可能會被XSS語法引用來攻擊
* 參考:
* [Orange: A Wormable XSS on HackMD!](https://blog.orange.tw/2019/03/a-wormable-xss-on-hackmd.html)
* [XSS without HTML: Client-Side Template Injection with AngularJS](https://portswigger.net/blog/xss-without-html-client-side-template-injection-with-angularjs)
* [How To Bypass CSP By Hiding JavaScript In A PNG Image]()
:::success
這篇只是挑重點寫的筆記,建議有看到任何不懂的東西就去google更詳細的資料來讀,
還有至少要把MDN的資料讀過
然後CSP也一直有在更新增加新語法,也建議常去關注新版本動態
[Content-Security-Policy - HTTP | MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy)
更進階的話就是去讀CSP可能會被繞過的手法,來減少自己寫出的CSP語法產生漏洞的可能性(像是上面的例子)
:::
### 本頁主要資料來源:
* [Content-Security-Policy - HTTP | MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy) - 這個很重要,建議看完
* [Content Security Policy - Wikipedia](https://en.wikipedia.org/wiki/Content_Security_Policy)
* [Content Security Policy Cheat Sheet - OWASP](https://www.owasp.org/index.php/Content_Security_Policy_Cheat_Sheet)
* [CSP Evaluator](https://csp-evaluator.withgoogle.com/) - Google提供的CSP檢測工具
###### tags: `Security`, `HTTP Header`, `Content Security Policy`, `CSP`
## CSP基本範例
* **CSP**可以完全限制外部連入的檔案和行內語法,這是預設全部阻擋的寫法(最安全):
```
content-security-policy: default-src 'none';
```
* 或是以**白名單**的形式允許信任的外部來源:
```
content-security-policy: default-src 'none'; script-src 'self' https://ajax.googleapis.com;
```
建議可以先用瀏覽器的開發工具(F12)去看看Facebook和Twitter的`content-security-policy`寫法
```md
1. Chrome瀏覽器開Twitter
2. F12 --> Network --> F5重新載入網頁 -->
3. 拉到最上面選取第一個載入的檔案(主網頁檔) -->
4. Headers --> Response Headers --> content-security-policy
```
:::warning
有實測一下`default-src 'none';`無法阻止Tampermomkey這類瀏覽器插件的腳本
(以前運作方式不同似乎可以用CSP擋,但是現在實測過是無法阻擋了)
也有一些瀏覽器插件可以直接停用CSP
所以CSP的功能應該沒辦法拿來阻擋想用腳本修改網頁的人
:::
## CSP在各語言的寫法
*根據MDN的資料說CSP HTTP Header語法如果重複出現,那麼效果會疊加並且採取最嚴格的設定(不過各家瀏覽器或是伺服器有沒有正確支援還是建議實測過比較安全)
### 原始HTTP Header:
HTTP Header是以換行當成區格,
要注意所有**CSP內容**要輸出成**同一行字串**
```
content-security-policy: default-src 'none'; img-src 'self' data:;
```
### Apache (.htaccess)
```apache=
<IfModule mod_headers.c>
Header set Content-Security-Policy " \
default-src 'none'; \
img-src 'self' data:; \
"
</IfModule>
```
如果子目錄要移除上層目錄的CSP規則可以用`Header unset`移除
```apache=
<IfModule mod_headers.c>
Header unset Content-Security-Policy
</IfModule>
```
### PHP
```php=
<?php
header("Content-Security-Policy: default-src 'none'; img-src 'self' data:;");
?>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>test</title>
</head>
<body>
<p>test</p>
<img src="example.jpg">
</body>
</html>
```
### HTML
```htmlmixed=
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; img-src 'self' data:;">
<title>test</title>
</head>
<body>
<p>test</p>
<img src="example.jpg">
</body>
</html>
```
## CSP指令(Directives)
:::info
這些指令經常變動,建議常去MDN看一下目前變動的狀態
https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy
:::
### Content Security Policy 1.0
* `default-src` - **預設值**,以下種類未設定時會採用`default-src`的設定值
* 這個一定要設定以避免遺漏,至少要設定成`'none'`或`'self'`
* 部份語法和`default-src`有差異的指令不能使用這個預設值(詳細請看MDN)
* `img-src` - `<img src="...">`
* `media-src` - `<video>` `<audio>` `<source>` `<track>`
* `style-src` - `<link rel="stylesheet" href="style.css">`
* `script-src` - `<script>`
* `font-src` - `@font-face`
* `frame-src` - `<iframe>`
* (Level 2時被棄用,但是Level 3又被加回來)
* `object-src` - `<object>` `<embed>` `<applet>`
* `connect-src` - `XMLHttpRequest()` `WebSocket()` `EventSource()` `sendBeacon()` `fetch()`
* ajax連外部.json檔要改這一個
* websocket
* `sandbox` - 指定`<iframe>` sandbox的屬性
* 用法: [CSP: sandbox - HTTP | MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/sandbox)
### Content Security Policy Level 2
* `base-uri` - `<base>`
* 用法: [CSP: base-uri - HTTP | MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/base-uri)
* `child-src` - `<iframe>` `Worker()`
* 建議改用`frame-src`和`worker-src`來個別定義
* `form-action` - 限制`<form action="...">`action所指向的目標
* [https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/form-action](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/form-action)
* `frame-ancestors` - 讓瀏覽器阻止其他網站嵌入這個網頁(防止網頁被拿去釣魚或欺騙點擊操作)
* 阻止的嵌入方式: `<frame>`, `<iframe>`, `<object>`, `<embed>`, or `<applet>`
* 用法: [CSP: frame-ancestors | MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/frame-ancestors)
* 其他功能類似且非CSP的舊方法: [X-Frame-Options | MDN](https://developer.mozilla.org/zh-TW/docs/Web/HTTP/Headers/X-Frame-Options)
* `plugin-types` - 限制`<embed>`, `<object>` or `<applet>`的MIME類型
* 用法: [CSP: plugin-types - HTTP | MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/plugin-types)
* **[已棄用]** `referrer`
* `report-uri` - 當用戶端違反CSP時,回傳給server的接收網址(HTTP POST json格式)
* 這個功能將被棄用,要改成`report-to`(但是大多瀏覽器還不支援)
* `upgrade-insecure-requests;` - 強制將網頁內容中所有`http://`請求升級成`https://`
* 若請求來源不支援`https://`不會降級回`http://`,而是請求失敗
* 這個語法不能讓網頁本身的`http://`轉跳到`https://`,若要轉跳要用`Strict-Transport-Security`
* 參考資料: [Strict-Transport-Security - HTTP | MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Strict-Transport-Security)
* `Strict-Transport-Security`是獨立的HTTP header,不在CSP範圍內
* `Strict-Transport-Security`是瀏覽器層級的轉跳
* 要從伺服端層級轉跳請找`301 redirect`重新定向語法
* `require-sri-for` - 強制外部引入的`script`或`style`必須使用SRI驗證內容完整性
* 語法:
* `Content-Security-Policy: require-sri-for script;`
* `Content-Security-Policy: require-sri-for style;`
* `Content-Security-Policy: require-sri-for script style;`
* 參考資料:
* [CSP: require-sri-for - HTTP | MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/require-sri-for)
* [Subresource Integrity (SRI) - Web security | MDN](https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity)
* `block-all-mixed-content;` - 禁止https/http混合內容
### Content Security Policy Level 3
* `manifest-src` - **icon圖標** `<link rel="manifest" href="mainfest.json">`
* **[尚未支援]** `navigate-to`
* `report-to` - 用來取代並棄用舊的`report-uri`,但是還沒被所有主流瀏覽器支援
* `worker-src` - `Worker()`
### 草案
* `trusted-types`
* Chrome v83 才開始支援(2020年5月)
* 比較簡略的講法是禁止在`el.innerHTML = "<img src="text.jpg>"`輸入HTML標籤,要改用`const img = document.createElement('img')`取代,利用`img.src = 'xyz.jpg'`參數的方式輸入值,以避免外來的HTML標籤輸入值造成XSS攻擊
* 詳細用法請參考: [Prevent DOM-based cross-site scripting vulnerabilities with Trusted Types](https://web.dev/trusted-types/)
* 這個用法需要大量改寫舊有的JS寫法,還有開下去可能會讓很多舊的JS框架會掛掉
Don't
```javascript
el.innerHTML = '<img src=xyz.jpg>';
```
Do
```javascript
el.textContent = '';
const img = document.createElement('img');
img.src = 'xyz.jpg';
el.appendChild(img);
```
## CSP語法(Syntax)
CSP以**白名單**的形式運作
建議以 `default-src 'none';` 或是 `default-src 'self';`
當成起頭再來設定細部選項,以提高安全性
若沒設定到時會自動採用default-src的設定值,避免遺漏
* `'none'` - 全部不允許
* `'self'` - 允許同網域
* `指定網域` < host-source > - 指定允許的網域,以空格分開
* 包含或不包含`http://`,`https://`前綴的網址,可加`*`萬用字元
* `example.com`
* `*.example.com`
* `https://example.com`
* `https://*.example.com`
* `wss://example.com` - 測試過`wss://`前綴不能省略一定要加
* `wss://*.example.com`
* < scheme-source >
* `http:`
* `https:`
* `data:`
* `mediastream:`
* `blob:`
* `filesystem:`
* `*` - 允許所有URL,除了`data:` `blob:` `filesystem:`
* 儘量不要使用`*`這種過於寬鬆的規則
* `'unsafe-inline'` - 允許html行內css或js
* 最常出現XSS攻擊語法的地方就是inline(例如伺服端語言過濾XSS失敗),所以建議將自有的JS,CSS語法全寫成獨立檔案,最好能不要開啟這個權限
* `'unsafe-eval'` - 允許`eval()`
* `eval()`是將字串重新轉回成可執行的程式碼,本身就是高風險的語法,建議不要開啟權限
* `'nonce-<base64-value>'`
* `'<hash-algorithm>-<base64-value>'`
最後兩個驗證碼比對用法請參考: [CSP: script-src - HTTP | MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/script-src)
## 常用白名單範例(以Apache .htaccess寫法為例)
:::info
常用的外部網站檔案用英文關鍵字丟Google都會有人寫過,不會寫的話可以用Google找比較快(通常寫法也會比自己寫的完整)
:::
### Google Analytics
```apache=
<IfModule mod_headers.c>
Header set Content-Security-Policy " \
default-src 'self'; \
script-src 'self' https://www.google-analytics.com https://www.googletagmanager.com; \
img-src 'self' data: https://www.google-analytics.com; \
"
</IfModule>
```
### Google CND (jQuery之類的CDN外連.js)
```apache=
<IfModule mod_headers.c>
Header set Content-Security-Policy " \
default-src 'self'; \
script-src 'self' https://ajax.googleapis.com; \
"
</IfModule>
```
```htmlmixed=
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
```
### Youtube
```apache=
<IfModule mod_headers.c>
Header set Content-Security-Policy " \
default-src 'self'; \
frame-src 'self' https://www.youtube.com; \
"
</IfModule>
```
```htmlmixed=
<iframe width="560" height="315" src="https://www.youtube.com/embed/vtwiz6P1rdw" frameborder="0" allow="autoplay; encrypted-media" allowfullscreen></iframe>
```
### OBS Studio (Browser Source)
如果你的網頁有要嵌入OBS中,記得要加`data:`才能修改自訂CSS
```apache=
<IfModule mod_headers.c>
Header set Content-Security-Policy " \
default-src 'self'; \
style-src 'self' 'unsafe-inline' data:; \
"
</IfModule>
```
## 其他CSP以外的安全性設定
### Referrer-Policy
[Referrer-Policy - HTTP | MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy) (使用細節請看MDN內文)
* 一個網頁中點選網址或嵌入圖片(所有外部來源`<a>`, `<area>`, `<img>`, or `<link>` )都會送出Referrer(來源網址)
* 網頁流量分析就是靠這個Referrer來取得來源網址
* 像是Google搜尋完點選網址到**目標網頁**就會送出完整的Referrer,所以**目標網頁**的流量分析工具就能知道這個用戶是用Google搜尋進來的,而且搜了什麼關鍵字都知道(因為關鍵字有在網址參數上面)
* 所以這個功能主要是可以增加用戶隱私,限制Referrer送出的方式,避免來源網址被第三方網站取得來源網址
* 舉例來說像是社群網站的網址都帶有使用者ID,這個功能就能有效限制用戶ID來源被第三方網站取得,對於增加用戶隱私有很大的幫助
* 避免內部機密網址(或是沒有完全對外公開的測試網站)被第三方網站取得網址
* **真的有機密的東西就不要放在公開網路上**
* 最嚴格的`no-referrer`可能會影響自己網站本身的流量分析和廣告運作(如果有放的話),所以還是要看狀況調整成就適合自己網站使用的參數
* `no-referrer`不能阻止Javascript的AJAX請求送出referrer
### CSP以外增加安全性的Header(以Apache .htaccess寫法為例)
```apache=
# =========== Header ===========
<IfModule mod_headers.c>
# 唯有當符合同源政策下,才能被嵌入到 frame 中。(防止網站被外部釣魚網站嵌入)
Header always append X-Frame-Options SAMEORIGIN
# 拒絕 MIME 類型錯誤的回應 (js,css)
Header set X-Content-Type-Options "nosniff"
# 將所有Cookie設為HttpOnly
Header always edit Set-Cookie (.*) "$1; HTTPOnly; Secure"
#隱藏PHP版本
Header always unset X-Powered-By
</IfModule>
# ==============================
# ========= 禁止訪問檔案 =========
# 這些檔案不應該放在外部網路能讀取到的地方
# 如果你還是上傳了這些檔案請把他給擋掉外部讀取
# 不然這會很危險
# .git可以把你所有程式碼還原
# composer.json, package.json可以知道你用了哪些package和版本
# 如果你用了舊版本沒更新,可能就會被找到漏洞
RedirectMatch 404 /\.git
<Files composer.json>
order allow,deny
deny from all
</Files>
<Files composer.lock>
order allow,deny
deny from all
</Files>
<Files .gitignore>
order allow,deny
deny from all
</Files>
<Files .htacces>
order allow,deny
deny from all
</Files>
<Files .htaccess.sample>
order allow,deny
deny from all
</Files>
<Files .php_cs>
order allow,deny
deny from all
</Files>
<Files .travis.yml>
order allow,deny
deny from all
</Files>
<Files CHANGELOG.md>
order allow,deny
deny from all
</Files>
<Files CONTRIBUTING.md>
order allow,deny
deny from all
</Files>
<Files CONTRIBUTOR_LICENSE_AGREEMENT.html>
order allow,deny
deny from all
</Files>
<Files COPYING.txt>
order allow,deny
deny from all
</Files>
<Files Gruntfile.js>
order allow,deny
deny from all
</Files>
<Files LICENSE.txt>
order allow,deny
deny from all
</Files>
<Files LICENSE_AFL.txt>
order allow,deny
deny from all
</Files>
<Files nginx.conf.sample>
order allow,deny
deny from all
</Files>
<Files package.json>
order allow,deny
deny from all
</Files>
<Files php.ini.sample>
order allow,deny
deny from all
</Files>
<Files README.md>
order allow,deny
deny from all
</Files>
# ===============================
# ========隱藏目錄下所有檔案========
<IfModule mod_autoindex.c>
Options -Indexes
</IfModule>
# ===============================
```
### 防止CDN來源的檔案被CDN業者篡改 (Subresource Integrity (SRI))
這個功能用法在[Bootstrap](https://getbootstrap.com/)的官網就能看到實例
利用`integrity`中的雜湊碼來檢查檔案是否被偷改過
相同的檔案雜湊碼每次算出來都會一樣
被偷改過的檔案雜湊碼則會不同(即使只改一個字雜湊碼也會完全不一樣)
雜湊碼不同時則檔案不會載入運行(更嚴謹的講法是會載入但不會運行,因為要有完整檔案才能給瀏覽器算雜湊碼)
:::warning
2021/11/3補充SRI的幾個坑(用Chrome 95實測過):
* SRI只會驗證html中引用且有正確使用SRI的那個單一檔案,被引用檔案中另外再引用的外部檔案則不會驗證(例如CSS中的`@import`,import的那個外部檔案不在驗證範圍內)
* `integrity=""` 中的雜湊碼格式如果填錯時會讓瀏覽器跳過驗證步驟,然後直接運行(例如雜湊碼前的`sha384-`忘記填時就不會驗證,會變成直接運行),chrome瀏覽器開發工具的console也不會顯示任何錯誤訊息
:::
```htmlmixed=
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.0/css/bootstrap.min.css" integrity="sha384-9gVQ4dYFwwWSjIDZnLEWnxCjeSWFphJiwGPXr1jddIhOegiu1FwO5qRGvFXOdJZ4" crossorigin="anonymous">
<script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.0/umd/popper.min.js" integrity="sha384-cs/chFZiN24E4KMATLdqdvsezGxaGsi4hLGOzlXwp5UZB1LY//20VyM2taTB4QvJ" crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.0/js/bootstrap.min.js" integrity="sha384-uefMccjFJAIv6A+rW+L4AHf99KvxDjWSu1z9VI8SKNVmz4sk7buKt/6v9KI65qnm" crossorigin="anonymous"></script>
```
* 參考資料: [Subresource Integrity (SRI) - Web security | MDN](https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity)
:::warning
真的建議認真評估一下是否為了省一點小流量而去使用這些外部JS library CDN
不然的話還是把檔案放自己伺服器比較安全
因為很難保證這些CDN永遠不會出問題,或是有心搞事
而且這些JS CDN含有大量開源library
另外這些library的新舊版也同時存在
根本不可能去檢查完這些library都沒問題
還有這些CDN高機率有在做流量分析
等於是你網站的流量分析被白白撿走(免費CDN的代價?)
就算他們沒有植入額外的JS流量分析code
他們還是能用使用者ip做最基本的流量分析
真的還是要用的話建議避開這些頁面:
* 使用者登入頁面
* 管理員後台頁面
* 含有使用者個資隱私的頁面
(如果你的網站有在使用Google Analytics或是其他外部流量分析工具也是建議避開這些頁面,以確保網站的使用者隱私,還有外部廣告也不應該放在這些頁面)
例子:
* https://www.inside.com.tw/article/4531
:::
#### 使用OpenSSL指令取得雜湊碼:
windows環境下可以直接用git附送的bash來輸入以下指令
```shell=
cat dist/main.js | openssl dgst -sha256 -binary | openssl base64 -A
cat dist/main.js | openssl dgst -sha384 -binary | openssl base64 -A
cat dist/main.js | openssl dgst -sha512 -binary | openssl base64 -A
```
#### 使用PHP取得雜湊碼:
:::warning
這篇是舉例用,建議實機上線不要這樣寫
應該把雜湊結果存起來直接寫在網頁裡
而不是每次連入都算一次,因為這樣會很浪費伺服器CPU運算
:::
```php=
<?php
header("Content-type:text/html;charset=utf-8");
/*
openssl指令:
(windows環境下可以直接用git附送的bash來輸入以下指令)
cat dist/main.js | openssl dgst -sha256 -binary | openssl base64 -A
cat dist/main.js | openssl dgst -sha384 -binary | openssl base64 -A
cat dist/main.js | openssl dgst -sha512 -binary | openssl base64 -A
*/
/*
列出php支援的算法
https://www.php.net/manual/zh/function.openssl-get-md-methods.php
*/
//print_r(openssl_get_md_methods(true));
$file = 'dist/main.js';
$file_contents = file_get_contents($file);
//留一個你要用的算法就好
$sha256 = 'sha256-' . base64_encode( openssl_digest($file_contents , 'sha256', TRUE) );
$sha384 = 'sha384-' . base64_encode( openssl_digest($file_contents , 'sha384', TRUE) );
$sha512 = 'sha512-' . base64_encode( openssl_digest($file_contents , 'sha512', TRUE) );
?>
<html>
<head>
<title>test</title>
</head>
<body>
<!--留一個你要用的算法就好-->
<script src="<?php echo $file; ?>" integrity="<?php echo $sha256; ?>" crossorigin="anonymous"></script>
<script src="<?php echo $file; ?>" integrity="<?php echo $sha384; ?>" crossorigin="anonymous"></script>
<script src="<?php echo $file; ?>" integrity="<?php echo $sha512; ?>" crossorigin="anonymous"></script>
</body>
</html>
```
### 防止點網址開新視窗時原網頁被切換篡改 (noopener)
白話講就是在A網頁點開B網頁連結的分頁
這時如果B網頁如果帶有惡意JS語法,那他可以把A網頁導到其他頁面
假設像是A網頁如果是fb,如果被導到假的fb登入頁面就可能會被騙走帳號密碼
這個連結寫法可以防止這個問題產生(細節原理請自行google `noopener`)
```htmlmixed=
<a rel="noopener noreferrer" target="_blank" href="http://example.com/">連結</a>
```