Apple: WebKit Canvas Denoising
MediumPublished Dec 26, 2024
### 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. Bec...
Affected versions
| Package | Affected | Fixed in |
|---|---|---|
| WebKit Product | all versions | No fix yet |
Details and references
### 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. ```Javascript 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 from
- GitHub (reviewed advisory)
More Google advisories
All Google| Date | Advisory | Severity | Fixed in |
|---|---|---|---|
| Jan 102025 | ENGAGE - Server Displaying Sensitive Information | Low | No fix yet |
| Jan 92025 | Integer Overflow in eBPF DEVMAP map_delete_elem Leads to Out-of-Bounds | High7.8 | See the advisory |
| Jan 92025 | Linux Kernel: Integer Overflow in eBPF XSK map_delete_elem Leads to Out-of-Bounds | High7.8 | See the advisory |
| Jan 92025 | Linux Kernel: Out of bounds Write in ksmbd_vfs_stream_write | Critical9.8 | v6.13-rc2 |
| Jan 92025 | Linux Kernel: Out of bounds Read in ksmbd_vfs_stream_read | Critical9.1 | v6.13-rc2 |
| Dec 232024 | MacPorts: Remote Code Execution | Medium | No fix yet |