Closed Bug 1621433 Opened 2 years ago Closed 2 years ago

In RFP mode, turn the all-white canvas into a fully random 'poison pill'

Categories

(Core :: DOM: Security, enhancement, P2)

enhancement

Tracking

()

RESOLVED FIXED
mozilla78
Tracking Status
firefox78 --- fixed

People

(Reporter: tjr, Assigned: tjr)

References

(Blocks 1 open bug, Regressed 1 open bug)

Details

(Whiteboard: [fingerprinting][domsecurity-active])

Attachments

(2 files)

Brave has debuted this interesting idea where they randomize WebGL, AudioContext, and Canvas not as a comprehensive fingerprinting solution, but instead as a 'poison pill' against naive tracking scripts that don't realize they should be smarter. Brave slightly randomizes the result (making it imperceptibly different but with the overall correct return result) meaning that a script that incorporates this 'poison pill' into its fingerprint will never match the user again and the fingerprint is useless.

(Because they are returning the real-ish value, they need to use a per-execution per-origin seed to avoid repeat calls that average out the random value.)

The notion of a poison pill is interesting in the context of Tor Browser because while Tor Browser (which can be thought of as RFP+enhancements Firefox lacks) tries to present a uniform fingerprint, it cannot do so entirely. Consider browser viewport size. Letterboxing rounds this to some extent, but imagine three users: Alice, Bob and Carol where Alice and Bob have the same browser size and Carol doesn't. Alice and Bob may have fingerprint X and Carol fingerprint Y.

Using the same poison pill idea, Alice, Bob, and Carol would present random fingerprints to a naive tracker and the same X/Y fingerprints to a smart tracker. (That is to say, Tor Browser is strictly no worse off by using the poison pill, and would be better against naive trackers.)

Because we are proposing returning a fully random canvas value each time, we don't need to worry about the adversary averaging out the randomness, so we can avoid all that per-execution, per-origin seed.

A downside of this is presently if a website shows you the canvas data it read, it's a white square. Now it would be.... random. Maybe not even parsable as an image. We'll have to test that...

Slightly OT: randomizing is great tool, depending on the metric being measured: i.e where it makes sense (no need to add complexity if not needed)

