iT邦幫忙

2025 iThome 鐵人賽

DAY 8
3
Modern Web

從 Canvas 到各式各樣的 Web API 之旅系列 第 8

Day 8 - Canvas 跨域安全:理解並避免 Tainted Canvas 問題

  • 分享至 

  • xImage
  •  

最後一篇 Canvas 系列的文章 🎉 來聊聊瀏覽器針對 Canvas 的安全設計。

在實作 Canvas 時,常會載入外部圖片當素材(圖庫、S3、CDN)。顯示素材時看似正常,直到要把畫布匯出成圖片canvas.toDataURL() / canvas.toBlob())時,突然噴出錯誤——原因就是 瀏覽器的跨域限制(CORS)Tainted Canvas(污染的畫布)

這個議題不僅僅是 API 使用細節,而是瀏覽器為了保障使用者資料隱私,所設計的一道安全防線。理解它,才能讓你的匯出功能更穩健。


什麼是「被污染(Tainted)」的 Canvas?

只要你把 沒有通過 CORS 授權 的跨域影像(圖片、影片、另一個畫布)畫進 <canvas>,瀏覽器會將該畫布標記為「tainted」。
之後任何像素讀取或匯出的操作(getImageData()toDataURL()toBlob())都會被 SecurityError 擋下來。

TaintedCanvas

為何瀏覽器這樣設計?

這是瀏覽器為了保護使用者隱私所設計的安全機制,避免惡意網站透過 <canvas> 偷取來自其他網站的圖片內容或敏感資訊。

當使用者已登入某個網站(如銀行、信箱等),該網站可能會提供需要登入權限才能顯示的圖片,例如個人頭像、帳戶頁面截圖、驗證碼等。若這些圖片被惡意網站載入並繪製到 <canvas>攻擊者就可能透過像素擷取手段(如 toDataURL())側錄畫面內容,竊取包含姓名、餘額等個資,甚至進行圖片驗證碼破解。

為了防範這類攻擊,一旦瀏覽器偵測到畫布中出現「未經授權的跨域圖片」,就會將 canvas 標記為 tainted(污染),禁止任何形式的像素提取操作,以保障用戶資料安全。


什麼情況會被污染?

  • 圖片 不同來源(origin),且伺服器未回應正確的 CORS 標頭(例如 Access-Control-Allow-Origin)。
  • 前端載圖沒設定 crossOrigin,導致瀏覽器視為「無授權的跨域影像」。
  • 你畫上去的不是圖片也一樣:video其他 canvas 只要源頭未通過 CORS,也會污染。

前端 - 正確載入授權的跨域圖片

做法 A:<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");
};

做法 B:先 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 標頭(見下節)。


後端(圖片伺服器) - 設定 CORS

最常見兩種情境

  1. 匿名跨域(建議)
    前端 crossOrigin = "anonymous",後端回應:
Access-Control-Allow-Origin: https://你的站點.com
Vary: Origin
  • 若資源公開,也可用 *(但請注意不要搭配憑證/ cookie)。
  1. 需要憑證(較少用)
    前端 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 能直接讀取回應內容 → 潛在風險最高。

為什麼 fetch/axios 自動帶 Origin,但 <img> 不會?

這是瀏覽器基於「請求風險等級」所設計的差異化安全機制:

  • fetch / axios(主動請求型)

    • 為防止跨站偷資料,瀏覽器會在發送跨域的 fetch/axios 請求時自動附加 Origin header。
    • 伺服器可以根據這個 Origin,主動決定是否回傳 Access-Control-Allow-Origin(這是 Response Header,必須在後端程式中設定,瀏覽器無法控制)。
    • 伺服器若未回傳正確的 Access-Control-Allow-Origin,瀏覽器雖然會收到 Response,但會阻止 JavaScript 存取內容。
  • <img> / <script> / <link>(被動載入型)

    • 僅用於載入與顯示,JS 無法直接存取內容。
    • 瀏覽器不視其為高風險,預設不附加 Origin header(除非顯式設 crossOrigin)。
    • 但若把圖片畫進 <canvas> 並嘗試匯出,就會觸發 CORS 要求,否則畫布會被污染。

風險等級表

請求型態 能否讀取內容 瀏覽器是否自動帶 Origin 風險等級
<img> / <script> / <link> ❌ 不能直接讀取 ❌ 預設不帶
<canvas> 匯出 (toDataURL() / getImageData()) ✅ 能讀取像素 ⚠️ 需 crossOrigin + 伺服器允許
fetch / axios ✅ 能讀取完整回應 ✅ 自動帶

小結

一次單純的 Canvas「匯出圖片」需求,背後其實踩著瀏覽器的安全防線。
理解 CORSTainted Canvas 的規則,你就知道什麼該在前端做、什麼要請後端協助。設定對了,你的 Canvas 就能安全順暢的匯出~


👉 歡迎追蹤這個系列,我會從 Canvas 開始,一步步帶你認識更多 Web API 🎯


上一篇
Day 7 - Canvas 生態圈:Fabric.js 如何幫你簡化複雜繪圖
系列文
從 Canvas 到各式各樣的 Web API 之旅8
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言