Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

The document.referrer override feature is not working as it should #17

Closed
tartpvule opened this issue Jan 2, 2021 · 9 comments
Closed
Labels
bug Something isn't working

Comments

@tartpvule
Copy link
Contributor

I am using Firefox ESR 78.6.0 on Linux. I found some issues here.

  1. A trivial bypass: Reflect.getOwnPropertyDescriptor(Document.prototype, 'referrer').get.call(document);
    Firefox does not actually have the referrer property on the document object, but on its prototype, Document.prototype.

  2. Due to Firefox's current limitations, there is a race condition between asynchronous operations in the extension content scripts and the page's scripts.
    It is possible for the site to sometimes grab the original referrer before sending.then(setReferrer, handleError); resolves.

<!DOCTYPE html><html>
<head><title>Test</title></head>
<body>
One <script>document.write(document.referrer);</script><br>
Two <span id="foo"></span><br>
Three <span id="bar"></span>
<script>setTimeout(function() { document.getElementById('foo').innerHTML = document.referrer; }, 0);</script>
<script>setTimeout(function() { document.getElementById('bar').innerHTML = document.referrer; }, 1000);</script>
</body>
</html>

Observe the results, in very fast-loading pages, it is possible for "One" and "Two" to show the original referrer; "One" being more likely to get it.

airtower-luna added a commit that referenced this issue Feb 5, 2021
Sites could circumvent the document.referrer modification using
reflection, see bug #17. Modifying the Document.prototype avoids this.
@airtower-luna
Copy link
Owner

Thank you for the report! I've committed a fix for the reflection issue (ab7571f), could you check if it works? 🐱

I'm not sure if there's anything I can do about the timing issue, if you have a proposal on how it might be done I'll be all ears. 👂 Otherwise I'm afraid we'll have to wait for the Firefox bug https://bugzilla.mozilla.org/show_bug.cgi?id=1601496 to be fixed.

@airtower-luna airtower-luna added the bug Something isn't working label Feb 5, 2021
@tartpvule
Copy link
Contributor Author

... could you check if it works?

Looks good.

I'm not sure if there's anything I can do about the timing issue, if you have a proposal on how it might be done I'll be all ears. ear Otherwise I'm afraid we'll have to wait for the Firefox bug https://bugzilla.mozilla.org/show_bug.cgi?id=1601496 to be fixed.

One way to do it might be to duplicate all the extension logic, namely all the rules and the decision engine, into a content script that is then registered dynamically using contentScripts.register(). But this might mean a major rewrite/refactoring of the extension, with more bugs like its interaction with the History API. Not fun at all, surely. I will probably play with this idea when/if I have some time to kill.

Thank you, airtower-luna.

P.S. The last comment in that Bug 1601496 as of this comment said "Putting this one in the backlog for now.". Will probably take another 10+ years to be "fixed", I think.

@tartpvule
Copy link
Contributor Author

@airtower-luna Continuing from #20

An intractable problem remains:
Bug 1424176 : "document_start" hook on child frames should fire before control is returned to the parent frame`
Do note that this impacts every security WebExtensions that try hooking in "document_start" content scripts.

Sad news: it probably cannot be worked around, at least not without going into very insane heights.
If it was just <iframe>, <frame>, and window.open, this would be relatively easily solvable, and in fact I have written the code to hook those.

Oh how wrong I was! Things are not that simple! There is an INSANE rabbit hole: "window.frames".

The insanity, quoting from MDN:

frameList === window evaluates to true.

Quoting from a comment by Boris Zbarsky in the linked mozilla.dev.platform Google Groups: (emphasis mine)

... this is the only API that returns windows for subdocuments loaded via <object> in Gecko and WebKit ...

It is also mentioned that this insane Web API has existed since the days of Netscape in the 90s! Netscape!

😱

The workaround is insane: we need to hook things like Document#createElement, Element#innerHTML, Element#outerHTML, and even then we will miss nesting iframes.

function getRealReferrer() {
 var iframe = document.createElement("iframe");
 iframe.src = "about:blank";
 document.body.appendChild(iframe);
 var contentWindow = window[window.length - 1];
 var realGetter = Reflect.getOwnPropertyDescriptor(contentWindow.Document.prototype, "referrer").get;
 var realReferrer = realGetter.call(document);
 document.body.removeChild(iframe);
 return realReferrer;
}

Help wanted. Or can someone just FIX that Bug?

@airtower-luna
Copy link
Owner

Oh dear, what a mess! 🙀

That kind of sounds like writing a patch for at least one of those Firefox bugs might be a more effective use of time than trying to block every possible circumvention trick, especially considering that adding complex anti-circumvention code kind of invites bugs. 😅

@tartpvule
Copy link
Contributor Author

Take a look at my workaround! https://github.com/tartpvule/referer-mod/tree/oot-bug1424176 😄
Just a bit of warning: not "production ready".

@r-a-y
Copy link

r-a-y commented Jun 2, 2021

A good anti-fingerprinting script to test against is CreepJS - https://abrahamjuliot.github.io/creepjs/

With Referer Modifier 0.9 enabled, it fails some document.referer checks. I haven't tested the WIP commits yet.

@tartpvule
Copy link
Contributor Author

@r-a-y Interesting! I'm learning something new!

AFAICT:

c: calling the interface prototype on the function should throw a TypeError
d: applying the interface prototype on the function should throw a TypeError

Invalid. They have new apiFunction() before the real tests.

e: creating a new instance of the function should throw a TypeError

Solvable by defining the hook as a new-able function (not a getter), then the current Object#toString checks will catch it. But we will then need to deal with the function name (and probably other things) later.

f: extending the function on a fake class should throw a TypeError

Unsolvable. We have no opportunity to intervene at all. 😞
TypeError: undefined is not an object or null is thrown.

All in all, I'm not sure it's worth the effort to gun for 100% fingerprint-proofing.
Truly fixing these will probably require patching Firefox code to add the ability to fine-tune exportFunction, which is something I get the feeling Mozilla is only very reluctantly exposing to content scripts, and thus not interested in expanding its functionality.

Anyway, check out my oot-bug1424176 tree and Bug1424176_poc_esr78.patch!
Would love your feedback!

@tartpvule
Copy link
Contributor Author

I have created a souce code patch for exportFunction to create "not a constructor" function forwarders.
Check out my mod_ExportFunction_esr78.patch

@airtower-luna
Copy link
Owner

I'm going to close this because a reliable fix would have to be done in Firefox. I've added a note about the limitation to the README with 0b991c4.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

No branches or pull requests

3 participants