ScreenshotNeo

BlogHow-to

How to Fix Safari Security Errors When Using toBlob

Safari’s toBlob SecurityError means your canvas is tainted by a cross-origin asset. Fix it with CORS, correct loading order, and reliable diagnostics.

By the ScreenshotNeo team30 September 20268 min read

How to Fix Safari Security Errors When Using toBlob

Safari throws SecurityError from canvas.toBlob() when the canvas is not origin-clean. The usual cause is an image, video frame, SVG, CSS background, or another canvas drawn from a different origin without successful CORS approval. Chrome may appear to work in a different setup, but the security rule is the same: browsers must prevent canvas pixels from becoming a cross-origin data-extraction channel.

Fix the complete request path: set crossOrigin before assigning src, configure the image server to return the correct CORS headers, draw only after load, and verify the final response after redirects. If you cannot change the remote server, use same-origin hosting or a server-side relay you control.

What the error means

HTMLCanvasElement.toBlob() asynchronously serializes the canvas bitmap into an image Blob. If any source drawn into that bitmap was fetched from another origin without CORS permission, the browser marks the canvas as tainted. Calling toBlob(), toDataURL(), or getImageData() then fails with a security exception. MDN describes this as the canvas becoming tainted as soon as cross-origin data without CORS approval is drawn into it (MDN: CORS enabled images).

This is not a Safari-specific image encoder defect. Safari is reporting that the canvas cannot be read safely. A canvas remains tainted even if you later draw same-origin pixels over it, and drawing a previously tainted canvas taints the destination too.

Minimal working fix

Set the CORS mode before starting the request. Wait for a successful load, then draw and export:

The image server must grant the page origin before canvas pixels can be exported.
The image server must grant the page origin before canvas pixels can be exported.
const image = new Image();
image.crossOrigin = "anonymous"; // Must be set before src

image.onload = () => {
  const canvas = document.querySelector("canvas");
  canvas.width = image.naturalWidth;
  canvas.height = image.naturalHeight;

  const ctx = canvas.getContext("2d");
  ctx.drawImage(image, 0, 0);

  canvas.toBlob((blob) => {
    if (!blob) {
      throw new Error("Image encoding failed");
    }

    const download = URL.createObjectURL(blob);
    const link = document.createElement("a");
    link.href = download;
    link.download = "export.png";
    link.click();
    URL.revokeObjectURL(download);
  }, "image/png");
};

image.onerror = () => {
  console.error("The image failed CORS or network checks");
};

image.src = "https://cdn.example/image.jpg";

The order matters. Assigning src first can start a request before the browser knows that the image should use CORS mode. Changing crossOrigin afterward does not repair that request; create a new Image and load it again.

Configure the image server

The browser only exposes pixels when the image response grants your page’s origin. For a page at https://app.example, a public, non-credentialed image response can include:

Access-Control-Allow-Origin: https://app.example
Vary: Origin

Use * only when the asset is genuinely public and no credentials are involved:

Access-Control-Allow-Origin: *

If the server varies the allowed origin, add Vary: Origin so a CDN does not reuse one origin’s response for another. The header must be present on the final image response, including a CDN or redirect target.

Credentialed images

Cookies, HTTP authentication, or another credentialed request changes the rules. Use an explicit origin and allow credentials:

Access-Control-Allow-Origin: https://app.example
Access-Control-Allow-Credentials: true
Vary: Origin

A wildcard Access-Control-Allow-Origin: * is rejected for credentialed access. In JavaScript, request credentials explicitly when required:

const image = new Image();
image.crossOrigin = "use-credentials";
image.onload = () => {
  // draw and export here
};
image.onerror = () => console.error("Credentialed CORS request failed");
image.src = "https://private.example/photo.jpg";

Do not use use-credentials unless the remote server is configured for it. For most public CDN images, anonymous is the right choice.

Complete example with error handling

This example validates the input, waits for loading, handles unsupported output types, and reports a useful failure:

async function imageToBlob(url, type = "image/png", quality) {
  const image = new Image();
  image.crossOrigin = "anonymous";

  await new Promise((resolve, reject) => {
    image.onload = resolve;
    image.onerror = () => reject(new Error(
      `Image could not be loaded with CORS: ${url}`
    ));
    image.src = url;
  });

  const canvas = document.createElement("canvas");
  canvas.width = image.naturalWidth;
  canvas.height = image.naturalHeight;

  const context = canvas.getContext("2d", { willReadFrequently: false });
  if (!context) throw new Error("2D canvas is unavailable");
  context.drawImage(image, 0, 0);

  return await new Promise((resolve, reject) => {
    canvas.toBlob((blob) => {
      if (blob) resolve(blob);
      else reject(new Error("Browser could not encode the canvas"));
    }, type, quality);
  });
}

imageToBlob("https://cdn.example/image.jpg", "image/webp", 0.85)
  .then((blob) => console.log(blob.type, blob.size))
  .catch((error) => console.error(error));

toBlob() is asynchronous and passes the result to its callback. If the requested MIME type is unsupported, the browser can fall back to PNG; that behavior is separate from a tainted-canvas SecurityError. Check blob.type rather than assuming the requested format.

Sources that commonly taint a canvas

  • Images: JPEG, PNG, WebP, AVIF, and animated formats loaded from another origin.
  • Video: drawing a cross-origin video frame has the same origin-clean requirement.
  • SVG: an SVG can contain nested images, stylesheets, or filters fetched from other origins. The outer URL appearing allowed does not guarantee every nested resource is allowed.
  • CSS backgrounds: libraries that render DOM or computed styles onto canvas may fetch background images independently.
  • Another canvas: drawing a canvas already tainted transfers the taint.
  • Redirects: the first URL may look correct while the final CDN response omits CORS.

