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檢查內容完整性避免被竄改之外
還要注意這些library的安全性問題
因為這些CDN含有大量開源的library,而且新舊版同時存在,若語法有安全問題則可能會被XSS語法引用來攻擊

還有Cookie在相同網域下(相同作用域)的所有網頁都能取得同樣的cookie,所以要把相同作用域網頁的CSP保持在一樣高的安全性設定,以防cookie被從CSP安全性比較低的網頁竊取走
如果真的要在特定目錄降低CSP安全性設定的話
另一種解決方式是用不同子網域來區隔開cookie的作用域

*如果可以的話建議將含有重要資訊的Cookie設為HttpOnly,這種狀態下瀏覽器端的JS就沒有權限存取cookie(方法請自行google)

這篇只是挑重點寫的筆記,建議有看到任何不懂的東西就去google更詳細的資料來讀,
還有至少要把MDN的資料讀過
然後CSP也一直有在更新增加新語法,也建議常去關注新版本動態
Content-Security-Policy - HTTP | MDN

更進階的話就是去讀CSP可能會被繞過的手法,來減少自己寫出的CSP語法產生漏洞的可能性(像是上面的例子)

基本範例

  • CSP可以完全限制外部連入的檔案和行內語法,這是預設全部阻擋的寫法(最安全):
1
content-security-policy: default-src 'none';
  • 或是以白名單的形式允許信任的外部來源:
1
content-security-policy: default-src 'none'; script-src 'self' https://ajax.googleapis.com;

建議可以先用瀏覽器的開發工具(F12)去看看Facebook和Twitter的content-security-policy寫法

1
2
3
4
1. Chrome瀏覽器開Twitter
2. F12 --> Network --> F5重新載入網頁 -->
3. 拉到最上面選取第一個載入的檔案(主網頁檔) -->
4. Headers --> Response Headers --> content-security-policy

本頁主要資料來源:

有實測一下default-src 'none';無法阻止Tampermomkey這類瀏覽器插件的腳本
(以前運作方式不同似乎可以用CSP擋,但是現在實測過是無法阻擋了)
也有一些瀏覽器插件可以直接停用CSP
所以CSP的功能應該沒辦法拿來阻擋想用腳本修改網頁的人

各語言寫法

*根據MDN的資料說CSP HTTP Header語法如果重複出現,那麼效果會疊加並且採取最嚴格的設定(不過各家瀏覽器或是伺服器有沒有正確支援還是建議實測過比較安全)

原始HTTP Header:

HTTP Header是以換行當成區格,
要注意所有CSP內容要輸出成同一行字串

1
content-security-policy: default-src 'none'; img-src 'self' data:;

Apache (.htaccess)

1
2
3
4
5
6
<IfModule mod_headers.c>
Header set Content-Security-Policy " \
default-src 'none'; \
img-src 'self' data:; \
"
</IfModule>

如果子目錄要移除上層目錄的CSP規則可以用Header unset移除

1
2
3
<IfModule mod_headers.c>
Header unset Content-Security-Policy
</IfModule>

PHP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?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

1
2
3
4
5
6
7
8
9
10
11
12
<!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>

指令 Directives

  • default-src - 預設值,以下種類未設定時會採用default-src的設定值
    • 這個一定要設定以避免遺漏,至少要設定成'none''self'
  • img-src - <img src="...">
  • media-src - <video> <audio> <source> <track>
  • style-src - <link rel="stylesheet" href="style.css">
  • script-src - <script>
  • manifest-src - icon圖標 <link rel="manifest" href="mainfest.json">
  • object-src - <object> <embed> <applet>
  • frame-src - <iframe>
  • frame-ancestors - <frame>, <iframe>, <object>, <embed>, or <applet>
  • worker-src - Worker()
  • child-src - <iframe> Worker() 已棄用,請改用上面兩個
  • form-action - <form>
  • font-src - @font-face
  • connect-src - XMLHttpRequest() WebSocket() EventSource() sendBeacon() fetch()
    • ajax連外部.json檔要改這一個
    • websocket
  • block-all-mixed-content; - 禁止https/http混合內容
  • upgrade-insecure-requests; - 強制將網頁內容中所有http://請求升級成https://
    • 若請求來源不支援https://不會降級回http://,而是請求失敗
    • 這個語法不能讓網頁本身的http://轉跳到https://,若要轉跳要用Strict-Transport-Security
      • 參考資料: Strict-Transport-Security - HTTP | MDN
      • Strict-Transport-Security是獨立的HTTP header,不在CSP範圍內
      • Strict-Transport-Security是瀏覽器層級的轉跳
      • 要從伺服端層級轉跳請找301 redirect重新定向語法
  • require-sri-for - 強制外部引入的scriptstyle必須使用SRI驗證內容完整性
  • report-uri - 當用戶端違反CSP時,回傳給server的接收網址(HTTP POST json格式)
    • 這個功能將被棄用,要改成report-to(但是大多瀏覽器還不支援)

語法 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:
  • * - 全部允許(儘量不要用)
  • 'unsafe-inline' - 允許html行內css或js
    • 最常出現XSS攻擊語法的地方就是inline,所以建議將自有的JS,CSS語法全寫成獨立檔案,最好能不要開啟這個權限
  • 'unsafe-eval' - 允許eval()
    • eval()是將字串重新轉回成可執行的程式碼,本身就是高風險的語法,不建議開啟權限
  • 'nonce-<base64-value>'
  • '<hash-algorithm>-<base64-value>'

最後兩個驗證碼比對用法請參考: CSP: script-src - HTTP | MDN

常用白名單範例(以Apache .htaccess寫法為例)

  • 常用的外部網站檔案用英文關鍵字丟Google都會有人寫過,不會寫的話可以用Google找比較快(通常寫法也會比自己寫的完整)

Google Analytics

1
2
3
4
5
6
7
<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)

1
2
3
4
5
6
<IfModule mod_headers.c>
Header set Content-Security-Policy " \
default-src 'self'; \
script-src 'self' https://ajax.googleapis.com; \
"
</IfModule>
1
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>

Youtube

1
2
3
4
5
6
<IfModule mod_headers.c>
Header set Content-Security-Policy " \
default-src 'self'; \
frame-src 'self' https://www.youtube.com; \
"
</IfModule>
1
<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

1
2
3
4
5
6
<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 (使用細節請看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寫法為例)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
# =========== 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官網就能看到實例
利用integrity中的雜湊碼來檢查檔案是否被偷改過
相同的檔案雜湊碼每次算出來都會一樣
被偷改過的檔案雜湊碼則會不同(即使只改一個字雜湊碼也會完全不一樣)
雜湊碼不同時則檔案不會載入運行(更嚴謹的講法是會載入但不會運行,因為要有完整檔案才能給瀏覽器算雜湊碼)

1
2
3
4
5
<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>

使用OpenSSL指令取得雜湊碼:

windows環境下可以直接用git附送的bash來輸入以下指令

1
2
3
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取得雜湊碼:

這篇是舉例用,建議實機上線不要這樣寫
應該把雜湊結果存起來直接寫在網頁裡
而不是每次連入都算一次,因為這樣會很浪費伺服器CPU運算

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
<?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)

1
<a rel="noopener noreferrer" target="_blank" href="http://example.com/">連結</a>