Skip to content

Apple: WebKit Canvas Denoising

Moderate
rcorrea35 published GHSA-24cm-69m9-fpw3 Dec 26, 2024

Package

WebKit (Apple)

Affected versions

Safari 17.6 and 18.0

Patched versions

None

Description

Summary

When browsing in Safari’s Private Mode, WebKit adds noise to canvas readback to prevent fingerprinting. Due to the way noise is clamped for blocks of identical pixels, this can be removed by scaling the canvas.

Severity

Moderate - A script performing canvas fingerprinting can remove the noise (added in Private Mode) and recover the original image, making canvas fingerprinting possible again.

Proof of Concept

  1. Use a canvas (c1) to draw the fingerprintable shapes.
  2. Set context.imageSmoothingEnabled = false (https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/imageSmoothingEnabled). This ensures that colors are kept identical when scaling.
  3. Create a new canvas c2 with height = c1.height * 3 and width = c1.width * 3.
  4. Set the scale of c2 to be 3x3. This creates a larger canvas where each original pixel is now represented by a 3x3 block of identical pixels (smoothing is disabled).
  5. Copy c1 into c2 using context.drawImage. This scales c1 up. The drawImage operation does not introduce noise.
  6. Use context.getImageData to read the pixel data from the scaled canvas. This triggers WebKit's noise algorithm.
  7. Because the scaling process created blocks of identical pixels, the center pixel of each block will have eight neighbors with the same color. This forces the noise clamping range to a single color, resulting in no noise being applied to that pixel, which allows the original color to be recovered.
function denoise(c1) {
  const scale = 3;

  const c2 = document.createElement('canvas');
  c2.width = c1.width * scale;
  c2.height = c1.height * scale;
  const context = c2.getContext('2d');
  context.imageSmoothingEnabled = false;
  context.scale(scale, scale);
  context.drawImage(c1, 0, 0);

  const scaled = context.getImageData(0, 0, c2.width, c2.height);
  const real = new ImageData(c1.width, c1.height);
  for (let y = 0; y < c1.height; y += 1) {
    for (let x = 0; x < c1.width; x += 1) {
      const realIdx = (y * c1.width + x) * 4;
      const scaledIdx = (y * scale * c2.width + x * scale + 1) * 4;
      for (let chan = 0; chan < 4; chan += 1) {
        real.data[realIdx + chan] = scaled.data[scaledIdx + chan];
      }
    }
  }
  return real;
}

Further Analysis

WebKit applies noise to canvas elements to make fingerprinting more difficult. However, this noise can be bypassed by manipulating the noise clamping process, specifically by scaling the canvas. This allows the attacker to completely recover the original, non-noised pixel colors, bypassing the fingerprinting protection.

WebKit’s algorithm applies a noise to each color channel of every pixel. The noise is derived from a hash of the origin and the original pixel color. To prevent the noise from being too visible, WebKit "clamps" the noise. This means that after the noise is applied, the new pixel color is compared to its 8 neighboring pixels. If the new color is outside the minimum and maximum color values of its neighbors, it's adjusted to fit within that range.

The key vulnerability is that if all neighboring pixels have the same color, the clamping range will only include that single color. This makes the noise deterministic and predictable.

Timeline

Date reported: 09/25/2024
Date fixed:
Date disclosed: 12/26/2024

Severity

Moderate

CVE ID

No known CVE

Weaknesses

No CWEs

Credits