幾天前協助幫公司產品導入一個外部 SDK 時,碰到了 block:csp
的問題。這篇文想來簡單記錄一下 CSP 是什麼和如何處理。
什麼是 CSP?
CSP 全名為 Content Security Policy ,它會藉由對頁面程式碼執行及資源載入的限制,一定程度內提升網站頁面的安全性。
最典型保護的就是腳本注入攻擊(XSS),譬如下方有一段 PHP 的程式碼:
<?php
// 資料庫帳號密碼等資訊...
// ...
$sql = "SELECT content FROM messages";
$result = $conn->query($sql);
// 這個 $messages 會存入 messages table 撈出的所有 messages
$messages = [];
if ($result->num_rows > 0) {
while($row = $result->fetch_assoc()) {
array_push($messages, $row["content"]);
}
}
// ...
// 其他邏輯...
?>
<!DOCTYPE html>
<html>
<head>
<title>Messages</title>
</head>
<body>
<h1>Messages</h1>
<form method="POST" action="/messages/create_message.php">
<input type="text" name="message">
<button type="submit">Send</button>
</form>
<?php foreach($messages as $message): ?>
<ul>
<!-- 渲染資料 -->
<li><?php echo $message; ?></li>
</ul>
<?php endforeach; ?>
</body>
這是一個非常簡易且含有 XSS 漏洞的訊息功能,它可以呈現與新增訊息。其中在這邊使用了 echo
:
<li><?php echo $message; ?></li>
PHP 的 echo
會將傳入的字串作為 HTML 渲染出來,使用者在訊息中可以填入任何東西,例如 A normal message
, <h1>Hello</h1>
, 或是輸入
<script
src="https://code.jquery.com/jquery-3.7.1.min.js"
integrity="sha256-/JqT3SQfawRcv/BIHPThkBvs0OEvtFFmqPF/lYI/Cxo="
crossorigin="anonymous"></script>
由於訊息會在頁面中渲染,他就等於直接幫所有使用此頁面的使用者安裝了 jQuery,這當然只是一個無傷大雅的小玩笑。然而,倘若他輸入的 src
內容是他自行建立的惡意腳本,無辜的使用者們就會因此受害。
可定義 CSP 的方式——使用 meta
這時候,如果我們在 <head>
中新增一個 <meta>
tag 並填入需要的內容,就可以讓瀏覽器去採取我們定義的 CSP:
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; img-src 'self'">
沒錯,就這麼單純而已!
這行 <meta>
tag 的 content default-src ‘self’
就是在告訴頁面說:「請你不要載入不同源的資源,包含 script, link 及 iframe」; img-src ‘self’
的意思則是:「請你不要載入不同源的圖片」。如此一來,如果有心人士嘗試進行攻擊就會被阻擋:
它也能防範直接在頁面內透過 <script>
執行 JavaScript:
如果使用者想要藉由 img
渲染不同網站的圖片也會被阻擋:
或是用 img
刻意存取不存在的圖片並透過 onerror
去執行 JavaScript 的行為:
這樣設定的情況下,瀏覽器只會載入同源的 .css
和 .js
資源:
在專案採用 Bundler 的狀況下,多數套件或框架最終都會被 Bundle 成一包或數包同源的資源在頁面中載入,如此就可以讓產品即使存在 XSS 的漏洞時,也能一定程度地防範產品使用者被有心人士攻擊。
但如果資源必須是在不同源的情況被載入,如 GA 或一些第三方的 SDK(可簡單理解為不同源的 .js 檔案),它們因為會有隨時更新的可能性,不允許我們將其下載下來直接存在我們自己的伺服器,這時候我們就可以針對這些比較特殊的第三方 SDK 設為例外的白名單:
<meta http-equiv="Content-Security-Policy" content="default-src 'self' cdnjs.cloudflare.com; img-src 'self' fakeimg.pl">
以這個例子來說,瀏覽器除了同源外,也會允許 cdnjs.cloudfare.com 的資源及 fakeimg.pl 的圖片:
<!DOCTYPE html>
<html>
<head>
<title>Messages</title>
<!-- CSP -->
<meta http-equiv="Content-Security-Policy" content="default-src 'self' cdnjs.cloudflare.com; img-src 'self' fakeimg.pl">
<link rel="stylesheet" type="text/css" href="/static/style.css">
</head>
<body>
<h1>Messages</h1>
<form method="POST" action="/messages/create_message.php">
<input type="text" name="message" />
<button type="submit">Send</button>
</form>
<?php foreach($messages as $message): ?>
<ul>
<li><?php echo $message; ?></li>
</ul>
<?php endforeach; ?>
<img src="https://fakeimg.pl/440x320/282828/eae0d0/?retina=1" alt="">
<img src="https://i.imgur.com/gLoK2Vw.jpeg" alt="">
<script src="https://www.not-friendly-site.tw/attack.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js" integrity="sha512-v2CJ7UaYy4JwqLDIrZUI/4hqeoQieOmAZNXBeQyjo21dadnwR+8ZaIJVT8EE2iyI61OV8e6M8PP2/4hpQINQ/g==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="/static/main.js"></script>
從上圖就可以看到,https://www.not-friendly-site.tw/attack.js
因為不符合頁面的 CSP 規範被阻擋、https://i.imgur.com/gLoK2Vw.jpeg
亦同。
另一個定義 CSP 的方式——寫在 Response header 中
CSP 除了 <meta>
外,也能定義在 Response header 中。以 PHP 為例:
header("Content-Security-Policy: default-src 'self' cdnjs.cloudflare.com; img-src 'self' fakeimg.pl");
在對頁面進行請求時,瀏覽器就會依據 Content-Security-Policy
的內容來遵循開發者定義的 CSP:
由於 Response Header 通常會由網頁伺服器來定義,如果專案的 CSP 不是寫在 <meta>
tag 中,就要請後端或 SRE 幫忙調整設定了,例如我們的產品就是將 CSP 定義在 Response Header 中,後來是 CTO 大大協助改設定後就解決了載入不了某個第三方 SDK 的問題。
結語
CSP 可以一定程度地提升頁面的安全,進一步提升頁面被惡意攻擊的防禦力,但在開發上還是盡可能地預防讓有心人士攻擊的機會,不要把防禦的職責全部丟給瀏覽器我覺得才是最好的。
關於 block:csp 的狀況說明和解決方式就記錄到這,如果內文有任何錯誤也歡迎不吝指出,感謝你閱讀至此。
References:
https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP