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
- Use a canvas (
c1
) to draw the fingerprintable shapes.
- Set
context.imageSmoothingEnabled = false
(https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/imageSmoothingEnabled). This ensures that colors are kept identical when scaling.
- Create a new canvas
c2
with height = c1.height * 3
and width = c1.width * 3
.
- 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).
- Copy
c1
into c2
using context.drawImage
. This scales c1
up. The drawImage
operation does not introduce noise.
- Use
context.getImageData
to read the pixel data from the scaled canvas. This triggers WebKit's noise algorithm.
- 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
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
c1
) to draw the fingerprintable shapes.context.imageSmoothingEnabled = false
(https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/imageSmoothingEnabled). This ensures that colors are kept identical when scaling.c2
withheight = c1.height * 3
andwidth = c1.width * 3
.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).c1
intoc2
usingcontext.drawImage
. This scalesc1
up. ThedrawImage
operation does not introduce noise.context.getImageData
to read the pixel data from the scaled canvas. This triggers WebKit's noise algorithm.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