前一篇講到:
Frontend 把按鈕藏起來,不代表 Backend 就真的安全。
今天換另一種很容易讓人低估的情況:
我只是把一段 HTML 顯示出來而已,為什麼還要特別防?
例如做 Email Preview:
Backend 回一段 HTML
↓
Frontend 顯示預覽
第一眼看起來很單純。
但只要這段內容是:
<html>
...
</html>
就不能只把它當成:
一段普通文字。
因為 HTML 本身可能具有:
Script
Link
Form
Navigation
其他瀏覽器行為
所以「怎麼顯示」本身就是一個安全問題。
假設:
const content = "<h1>Hello</h1>";
如果當一般文字顯示:
<div>{content}</div>
畫面可能看到:
<h1>Hello</h1>
React 會把它當:
文字
而不是 HTML。
但如果真的想把它 Render 成:
<h1>Hello</h1>
事情就不一樣了。
因為現在 Browser 會開始:
解析這段 HTML。
很多人第一次想到 HTML,可能只會想到:
<h1>
<p>
<table>
<img>
但 HTML 文件裡也可能存在:
<script>
或:
<form>
甚至可以包含會導向其他地方的:
<a href="...">
所以如果一段內容來源不完全可信,
直接把它當成頁面的一部分執行,
風險就會提高。
XSS:
Cross-Site Scripting
可以先理解成:
原本只是想顯示內容,結果裡面的惡意程式碼被瀏覽器執行了。
例如概念上:
<script>
// 惡意程式碼
</script>
如果這種內容真的能在網站環境中執行,
就可能造成安全問題。
所以前端碰到:
HTML String
時,
不能只問:
顯示出來了嗎?
還要問:
Browser 會把裡面的哪些能力一起打開?
假設 Backend 回:
<html>
<body>
<h1>Invoice</h1>
<p>Hello Customer</p>
</body>
</html>
如果直接塞進目前頁面的 DOM,
這段 HTML 會跟:
目前 App
混在同一個頁面環境裡。
而 iframe 的概念比較像:
主頁面
┌─────────────────────────┐
│ React App │
│ │
│ ┌─────────────────┐ │
│ │ iframe │ │
│ │ │ │
│ │ Email HTML │ │
│ │ │ │
│ └─────────────────┘ │
│ │
└─────────────────────────┘
也就是:
把預覽內容放在另一個文件環境裡。
iframe 本身不代表安全這點很重要。
不能變成:
用了 iframe
=
安全
iframe 只提供了一個:
隔離的文件環境
但裡面的內容到底能做什麼,
還要看 iframe 的設定。
這時候就會看到:
sandbox
sandbox 可以先理解成「預設先把能力關掉」例如:
<iframe sandbox />
可以先把它想成:
這個 iframe 裡面的內容可以顯示,但先限制很多原本可能擁有的能力。
也就是:
先限制
↓
真的需要什麼
↓
再明確開放
這種思路跟權限很像。
不是:
全部開著
↓
再想哪些要關
而是:
預設限制
↓
最小化允許
資安裡有一個很重要的概念:
Principle of Least Privilege
最小權限原則。
可以先理解成:
只給完成這件事真正需要的能力。
例如:
Email Preview
真正需求可能只是:
顯示文字
顯示排版
顯示圖片
那就不一定需要:
執行 Script
送出 Form
控制主頁面
開新視窗
既然不需要,
就沒有理由全部開放。
sandbox 不是只有開或關iframe 的 sandbox 可以搭配不同允許條件。
例如概念上可能看到:
<iframe
sandbox="allow-same-origin"
/>
或其他:
allow-...
設定。
這代表:
我不是把所有限制全部拿掉,而是只針對某些能力開例外。
所以理解 sandbox 時,
不要只記:
有 sandbox = 安全
更重要的是:
到底開放了哪些能力?
allow-scripts 要特別小心?假設:
sandbox="allow-scripts"
代表 iframe 裡的 Script 可以執行。
如果這段 HTML 本來就是:
不完全可信任的內容
那就等於又打開了一個重要能力。
所以真正評估時要問:
這個 Preview 真的需要 JavaScript 嗎?
如果答案是:
不需要
那通常就沒有必要開。
allow-same-origin 也不是一句「圖片需要就加」這類設定的真正影響要看:
iframe 的來源
內容怎麼載入
頁面 Origin
Cookie / Storage
其他安全設定
所以我現在看到:
allow-same-origin
不會把它當成固定模板。
而會先問:
為什麼這裡需要?
資安設定如果只是:
別的地方有,所以我複製
其實很危險。
Email HTML 很常有:
<img src="...">
有些 Email 裡的圖片不是一般公開網址,
而是:
cid:
例如:
<img src="cid:logo">
這種圖片原本是在 Email MIME 結構裡,
不是普通 Web URL。
所以如果 Backend 回:
HTML
+
CID Image Data
Frontend 可能還需要把:
cid:xxx
轉成 Browser 真正能顯示的來源。
完整流程可能是:
Backend
↓
Email HTML
+
CID Images
↓
Frontend
↓
處理圖片來源
↓
iframe
↓
sandbox
↓
Preview
也就是:
顯示一封 Email Preview,本身其實跨了好幾個層次。
dangerouslySetInnerHTML?React 有:
dangerouslySetInnerHTML
它可以把 HTML String 直接塞進 DOM。
例如:
<div
dangerouslySetInnerHTML={{
__html: html,
}}
/>
名字其實已經很直接了:
dangerously
因為它會繞過 React 原本把內容當純文字處理的保護。
如果:
HTML 來源不可信
而且又沒有正確做:
Sanitization
就可能帶來 XSS 風險。
iframe sandbox 和 Sanitization 不是完全同一件事這裡也不能混在一起。
Sanitization
比較像:
把不允許的 HTML / attribute / script 移除或清理。
而:
iframe sandbox
比較像:
限制這份文件執行時能做什麼。
所以:
一個處理內容
一個限制執行環境
兩者解決的角度不同。
實際系統要不要兩個都用,
要看內容來源、需求和安全模型。
這個觀念很重要。
很容易產生一種直覺:
這是我們自己的 Backend 回來的
↓
所以一定安全
但 Backend 裡的內容可能又來自:
使用者輸入
外部 Email
第三方資料
資料庫舊資料
Template + Dynamic Content
所以真正要問的是:
這段 HTML 最原始的來源是什麼?
而不是只看:
最後是哪支 API 回來的。
前面 Debug 我們一直問:
這份資料從哪來?
到了 Security,
一樣要問。
例如:
HTML
↓
API Response
↓
Backend
↓
Database
↓
User-generated Content
如果最上游其實是不可信資料,
那中間經過:
Backend
並不會自動把它變安全。
① HTML 從哪裡來?
② 內容可以被誰修改?
③ 裡面需要 Script 嗎?
④ 需要 Form 嗎?
⑤ 需要 Navigation 嗎?
⑥ 圖片來源怎麼處理?
⑦ 是否需要 Sanitization?
⑧ iframe sandbox 開了哪些能力?
這些問題比:
iframe 有沒有成功顯示?
更完整。
前一天:
Frontend
↓
Backend
我們說:
Client 不能被當成可信任邊界。
今天:
App
↓
External / Dynamic HTML
也是一個邊界。
所以:
來源 A 的內容
↓
進入來源 B 的執行環境
就應該多想一次:
它到底被允許做什麼?
例如 Email Preview 完成時:
HTML 看得到
圖片看得到
排版正常
功能面已經成功。
但工程上還要再多一層:
這份內容會不會執行不需要的能力?
這就是:
Feature Correctness
和:
Security
的差異。
只是顯示 HTML
≠
HTML 沒有風險
以及:
iframe
≠
自動安全
真正重要的是:
內容來源
+
執行環境
+
允許能力
而:
sandbox
背後的核心不是:
加一個 attribute 就安全了。
而是:
預設限制能力,只開真正需要的部分。
這跟昨天的 Backend Authorization 其實是同一個思路:
不要因為東西「看起來只能這樣用」
就相信它一定只會這樣被用。
Frontend Button:
看不到
≠
不能呼叫 API
HTML Preview:
只是拿來看
≠
內容只能被動顯示
所以 Security 很多時候不是:
「壞人會怎麼攻擊?」
而是先從一個更基本的問題開始:
我現在到底給了這份資料多少能力?
下一篇就可以正式把前 28 天全部收進一套方法:
看到 Bug 時,我現在不再第一時間改程式碼了。