Audit every resource that reaches drawImage(), including assets introduced by a rendering library rather than by your own code.

Safari diagnostic checklist

  1. Open Safari Web Inspector and reproduce the error.
  2. In Console, read the CORS message. Script often receives only a generic failure, while the console identifies the blocked origin.
  3. In Network, open the final image request, including any redirect chain.
  4. Confirm the request uses CORS mode and the response contains Access-Control-Allow-Origin matching the page origin.
  5. For credentials, confirm Access-Control-Allow-Credentials: true and an explicit origin.
  6. Check that a CDN or proxy did not cache a response for the wrong origin; add Vary: Origin where appropriate.
  7. Test from an HTTP(S) development origin. A file:// page, sandboxed iframe, or opaque origin can create misleading CORS behavior.

Common errors and fixes

Error or symptom Cause Fix
SecurityError from toBlob() A cross-origin source was drawn without successful CORS. Set crossOrigin before src and add the matching response header.
Works without crossOrigin but export fails The image can be displayed, but display permission is not pixel-read permission. Use CORS mode and configure the server.
Setting crossOrigin has no effect src was assigned first, so the request already started. Create a new image, set the property first, then assign src.
Credentialed request rejected Wildcard origin was used with credentials, or credentials headers are missing. Return the exact origin plus Access-Control-Allow-Credentials: true.
Redirected image fails The final response lacks CORS even though the initial URL had it. Inspect and fix headers at the final URL and every relevant CDN layer.
toBlob() callback receives null Encoding failed or the requested MIME type is unsupported. Handle null; retry with image/png and check available canvas support.
Only some pages fail One nested SVG, background, video, or third-party image is not CORS-enabled. Find the individual request in Network tools and remove, proxy, or configure it.
Local testing behaves differently file://, sandbox, or opaque-origin rules differ from production. Serve the app from a local HTTP(S) server with a stable origin.

When you cannot change the remote server

There is no client-side JavaScript switch that safely bypasses CORS. Browser extensions and disabled security flags are development-only experiments and do not solve a production deployment.

Choose one of these architectures:

  1. Host the asset yourself: copy or upload the file to a domain you control and serve it with the right headers.
  2. Use a same-origin server relay: your backend fetches the remote asset, validates the target and content type, and returns it from your own origin with controlled caching and CORS.
  3. Render on the server: if your actual goal is a screenshot or document, capture the page outside the browser and return the resulting file.

A relay should impose URL allowlists, size limits, timeouts, and response validation. Otherwise it can become an open proxy or consume excessive memory while processing large images.

Performance and reliability considerations

  • Load once: reuse a successfully loaded image when exporting multiple sizes or formats.
  • Set canvas dimensions before drawing: changing width or height clears the bitmap and can remove previous work.
  • Limit pixels: very large canvases increase memory and encoding time, especially on mobile Safari. Resize before drawing when the output does not require native resolution.
  • Release object URLs: call URL.revokeObjectURL() after downloads or previews to avoid retaining blobs.
  • Use explicit timeouts around fetches: a stalled remote image can leave your UI waiting even though the eventual failure is a network issue, not a canvas issue.
  • Keep CORS stable through caches: use Vary: Origin for origin-dependent responses and purge incorrect cached variants.
  • Expect third-party change: an image provider can change headers, redirects, cookies, or CDN behavior without changing your code. Monitor representative exports.

Or skip the browser setup

If your goal is a page screenshot rather than client-side pixel manipulation, ScreenshotNeo captures the page on the server and returns PNG, JPEG, WebP, or PDF. Cookie and consent banners are accepted and removed before capture, along with 60+ known consent platforms, newsletter popups, and chat widgets. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed, and response headers identify the page verdict and billing status. Its MCP server provides take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients.

Server-side capture can remove common overlays before producing the screenshot.
Server-side capture can remove common overlays before producing the screenshot.

See the full option list and parameter reference in the ScreenshotNeo documentation. A one-call request looks like this:

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

You can still control full-page capture, lazy images, CSS selectors, dark mode, device and retina settings, custom CSS and JavaScript, waits, blocked resources, headers, cookies, user agents, timezone, geolocation, transparent backgrounds, resizing, caching TTL, signed links, asynchronous webhooks, bulk capture, and PDF settings. The free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000.

Create a free ScreenshotNeo account and get 1,000 screenshots each month without adding a card.

FAQ

Does Safari require a different CORS header than Chrome?

No. Safari may expose different diagnostics or timing, but the origin-clean requirement applies across browsers.

Can I call toBlob() before the image load event?

Wait for load. Before that event, dimensions and pixels may be incomplete, and a failed request cannot be distinguished from a successful image.

Will converting the image to a data URL in JavaScript bypass CORS?

No. Reading the remote response to create a data URL is itself subject to CORS. Conversion must happen on a server or after a permitted CORS load.

Why does drawing a transparent PNG still taint the canvas?

Transparency and origin permission are separate. A transparent image still contains pixel data and needs CORS approval.

Is toDataURL() safer than toBlob()?

No. Both are blocked when the canvas is tainted. toBlob() is generally preferable for large exports because it avoids putting the entire encoded file in a JavaScript string.