How CORS Affects WebGL Image Textures

Loading external image assets into WebGL is governed by strict Cross-Origin Resource Sharing (CORS) rules enforced by modern web browsers to prevent data exfiltration. While standard HTML elements can display cross-origin images without restriction, WebGL requires direct read access to raw pixel buffers to generate textures. This article explains why the browser blocks unauthorized cross-origin assets in WebGL, the immediate security exceptions that occur, and the exact steps required on both the client and server to properly load external textures.

When an image is loaded through a standard HTML <img> tag, the browser renders the visual content to the screen but does not allow JavaScript to inspect the underlying pixel data. WebGL, however, relies on methods like gl.texImage2D() to upload pixel bytes directly into GPU memory, where shaders can read, manipulate, and potentially transmit that data back to an unauthorized server. To prevent malicious scripts from reading sensitive user data—such as authenticated internal network pages or private images stored on other domains—browsers enforce the Same-Origin Policy via CORS.

Unlike the Canvas 2D API, which permits drawing a cross-origin image and only "taints" the canvas to block subsequent read operations (such as toDataURL() or getImageData()), WebGL fails immediately at the upload stage. If a script attempts to pass a cross-origin image to gl.texImage2D() without explicit CORS clearance, the browser halts execution and throws a SecurityError: The operation is insecure DOMException. The texture will not be uploaded, rendering the asset unusable in the WebGL context.

To successfully use an external image as a WebGL texture, two requirements must be satisfied simultaneously:

  1. Client-Side Request Configuration: The JavaScript image loader must explicitly request cross-origin authorization before setting the image source. This is done by setting the crossOrigin attribute on the HTMLImageElement:
const image = new Image();
image.crossOrigin = "anonymous";
image.src = "https://example.com/texture.png";
image.onload = () => {
    gl.bindTexture(gl.TEXTURE_2D, texture);
    gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
};
  1. Server-Side Header Authorization: The server hosting the image must include the appropriate access control headers in its HTTP response. At a minimum, it must return:
Access-Control-Allow-Origin: *

(Or specify the exact requesting domain instead of the wildcard * if credentials are required).

A common pitfall involves the browser HTTP cache. If an image is first loaded by the browser without the crossOrigin attribute (for example, via a standard <img> tag or CSS background-image), the browser might cache the asset without the required CORS headers. When WebGL subsequently requests the same URL with crossOrigin = "anonymous", the browser may serve the cached version lacking the Access-Control-Allow-Origin header, causing the WebGL upload to fail. Servers can prevent this issue by serving images with a Vary: Origin header, ensuring that requests with different origin modes are cached and evaluated independently.