<canvas>を使って画像に文字を合成したり、フレームを付けたりして「さあ保存しよう」とcanvas.toDataURL()を実行した瞬間、以下のようなエラーに直面したことはないでしょうか。

Uncaught DOMException: Failed to execute 'toDataURL' on 'HTMLCanvasElement': Tainted canvases may not be exported.

SecurityError: The operation is insecure.

画像を扱うケースでこのエラーが出る代表的な原因は、Canvasのメソッド呼び出しミスではなく、CORS(Cross-Origin Resource Sharing)の許可がない別オリジンの画像をCanvasに描画したことで、Canvasが「汚染(tainted)」された状態になっていることです。

この記事では、画像は画面に表示できているのに書き出しだけ失敗する理由から、crossOrigin属性とサーバー側ヘッダーの役割、複数画像の切り分け手順、外部画像を安全に扱うための設計までを解説します。

canvas.toDataURL()でSecurityErrorになる原因

結論から言うと、外部画像を扱っている場合の代表的な原因は「Canvasへ描画した画像の中に、CORSの許可を得ていない別オリジンの画像が混ざっているため」です。

const canvas = document.querySelector('#canvas');
const ctx = canvas.getContext('2d');

const img = new Image();
img.src = 'https://example.com/external-image.jpg'; // 別オリジンの画像

img.onload = () => {
  ctx.drawImage(img, 0, 0); // ここまではエラーなく画面に描画される

  // ここで SecurityError が発生する
  const dataURL = canvas.toDataURL('image/png');
};

HTML仕様において、CORSの承認なしで読み込まれたクロスオリジン画像が1枚でもCanvasに描画されると、そのCanvasは「origin-clean(オリジンが清潔な状態)」ではなくなります。

このようにorigin-cleanではなくなったCanvasは、一般に「tainted canvas(汚染されたCanvas)」と呼ばれます。

tainted canvasとは?

Canvas要素は、初期状態では「origin-clean」です。

【Canvasの状態変化】
[初期状態のCanvas] (origin-clean: true)
       ↓
自前の図形やテキスト、同一オリジンの画像を描画 → 問題なし
       ↓
CORS許可のない外部画像を drawImage() 
       ↓
[tainted canvas(汚染状態)] (origin-clean: false)
       ↓
toDataURL() / toBlob() を呼ぶとブラウザが SecurityError を投げる

一度taintedになったCanvasは、同一オリジンの画像を上から描いたり、clearRect()で消したりするだけではorigin-cleanに戻りません。

なお、Canvasのwidthまたはheight属性を変更してbitmap自体をリセットするとorigin-cleanに戻せますが、描画内容やコンテキストの状態もすべてリセットされます。

通常は、原因となる外部画像の読み込み方法を直したうえでCanvasを描画し直します。

なぜ画像は表示できるのにCanvasから保存できないのか

検索者が最も疑問に感じるのが、「<img src="...">やctx.drawImage()では画面上に正常に見えているのに、なぜ保存(データ取得)だけ禁止されるのか」という点です。

ブラウザはセキュリティ上、以下の2つの操作を明確に区別しています。

  • 画面にレンダリング(描画)すること:許可(閲覧するだけならユーザー情報漏洩の危険が低いため)
  • ピクセルデータをJavaScript側へ読み戻すこと:厳格に制限(同一生成元ポリシー/ CORSの対象)

もし別オリジンの画像をCanvasに描画して自由にピクセルデータを取得できると、ログイン中のユーザーしか見られない非公開画像や別ドメインの機密データを、悪意あるスクリプトがCanvas経由でBase64化して盗み出す(データ抽出する)ことが可能になってしまいます。

MDNでも説明されている通り、ブラウザはこの情報流出を防ぐため、「CORS許可のない外部画像を取り込んだCanvasからは、ピクセルデータを一切外部へ出力させない」という保護措置(tainted化)を行っています。

tainted canvasになると使えなくなる処理

tainted canvasになると、toDataURL()だけでなく、Canvasの内容を読み出したりキャプチャしたりする代表的なAPIがSecurityErrorの対象になります。

メソッド 主な用途 tainted時の挙動
canvas.toDataURL() Data URL(Base64)文字列の取得 SecurityError
canvas.toBlob() 画像Blobの取得(ダウンロード・送信) SecurityError
ctx.getImageData() ピクセル単位の生データ配列の取得 SecurityError
canvas.captureStream() Canvas描画のメディアストリーム取得 SecurityError

