airtower-luna / referer-mod

Web Extension to modify the Referer header in HTTP requests
https://addons.mozilla.org/en-US/firefox/addon/referer-modifier/
GNU General Public License v3.0
46 stars 13 forks source link

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

Closed tartpvule closed 2 years ago

tartpvule commented 3 years ago

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 commented 3 years ago

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

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.

tartpvule commented 3 years ago

... 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 commented 3 years ago

@airtower-luna Continuing from https://github.com/airtower-luna/referer-mod/pull/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!

:scream:

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 commented 3 years ago

Oh dear, what a mess! :scream_cat:

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. :sweat_smile:

tartpvule commented 3 years ago

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

r-a-y commented 3 years ago

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 commented 3 years ago

@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. :disappointed: 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 commented 3 years ago

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 commented 2 years ago

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 0b991c43b90fec14beaa19fb0bf0caec4690b01e.