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 SecurityErrorctx.getImageData()throws SecurityError- Works with same-origin images but not CDN images
Real Error Message
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:
Access-Control-Allow-Origin: https://your-domain.comFor AWS S3:
{
"CORSRules": [
{
"AllowedOrigins": ["https://your-domain.com"],
"AllowedMethods": ["GET"],
"AllowedHeaders": ["*"],
"MaxAgeSeconds": 3600
}
]
}For 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 ```