toDataURL()をtoBlob()に書き換えても解決しないのは、どちらも「Canvas内部のデータを外へ取り出す処理」であるためです。

外部画像をCanvasで安全に使う方法

外部サーバーの画像をCanvasへ取り込み、かつ安全に画像書き出しを行うには、フロントエンドとサーバー側の両方でCORSの設定が必要です。

1. JavaScriptではcrossOriginをsrcより先に指定する

JavaScriptでImageオブジェクトを生成する場合、必ずsrcを代入する前にcrossOriginプロパティを設定します。

const img = new Image();

// 1. 必ず src を設定する前に crossOrigin を指定する
img.crossOrigin = 'anonymous';

// 2. その後に画像URLを代入する
img.src = 'https://example.com/image.jpg';

img.onload = () => {
  ctx.drawImage(img, 0, 0);

  // CORSが正常に許可されていれば toDataURL() が成功する
  const dataURL = canvas.toDataURL('image/png');
};

img.crossOrigin = 'anonymous'を指定することで、ブラウザはその画像を「CORSモード」でリクエスト(Originヘッダーを付与してリクエスト)します。

HTMLのimg要素を使う場合

HTML上に配置した画像をdrawImage()で取り込む場合は、crossorigin属性を指定します。

<img
  id="source-image"
  src="https://example.com/image.jpg"
  crossorigin="anonymous"
  alt="外部画像"
>

crossOrigin="anonymous"だけでは直らない

初心者が最もつまずきやすいのが、「フロント側にimg.crossOrigin = 'anonymous'を書いたのにエラーが消えない」というケースです。

crossOriginの指定は、あくまで「ブラウザからサーバーへCORSモードで問い合わせる」ための宣言に過ぎません。

実際にアクセスを許可するかどうかは、画像配信元のサーバーが判断します。

画像配信サーバー側のHTTPレスポンスヘッダーに、適切なAccess-Control-Allow-Originが含まれていなければCORSは成立しません。

Access-Control-Allow-Origin: https://your-site.example.com

不特定多数のサイトから利用されることを想定した公開画像では、Access-Control-Allow-Origin: *が使われることもあります。

一方、利用元を限定したい場合は、許可するオリジンを明示します。

crossOriginを指定したら逆に画像が表示されなくなった理由

元々は画面に表示されていた画像に対し、img.crossOrigin = 'anonymous'を追加した途端、onerrorが発火して画像自体が表示されなくなることがあります。

【crossOrigin = 'anonymous' (CORSモード)】
  CORSリクエスト ──→ 画像サーバー
                         ↓
           Access-Control-Allow-Origin なし
                         ↓
  ブラウザがレスポンスの利用を許可しない
                         ↓
  画像ロード失敗 / onerror

これはブラウザがCORS仕様を正しく適用した結果です。

画像配信元がCORSを許可していない場合、CORSモードで取得してもレスポンスを画像として利用できず、画像の読み込みが失敗します。

外部画像のサーバーを変更できない場合はどうする?

SNSのアイコンや他社サイトの画像など、画像配信元サーバーのヘッダー(Access-Control-Allow-Origin)を自分で変更できない場合、ブラウザ側のJavaScriptだけでこのCORS制約を解除する方法はありません。

「JavaScriptのハックでtaintedを無理やり解除する裏技」は存在しないため、以下のようなアーキテクチャ・設計の見直しが必要になります。

  • CORSを公式に許可している画像配信元・APIを利用する
  • 画像を自分の管理下にあるサーバーやCDNへ移行・配置する
  • 利用規約・権利関係を確認した上で、自社サーバー側のプロキシ(BFF: Backend for Frontend)を経由して画像を取得・配信する

※自社サーバーを経由して画像を取得・配信する場合は、ユーザーから受け取った任意URLをそのまま取得する仕組みにはせず、取得先を許可リストで制限するなど、オープンプロキシやSSRF(Server-Side Request Forgery)にならない安全な設計が必要です。

同一オリジンならSecurityErrorは起きない?

同じオリジンから配信されている画像であれば、CORSリクエストを行わなくてもCanvasがtaintedになることはありません。