Not only would it render (pun intended) a lot of fingerprinting scripts useless until they became smarter, it nullifies those metrics when they do become smarter. I believe this is the only solution for the following (with very little breakage: see how CanvasBlocker handles these, except measureText which isn't implemented yet)

This is an interesting idea. I wonder if it would be sufficient to generate one random byte and fill the entire canvas with it. That would always result in a grayscale image (I think) and it would be faster.

(In reply to Mark Smith [:mcs] from comment #3)

This is an interesting idea. I wonder if it would be sufficient to generate one random byte and fill the entire canvas with it. That would always result in a grayscale image (I think) and it would be faster.

I don't think so. That would mean that every user has 256 fingerprints instead of 1. But we could make it a max of 16 random bytes (128 bits).....

Tom, since you uploaded a patch I assume you are actively working on this bug, if not, please feel free to move to the backlog.

Assignee: nobody → tom
Status: NEW → ASSIGNED
Priority: -- → P2
Whiteboard: [fingerprinting] → [fingerprinting][domsecurity-active]

Not needed. It is always better to produce deterministic result.

Because we are proposing returning a fully random canvas value each time, we don't need to worry about the adversary averaging out the randomness, so we can avoid all that per-execution, per-origin seed.

No, it would result in some (>1, given that ones having rfp are rare) bits of information for them and some bits of info for all other users.

It is always better to produce deterministic result.

I mean for 2d-canvas that are used it is completely possible to use a pure software renderer as was suggested by Shacham and Mowery (inventors of canvas fingerprinting) quite long ago in their paper.

In RFP mode, canvas image extraction leads to an all-white image, replace that
with a random (sample 32 bytes of randomness and fill the buffer with that)
'poison pill'. This helps defeat naive fingerprinters by producing a random
image on every try.

Updated browser_canvas_fingerprinting_resistance.js to test whether the canvas
data is equal to the placed data (instead of testing if the canvas data was
equal to the placeholder data.)

Testing: ./mach test permissions/ and ./mach test gfx/ seem to pass.

Updates and replaces D66308.

Attachment #9143735 - Attachment description: Bug 1621433 - In RFP mode, turn canvas image extraction into a fully-random 'poison pill' for fingerprinters r?tjr → Bug 1621433 - In RFP mode, turn canvas image extraction into a random 'poison pill' for fingerprinters r?tjr

(In reply to KOLANICH from comment #7)

It is always better to produce deterministic result.

I mean for 2d-canvas that are used it is completely possible to use a pure software renderer as was suggested by Shacham and Mowery (inventors of canvas fingerprinting) quite long ago in their paper.

+1, this is the full solution. I don't think fuzzing outputs is wholly protective, particularly for WebGL. Using software renderers here allows us to fully implement the web specs and retain functionality at merely reduced performance, without still allowing tricky-but-established approaches to side-channeling data exfil.

Pushed by tritter@mozilla.com:
https://hg.mozilla.org/integration/autoland/rev/8a48a3a488ab
In RFP mode, turn canvas image extraction into a random 'poison pill' for fingerprinters r=tjr,jrmuizel

Backed out for hazard failures on CanvasRenderingContext2D.cpp

backout: https://hg.mozilla.org/integration/autoland/rev/5b3b92ac7ed09187d6cbfee0418a8b4927c4c728

push: https://treeherder.mozilla.org/#/jobs?repo=autoland&revision=8a48a3a488ab9f48ae4feaba11f0e5dab4cf00b9&searchStr=linux%2Cx64%2Cdebug%2Chazard-linux64-haz%2Fdebug%2C%28h%29&selectedTaskRun=XQiNV6j2RQ6O1KKxcayaHQ-1

failure log: https://treeherder.mozilla.org/logviewer.html#/jobs?job_id=301369664&repo=autoland&lineNumber=76824

[task 2020-05-08T06:04:38.560Z] Found 1 hazards 198 unsafe references 0 missing
[task 2020-05-08T06:04:38.561Z] Running heapwrites to generate heapWriteHazards.txt
[task 2020-05-08T06:04:38.561Z] PATH="/builds/worker/fetches/sixgill/usr/bin:${PATH}" SOURCE='/builds/worker/checkouts/gecko' LD_LIBRARY_PATH="${LD_LIBRARY_PATH}:/builds/worker/workspace/obj-haz-shell/dist/bin" ANALYZED_OBJDIR='/builds/worker/workspace/obj-analyzed' XDB='/builds/worker/fetches/sixgill/usr/bin/xdb.so' /builds/worker/workspace/obj-haz-shell/dist/bin/js /builds/worker/checkouts/gecko/js/src/devtools/rootAnalysis/analyzeHeapWrites.js > heapWriteHazards.txt
[task 2020-05-08T06:05:13.489Z] + check_hazards /builds/worker/workspace/analysis
[task 2020-05-08T06:05:13.490Z] + set +e
[task 2020-05-08T06:05:13.490Z] ++ grep -c 'Function.*has unrooted.*live across GC call' /builds/worker/workspace/analysis/rootingHazards.txt
[task 2020-05-08T06:05:13.491Z] + NUM_HAZARDS=1
[task 2020-05-08T06:05:13.492Z] ++ grep -c '^Function.takes unsafe address of unrooted' /builds/worker/workspace/analysis/refs.txt
[task 2020-05-08T06:05:13.493Z] + NUM_UNSAFE=198
[task 2020-05-08T06:05:13.493Z] ++ grep -c '^Function.
has unnecessary root' /builds/worker/workspace/analysis/unnecessary.txt
[task 2020-05-08T06:05:13.495Z] + NUM_UNNECESSARY=1297
[task 2020-05-08T06:05:13.496Z] ++ grep -c '^Dropped CFG' /builds/worker/workspace/analysis/build_xgill.log
[task 2020-05-08T06:05:13.557Z] + NUM_DROPPED=0
[task 2020-05-08T06:05:13.557Z] ++ perl -lne 'print $1 if m!found (\d+)/\d+ allowed errors!' /builds/worker/workspace/analysis/heapWriteHazards.txt
[task 2020-05-08T06:05:13.558Z] + NUM_WRITE_HAZARDS=0
[task 2020-05-08T06:05:13.559Z] ++ grep -c '^Function.*expected hazard.*but none were found' /builds/worker/workspace/analysis/rootingHazards.txt
[task 2020-05-08T06:05:13.560Z] + NUM_MISSING=0
[task 2020-05-08T06:05:13.560Z] + set +x
[task 2020-05-08T06:05:13.560Z] TinderboxPrint: rooting hazards<br/>1
[task 2020-05-08T06:05:13.560Z] TinderboxPrint: (unsafe references to unrooted GC pointers)<br/>198
[task 2020-05-08T06:05:13.560Z] TinderboxPrint: (unnecessary roots)<br/>1297
[task 2020-05-08T06:05:13.560Z] TinderboxPrint: missing expected hazards<br/>0
[task 2020-05-08T06:05:13.560Z] TinderboxPrint: heap write hazards<br/>0
[task 2020-05-08T06:05:13.561Z] TEST-UNEXPECTED-FAIL | hazards | unrooted 'nogc' of type 'JS::AutoCheckCannotGC' live across GC call at dom/canvas/CanvasRenderingContext2D.cpp:5109
[task 2020-05-08T06:05:13.562Z] TEST-UNEXPECTED-FAIL | hazards | 1 rooting hazards detected

hazard:

Function '_ZN7mozilla3dom24CanvasRenderingContext2D17GetImageDataArrayEP9JSContextiijjR12nsIPrincipalPP8JSObject$uint32 mozilla::dom::CanvasRenderingContext2D::GetImageDataArray(JSContext*, int32, int32, uint32, uint32, nsIPrincipal*, JSObject**)' has unrooted 'nogc' of type 'JS::AutoCheckCannotGC' live across GC call '_ZN7mozilla3dom29GeneratePlaceholderCanvasDataEjPPh$void mozilla::dom::GeneratePlaceholderCanvasData(uint32, uint8**)' at dom/canvas/CanvasRenderingContext2D.cpp:5109

Flags: needinfo?(tom)
Pushed by csabou@mozilla.com:
https://hg.mozilla.org/integration/autoland/rev/ab2a75db3ebe
In RFP mode, turn canvas image extraction into a random 'poison pill' for fingerprinters r=tjr,jrmuizel
Status: ASSIGNED → RESOLVED
Closed: 2 years ago
Resolution: --- → FIXED
Target Milestone: --- → mozilla78
Regressions: 1638211
Regressions: 1638255
Regressions: 1663586

(In reply to Tom Ritter [:tjr] (ni? for response to sec-[advisories/bounties/ratings/cves]) from comment #0)

Brave slightly randomizes the result (making it imperceptibly different but with the overall correct return result)

(Because they are returning the real-ish value, they need to use a per-execution per-origin seed to avoid repeat calls that average out the random value.)

It seems pretty very weak: FYI: it only alters approx 25% of the pixels, only the b channel, and only by 1 pixel. Sso feel free to FP the r + g + a channels: not entirely sure how much that affects entropy: I was thinking of playing with greyscale

RFP on the other hand just monsters every pixel, every channel :)

and only by 1 pixel

I meant the integer value by 1

(In reply to Simon Mainey from comment #15)

RFP on the other hand just monsters every pixel, every channel :)

While confusing users into thinking their GPU is failing. Is it not possible to include a tiled symbol (eg 🚫) in the poison pill to help communicate to users that the corruption is intentional? This is a common stumbling block for new RFP users.

(In reply to Kestrel from comment #17)

While confusing users into thinking their GPU is failing. Is it not possible to include a tiled symbol (eg 🚫) in the poison pill to help communicate to users that the corruption is intentional? This is a common stumbling block for new RFP users.

That's what the urlbar canvas icon is for. I'm not sure if there are instances where it is not shown. Note that RFP is not front facing or promoted. But I agree that any breakage (e.g. uploading images on whatsapp, twitter etc) can be confusing and not everyone looks at the urlbar

I'm personally not sold on the idea universally, but I think it might be worthy in selective instances (e.g. user initiated, uploads: for which there is bug 1631673 ). RFP isn't trying to hide, so I don't think an overlay symbol or something in the top corner (RTL) of the canvas, and if it meets conditions such as dimensions, is a FP risk. Wonder what tom thinks (sorry for the noise)

Flags: needinfo?(tom)

remove needinfo

Flags: needinfo?(tom)
You need to log in before you can comment on or make changes to this bug.