The Problem

When you draw a cross-origin image onto an HTML canvas, the canvas becomes "tainted." Once tainted, you cannot call toDataURL(), toBlob(), or getImageData() -- the browser throws a security error to prevent data leakage.

Symptoms

  • "Tainted canvases may not be exported" error
  • canvas.toDataURL() throws SecurityError
  • ctx.getImageData() throws SecurityError
  • Works with same-origin images but not CDN images

Real Error Message

bash
SecurityError: Failed to execute 'toDataURL' on 'HTMLCanvasElement':
Tainted canvases may not be exported.
    at exportCanvas()

Real Error Scenario

```javascript const canvas = document.getElementById('editor'); const ctx = canvas.getContext('2d'); const img = new Image(); img.src = 'https://cdn.example.com/photo.jpg';

img.onload = () => { ctx.drawImage(img, 0, 0); const dataUrl = canvas.toDataURL(); // THROWS SecurityError! }; ```

How to Fix It

Fix 1: Set crossOrigin Attribute on Image

```javascript const img = new Image(); img.crossOrigin = 'anonymous'; // MUST be set BEFORE src img.src = 'https://cdn.example.com/photo.jpg';

img.onload = () => { ctx.drawImage(img, 0, 0); const dataUrl = canvas.toDataURL(); // Now works! }; ```

Fix 2: Configure CDN CORS Headers

The server hosting the image MUST return the proper CORS header:

bash
Access-Control-Allow-Origin: https://your-domain.com

For AWS S3:

json
{
  "CORSRules": [
    {
      "AllowedOrigins": ["https://your-domain.com"],
      "AllowedMethods": ["GET"],
      "AllowedHeaders": ["*"],
      "MaxAgeSeconds": 3600
    }
  ]
}

For Nginx:

nginx
location ~* \.(jpg|jpeg|png|gif|webp)$ {
  add_header Access-Control-Allow-Origin "https://your-domain.com";
}

Fix 3: Proxy Images Through Your Server

```javascript // Server-side proxy app.get('/api/image-proxy', async (req, res) => { const url = req.query.url; const response = await fetch(url); const buffer = await response.arrayBuffer(); res.setHeader('Content-Type', response.headers.get('content-type')); res.send(Buffer.from(buffer)); });

// Client img.src = /api/image-proxy?url=${encodeURIComponent(imageUrl)}; ```

Fix 4: Use a Blob URL

```javascript const response = await fetch('https://cdn.example.com/photo.jpg'); const blob = await response.blob(); const blobUrl = URL.createObjectURL(blob);

const img = new Image(); img.src = blobUrl; // Same-origin blob URL, no taint

img.onload = () => { ctx.drawImage(img, 0, 0); const dataUrl = canvas.toDataURL(); // Works! URL.revokeObjectURL(blobUrl); }; ```

Fix 5: Debug CORS on Image Server

```bash curl -I https://cdn.example.com/photo.jpg \ -H "Origin: https://your-domain.com"

# Should return: # Access-Control-Allow-Origin: https://your-domain.com ```