ただし、「オリジン(Origin)」の定義に注意が必要です。

https://example.com/     ← これを現在のページとすると...

https://example.com/img.png       → 同一オリジン(問題なし)
http://example.com/img.png        → 別オリジン(プロトコル/スキームが異なる)
https://cdn.example.com/img.png   → 別オリジン(サブドメイン/ホストが異なる)
https://example.com:8080/img.png  → 別オリジン(ポート番号が異なる)

「自社のサイトだから同一オリジンだと思っていたら、画像だけ別サブドメインのCDN(cdn.example.com)やS3互換ストレージから配信していた」というケースは非常によくあります。

サブドメインが異なる場合も別オリジン扱いになるため、CDN側でCORSヘッダーの設定が必要です。

どの画像がCanvasをtaintedにしたのか調べる方法

Canvas上に複数の画像を合成している場合、どの1枚が原因で汚染されたのかを特定する手順です。

ctx.drawImage(backgroundImage, 0, 0);
ctx.drawImage(userAvatar, 50, 50);
ctx.drawImage(stampIcon, 100, 100);

1.描画処理を1枚ずつコメントアウトして特定する

画像を1枚ずつ描画から外し、どの画像を足した瞬間にtoDataURL()がエラーになるかを切り分けます。

2. DevToolsの「Network」タブでレスポンスヘッダーを確認する

  1. ブラウザの開発者ツールを開き、「Network(ネットワーク)」タブを選択
  2. 読み込んでいる画像ファイル(.pngや.jpg)をクリック
  3. 「Response Headers(レスポンスヘッダー)」を確認
  4. Access-Control-Allow-Originが存在しているか、現在のドメインが許可されているかをチェック

toDataURL・toBlobのコードを直しても解決しない理由

toDataURL()でエラーが出た際、「toBlob()に書き換えたら動くのではないか」とコードを変更したくなることがあります。

しかし前述の通り、一度taintedになったCanvasでは、toBlob()、getImageData()、captureStream()など、代表的なデータ取得・キャプチャAPIでも同様にSecurityErrorが発生します。

問題の本質は「書き出しメソッドの選択」ではなく、「Canvasへ描画する前の画像読み込み段階」にあります。

書き出しコードではなく、画像リソースの読み込み方法とCORSヘッダー側を見直す必要があります。

toDataURL()とtoBlob()自体の違いについては、「canvas.toDataURL()とtoBlob()の違い」の記事で詳しく解説しています。

CanvasのSecurityErrorが出たときの確認手順

エラーが発生した際は、以下のステップで順に原因を特定してください。

  1. エラー発生箇所の特定:toDataURL()やtoBlob()、getImageData()の行で発生しているか
  2. 描画リソースの確認:CanvasへdrawImage()した要素の中に外部URLの画像がないか
  3. オリジンの確認:画像URLが現在のページとスキーム・ホスト・ポートまで完全一致しているか(CDNやサブドメインではないか)
  4. JS側の設定順序:img.crossOrigin = 'anonymous'をimg.srcより前に記述しているか
  5. サーバー側レスポンスの確認:DevToolsのNetworkタブで、画像にAccess-Control-Allow-Originが返っているか
  6. 配信元の制御可否:外部サーバーのCORSを変更できない場合、自社サーバーへの画像配置やプロキシ中継を検討する

まとめ

canvas.toDataURL()で発生するSecurityErrorは、書き出しコード自体の不備ではなく、ブラウザの同一生成元ポリシーに基づくCanvas汚染(tainted)が原因です。

  • エラーの仕組み:CORS許可のない別オリジン画像を取り込むとCanvasがtaintedになり、toDataURL()やtoBlob()、getImageData()などのデータ取得系APIが制限される
  • フロント側の対応:img.srcを設定する前にimg.crossOrigin = 'anonymous'を指定する
  • サーバー側の対応:画像配信サーバーから適切なAccess-Control-Allow-Originヘッダーを返す必要がある(両方が揃って初めて解決する)
  • 設計の判断:CORSを変更できない外部画像は、自社サーバー・CDNへの配置や中継などの構成見直しが必要

「画面に表示できているから大丈夫」と過信せず、Canvasでピクセルを書き出す際は画像リソースのオリジンとCORSヘッダーを必ず確認しましょう。