<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」タブでレスポンスヘッダーを確認する
- ブラウザの開発者ツールを開き、「Network(ネットワーク)」タブを選択
- 読み込んでいる画像ファイル(.pngや.jpg)をクリック
- 「Response Headers(レスポンスヘッダー)」を確認
Access-Control-Allow-Originが存在しているか、現在のドメインが許可されているかをチェック
toDataURL・toBlobのコードを直しても解決しない理由
toDataURL()でエラーが出た際、「toBlob()に書き換えたら動くのではないか」とコードを変更したくなることがあります。
しかし前述の通り、一度taintedになったCanvasでは、toBlob()、getImageData()、captureStream()など、代表的なデータ取得・キャプチャAPIでも同様にSecurityErrorが発生します。
問題の本質は「書き出しメソッドの選択」ではなく、「Canvasへ描画する前の画像読み込み段階」にあります。
書き出しコードではなく、画像リソースの読み込み方法とCORSヘッダー側を見直す必要があります。
toDataURL()とtoBlob()自体の違いについては、「canvas.toDataURL()とtoBlob()の違い」の記事で詳しく解説しています。
CanvasのSecurityErrorが出たときの確認手順
エラーが発生した際は、以下のステップで順に原因を特定してください。
- エラー発生箇所の特定:
toDataURL()やtoBlob()、getImageData()の行で発生しているか - 描画リソースの確認:Canvasへ
drawImage()した要素の中に外部URLの画像がないか - オリジンの確認:画像URLが現在のページとスキーム・ホスト・ポートまで完全一致しているか(CDNやサブドメインではないか)
- JS側の設定順序:
img.crossOrigin = 'anonymous'をimg.srcより前に記述しているか - サーバー側レスポンスの確認:DevToolsのNetworkタブで、画像に
Access-Control-Allow-Originが返っているか - 配信元の制御可否:外部サーバーの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ヘッダーを必ず確認しましょう。