最後一篇 Canvas 系列的文章 🎉 來聊聊瀏覽器針對 Canvas 的安全設計。
在實作 Canvas
時,常會載入外部圖片當素材(圖庫、S3、CDN)。顯示素材時看似正常,直到要把畫布匯出成圖片(canvas.toDataURL()
/ canvas.toBlob()
)時,突然噴出錯誤——原因就是 瀏覽器的跨域限制(CORS) 與 Tainted Canvas(污染的畫布)。
這個議題不僅僅是 API 使用細節,而是瀏覽器為了保障使用者資料隱私,所設計的一道安全防線。理解它,才能讓你的匯出功能更穩健。
只要你把 沒有通過 CORS 授權 的跨域影像(圖片、影片、另一個畫布)畫進 <canvas>
,瀏覽器會將該畫布標記為「tainted」。
之後任何像素讀取或匯出的操作(getImageData()
、toDataURL()
、toBlob()
)都會被 SecurityError 擋下來。
這是瀏覽器為了保護使用者隱私所設計的安全機制,避免惡意網站透過 <canvas>
偷取來自其他網站的圖片內容或敏感資訊。
當使用者已登入某個網站(如銀行、信箱等),該網站可能會提供需要登入權限才能顯示的圖片,例如個人頭像、帳戶頁面截圖、驗證碼等。若這些圖片被惡意網站載入並繪製到 <canvas>
,攻擊者就可能透過像素擷取手段(如 toDataURL()
)側錄畫面內容,竊取包含姓名、餘額等個資,甚至進行圖片驗證碼破解。
為了防範這類攻擊,一旦瀏覽器偵測到畫布中出現「未經授權的跨域圖片」,就會將 canvas 標記為 tainted(污染),禁止任何形式的像素提取操作,以保障用戶資料安全。
Access-Control-Allow-Origin
)。crossOrigin
,導致瀏覽器視為「無授權的跨域影像」。<img>
/ Image()
+ crossOrigin="anonymous"
const img = new Image();
// 不帶 cookie 的匿名請求(對應伺服器須回應 ACAO)
img.crossOrigin = "anonymous";
img.src = "https://cdn.example.com/assets/photo.png";
img.onload = () => {
const cvs = document.querySelector("canvas");
const ctx = cvs.getContext("2d");
ctx.drawImage(img, 0, 0);
// 如果遠端有正確 CORS,這裡就能順利匯出
cvs.toBlob((blob) => {
// 上傳、下載、預覽都可以
}, "image/png");
};
fetch
(具 CORS)、轉成 Blob
、再變 ObjectURL
async function loadImageToCanvas(url, canvas) {
const res = await fetch(url, { mode: "cors" }); // 必須允許跨域
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const blob = await res.blob();
const bitmap = await createImageBitmap(blob); // 乾淨又快
const ctx = canvas.getContext("2d");
ctx.drawImage(bitmap, 0, 0);
}
注意:不論 A 或 B,伺服器端都必須回應正確的 CORS 標頭(見下節)。
crossOrigin = "anonymous"
,後端回應:Access-Control-Allow-Origin: https://你的站點.com
Vary: Origin
*
(但請注意不要搭配憑證/ cookie)。crossOrigin = "use-credentials"
,並確保 fetch
/XHR 也有 credentials: "include"
;Access-Control-Allow-Origin: https://你的站點.com
Access-Control-Allow-Credentials: true
Vary: Origin
*
不能 搭配Allow-Credentials: true
。
一旦畫布被污染,只要你嘗試讀像素或匯出,瀏覽器就會丟出 SecurityError
。
你可以用 try/catch
快速檢測:
function isCanvasTainted(canvas) {
const ctx = canvas.getContext("2d");
try {
// 任一像素取用都會觸發 SecurityError
ctx.getImageData(0, 0, 1, 1);
return false; // 沒丟錯 = 乾淨
} catch (err) {
return true; // 丟錯 = 被污染
}
}
<img>
能顯示,Canvas 卻不能匯出?<img>
:單純顯示圖片,JS 無法直接存取像素 → 風險低,不需強制 CORS。<canvas>
:可透過 toDataURL()
、getImageData()
讀像素 → 風險高,必須滿足 CORS 條件,否則標記為 tainted。fetch/axios
:屬於主動資料存取,JS 能直接讀取回應內容 → 潛在風險最高。<img>
不會?這是瀏覽器基於「請求風險等級」所設計的差異化安全機制:
fetch / axios(主動請求型)
fetch/axios
請求時自動附加 Origin
header。Origin
,主動決定是否回傳 Access-Control-Allow-Origin(這是 Response Header,必須在後端程式中設定,瀏覽器無法控制)。<img>
/ <script>
/ <link>
(被動載入型)
Origin
header(除非顯式設 crossOrigin
)。<canvas>
並嘗試匯出,就會觸發 CORS 要求,否則畫布會被污染。請求型態 | 能否讀取內容 | 瀏覽器是否自動帶 Origin |
風險等級 |
---|---|---|---|
<img> / <script> / <link> |
❌ 不能直接讀取 | ❌ 預設不帶 | 低 |
<canvas> 匯出 (toDataURL() / getImageData() ) |
✅ 能讀取像素 | ⚠️ 需 crossOrigin + 伺服器允許 |
中 |
fetch / axios | ✅ 能讀取完整回應 | ✅ 自動帶 | 高 |
一次單純的 Canvas「匯出圖片」需求,背後其實踩著瀏覽器的安全防線。
理解 CORS 與 Tainted Canvas 的規則,你就知道什麼該在前端做、什麼要請後端協助。設定對了,你的 Canvas 就能安全順暢的匯出~
👉 歡迎追蹤這個系列,我會從 Canvas 開始,一步步帶你認識更多 Web API 🎯