arkenfox / user.js

Firefox privacy, security and anti-tracking: a comprehensive user.js template for configuration and hardening
MIT License
9.51k stars 506 forks source link

update the userscripts #860

Closed earthlng closed 4 years ago

earthlng commented 4 years ago

Let's try the history.length spoofer first. This is what it looks like atm:

// ==UserScript==
// @name        Conceal history.length
// @description Intercepts read access to history.length property
// @namespace   localhost
// @include     *
// @run-at      document-start
// @version     1.0.1
// @grant       none
// ==/UserScript==

let _history={length:history.length};
Object.defineProperty(history,'length',{
  get: function() {
    if (_history.length > 2) {
      return 2;
    } else {
      return _history.length;
    }
  }
});

this apparently works in VM because VM does weird things and probably doesn't respect the security boundaries between privileged content scripts and page scripts. So let's ignore VM and try to make it work with GM.

This is what I got so far:

// ==UserScript==
// @name        history.length spoof
// @include     *
// @run-at      document-start
// @version     1
// @grant       none
// ==/UserScript==

const r = Reflect.defineProperty(history.__proto__.wrappedJSObject,
  'length', {
    get: exportFunction(function length() { console.log("spoofing now..."); return 20; }, window, {defineAs: "get length"})
  }
);

if (!r) console.error('history.length spoof: defineProperty failed');

and a scratchpad script to check if the spoofing is detectable:

(() => {
  if (history.length !== 20) return console.warn("history.length not spoofed");

  const sfunc = history.__lookupGetter__('length').toString();
  const propdesc = JSON.stringify(Object.getOwnPropertyDescriptor(history, 'length'));
  const atts = JSON.stringify(Object.getOwnPropertyDescriptor(history.__proto__, 'length')); // "{\"enumerable\":true,\"configurable\":true}"
  const getlength = JSON.stringify(Object.getOwnPropertyDescriptor(history.__proto__, 'length').get.length);
  const getname = JSON.stringify(Object.getOwnPropertyDescriptor(history.__proto__, 'length').get.name);

  //console.log(sfunc);
  console.log('concealed custom function:', sfunc === "function length() {\n    [native code]\n}");
  //console.log(propdesc);
  console.log(typeof propdesc === 'undefined');
  //console.log(atts);
  console.log(atts === '{"enumerable":true,"configurable":true}');
  //console.log(getlength);
  console.log(getlength === '0');
  //console.log(getname);
  console.log(getname === '"get length"');

  try {
    const t = typeof Object.getPrototypeOf(history).length;
    console.warn('history.length spoofing detected!');
  } catch (e) { console.info('no spoofing detected'); }

})();

The scratchpad should show 5x true plus the info no spoofing detected.

What I have so far seems to check all the boxes except that typeof Object.getPrototypeOf(history).length is now a number (instead of a getter?) and probably as a result of that, calling Object.getPrototypeOf(history) also triggers the custom function.

I also have no idea how to access the original history.length in the custom function for comparison (ie if (_history.length > 2)). Bonus points if someone can make that work ;)

@kkapsner or anyone else (@erosman, @KOLANICH, @claustromaniac ?), please help!

Running the scratchpad (w/o the if (history.length !== 20)) to test Canvasblocker's history.length spoofing shows that it's totally undetectable - how the hell did you do that and what am I doing wrong? :)

ps: exportFunction is not supported by Firemonkey (atm?) and IDK if wrappedJSObject works in the Userscripts API sandbox either

erosman commented 4 years ago

ps: exportFunction is not supported by Firemonkey (atm?) and IDK if wrappedJSObject works in the Userscripts API sandbox either

Above are not Web API. In FM, user-scripts have the same privilege as page script (plus the dedicated GM.*). In page script, all Web API works normally.

Script Managers that don't use the dedicated userScripts API, inject the user-scripts in a similar way a privileged content script is injected, thus more privileged functions are exposed.

What is the end purpose and benefit of the script?

earthlng commented 4 years ago

Yes, I'm aware of the differences between the userScripts and contentScripts API.

Above are not Web API.

that doesn't mean that you couldn't make them available if you wanted to. Not saying that you should, just that it's possible :)

What is the end purpose and benefit of the script?

the purpose is to hide the real history.length from the website. the benefit is questionable but I'm more interested in how to make something like this work in general rather than caring too much about this particular script

exportFunction is not supported by Firemonkey (atm?)

don't take that as criticism - it's just stating a fact

kkapsner commented 4 years ago

how the hell did you do that and what am I doing wrong?

I call the original getter within the new one and therefore an error is triggered on the prototype. You can check out https://github.com/kkapsner/CanvasBlocker/blob/master/lib/modifiedHistoryAPI.js

But your script is detectable in another way: window["get length"] !== undefined. I think you use the defineAs option to show the correct function name. But this also registers a global variable with that name (https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XPCOM/Language_Bindings/Components.utils.exportFunction#Parameters). To prevent that I really create a getter in CB.

erosman commented 4 years ago

the benefit is questionable but I'm more interested in how to make something like this work in general rather than caring too much about this particular script

The history can be cleared in another way.

A possible approach could be a combination of window.addEventListener('popstate' ...) or window.addEventListener('beforeunload'...) & window.stop() & Location.replace()

that doesn't mean that you couldn't make them available if you wanted to. Not saying that you should, just that it's possible :)

True :) I have been asked that before and I believe it creates security holes and defy the purpose of the secure API. If there is a safe way (I need to check that with AMO security team), it can be considered.

earthlng commented 4 years ago

@erosman if you want to keep the userScripts APIs clean and safe, you could just give scripts an option to be registered as a contentScript instead of a userScript. That should be pretty easy to implement since both APIs' register functions use the same contentScriptOptions (except css and scriptMetadata)

KOLANICH commented 4 years ago

@earthlng,

  1. if you want to spoof more than a single property, you can probably want Proxy.
  2. for methods don't forget to substitute toString and toSource ... and all their children infinitely deep down the chain. It is possible (I have done it myself in 2010, but have lost the source, it was a bit tricky to do in order not to cause an infinite recursion), but ...
  3. invocation of hooks takes time. And this time depends on what is in the hook and on CFG path taken. Usually more time than invocation of pure browser API. And probably less time if you just skip API invocation and the API is slow. So it can be possible to detect the hooking and fingerprint your hooks, if the website knows how much time an unhooked execution takes. Probably can be countermeasured by balancing all the branches. But think a bit - a website can measure the performance of something definitely unhooked and then try to predict the time the operation should take if it is not hooked, for example by using a known relation (I have not conducted a research about that, so it is up to you) (time api call takes in an etalon environment)/(time benchmark takes on the same machine). I guess the relation can be a function of the used hardware, environment settings and the state of OS schedulers. Hardware probably can be fingerprinted and predicted and its effect estimated. OS schedulers can probably be ruled out by warming the caches: if memory mapping is used for accessing disc, on the second attempt to read the same info the data will be already in memory and probably in CPU cache, so no context switches will likely occur, so no scheduler will likely be involved.

Anyway, my recommendation to you is to start not from a userscript, but from a webextension or just a webpage, because for them you can use the devtools console.

earthlng commented 4 years ago

@kkapsner

your script is detectable in another way

thanks. I was pretty sure that there were probably more ways to detect it but didn't find them

You can check out https://github.com/kkapsner/CanvasBlocker/blob/master/lib/modifiedHistoryAPI.js

That's what you told me the last time, too. Guess what, I'm still not getting it! :)

earthlng commented 4 years ago

@KOLANICH

thanks. I'm not really that interested primarily in the privacy aspect of this but mainly just want to know how to do this kind of thing properly. And to fix the script for people who want to use it with GM.

erosman commented 4 years ago

if you want to keep the userScripts APIs clean and safe, you could just give scripts an option to be registered as a contentScript instead of a userScript. That should be pretty easy to implement since both APIs' register functions use the same contentScriptOptions (except css and scriptMetadata)

Actually, contentScript API creates privileged content script with the same privilege as extension content script. userScript API which is based on contentScript API was specifically created to limit that privilege. ;) (In fact I was the one who asked for a secure API for user-scripts when contentScript API came out initially and liaised with the API developer through its development.)

FM actually uses contentScript API to inject CSS (no security issues there) and the secure userScript API to inject untrusted, unverified, 3rd party user-script.

kkapsner commented 4 years ago

@earthling:

// ==UserScript==
// @name        history.length spoof
// @include     *
// @run-at      document-start
// @version     1
// @grant       none
// ==/UserScript==

const descriptor = Object.getOwnPropertyDescriptor(history.__proto__.wrappedJSObject, "length");
const original = descriptor.get;
const fakeMax = 2;
const temp = {
    get length(){
        const originalValue = original.apply(this, window.Array.from(arguments));
        const fakedValue = Math.min(fakeMax, originalValue);
        if (fakedValue !== originalValue){
            console.log("spoofing now...");
        }
        return fakedValue;
    }
};
descriptor.get = exportFunction(
    Object.getOwnPropertyDescriptor(temp, "length").get,
    window
);

const r = Reflect.defineProperty(
    history.__proto__.wrappedJSObject,
    'length',
    descriptor
);

if (!r) console.error('history.length spoof: defineProperty failed');
earthlng commented 4 years ago

@kkapsner wow, thank you so much!! I was almost there but not quite ;)

I still have a few questions if you don't mind ...

So we have to call the original getter within the new one to trigger an error on the prototype, mainly to make it undetectable, right? Or is there another reason? And we need to do that even if we didn't want or need the original value in the custom function, right? Is that only required for getters (+ setters?) or also for values, do you know?

And we don't really need the window.Array.from(arguments) in .apply in this case but it doesn't hurt either, right?

earthlng commented 4 years ago

@Thorin-Oakenpants

now that we have the perfect solution for use with a proper UserScripts manager like Greasemonkey, there's still the issue of what to do about other managers like VM with its nasty workarounds like injecting script blocks or eval shenanigans or whatever the hell they're doing; and that wiki page in general.

With the whole document-start thing still unclear to me and without knowing exactly if or when that was fixed - either in FF itself and/or which extension (and in which version!) landed support for it --- and with the issue of different managers using different techniques to load/inject userscripts, it would probably be best to just get rid of that wiki page entirely. I don't want to update it, I don't want to maintain it, I want nothing to do with it.

The functionality of the 3 scripts that "we" offered, are all implemented in CB and people can just use that instead.

I'd also like to remove any mention of VM from all our pages and if we do want to recommend a userscript manager at all, it should be GM IMO. Plus perhaps FM for people who want to run some simple scripts in the safest possible way and/or to inject untrusted, unverified, 3rd party user-scripts. But a lot of 3rd-party user-scripts probably won't work with FM, so yeah, IDK.

waddaya say?

earthlng commented 4 years ago

@erosman

Actually, that is not safe.

what about the script discussed here is not safe? We wrote it, we know what it does, it's probably as undetectable as is possible while still maintaining its functionality and AFAIK there's simply no other way to achieve that without the ability to "breach" the sandbox.

I see your point of wanting to create a tool to run scripts in the safest way possible but if that prevents people from doing what they want to do, they'll just simply use another extension and all your hard work and good intentions achieved nothing. You're not responsible for the scripts that people decide to run and if someone wants to shoot themselves in the foot there's nothing you can do about that.

Someone else in the other thread proposed to show warning popups/notifications if a script needs/wants elevated privileges and I suggested to give users a way to run scripts in either the safe userScripts- or the more privileged contentScripts-sandbox, which would be a nice way of having the best of both worlds available in 1 tool. And you could put as many warnings around that as you see fit.

erosman commented 4 years ago

what about the script discussed here is not safe?

I was not referring to the script you have. I was referring to the whole practice of using contentScript API to inject unknown scripts.

contentScript API was created to programmatically inject trusted code as an alternative to injecting script/css via entry in manifest.json.

Using contentScript API , or other API meant for trusted code, to inject 3rd party untrusted code is not the safest option. AFAIK, FM is the only one manager that uses the secure userScript API which created for this purpose.

kkapsner commented 4 years ago

@earthlng

So we have to call the original getter within the new one to trigger an error on the prototype, mainly to make it undetectable, right? Or is there another reason?

In CB the original value is used to compare if we actually faked something. Also I return the original value if the value is smaller than the threshold. Otherwise it would be very easy to detect that: just open a completely new window and if the length is not 1 there is something fishy.

And we need to do that even if we didn't want or need the original value in the custom function, right? Is that only required for getters (+ setters?) or also for values, do you know?

For values you do usually do not have the error "problem" on the prototype. But I never had a value to fake in CB. Even window.opener and window.name are getters in FF (window.opener is a value in Chrome though).

And we don't really need the window.Array.from(arguments) in .apply in this case but it doesn't hurt either, right?

You actually do not need it. I only hurts the performance a little bit.

@erosman In my tests I saw that the .wrappedJSObject is already present in userScripts (which is kind of the old unsafeWindow object). So you would only have to expose exportFunction to the userScript (not sure if this can be done in an APIscript... but I saw that you have already https://github.com/erosman/support/issues/103 open with that) and the scripts would be fine. I do not know of a scenario where this would be a security issue.

earthlng commented 4 years ago

@kkapsner thanks for your answers.

Also I return the original value if the value is smaller than the threshold. Otherwise it would be very easy to detect that

Oh definitely. The old script did that and I was going to add it in the new script as well, once I knew how (which I do now, thanks to you)

expose exportFunction to the userScript (not sure if this can be done in an APIscript

it can be easily done and Luca Greco, the guy from mozilla who apparently implemented the userScripts API, showed how here.

If .wrappedJSObject is still available to userScripts and eval is too, it's really not that much more secure than contentScripts. There are other benefits of course, like (a) better isolation, (b) they apparently fixed a potential race-condition which I think means that runAt document-start should be more reliable and (c) the ability to pass variables etc to content scripts before they're injected while still guaranteeing that fe document-start actually runs at document start. The APIscript can also control which of the "small subset of the WebExtension APIs" available to contentScripts it wants to make available to userscripts. But that's mainly useful for userscript managers while the other benefits could also be handy for other webextensions with content scripts.

erosman commented 4 years ago

Just for Info: I had a chat with Luca Greco and pasted a snippet to erosman/support#103

earthlng commented 4 years ago

@kkapsner sorry if I'm starting to annoy you with all my questions :) But if you don't mind here are a few more ...

  1. I see in your CB code that you're using IIFE's like this:

    (function(){
    // ...
    }());

    ie the calling brackets inside the outer brackets instead of (function(){...})();. I assume they both work but is there any difference between the 2, or a reason for when to use 1 over the other?

  2. I tried to rewrite the old window.name userscript:

    
    // ==UserScript==
    // @name        Conceal window.name
    // @version     2.0
    // @include     *
    // @run-at      document-start
    // @namespace   ghacksuserjs
    // ==/UserScript==

const unsafeWindow = window.wrappedJSObject; const descriptor = Object.getOwnPropertyDescriptor(unsafeWindow, 'name'); const originalget = descriptor.get; const fakegetter = { get name(){ const originalName = originalget.apply(this); //No CAPTCHA reCAPTCHA if(/^https:\/\/www.google.com\/recaptcha\/api2\/(?:anchor|frame)\?.+$/.test(window.location.href) && /^I[0-1]_[1-9][0-9]+$/.test(originalName)) return originalName;

if (originalName != '') console.warn('Intercepted read access to window.name "'+originalName+'" from '+window.location);
return '';

} }; descriptor.get = exportFunction( Object.getOwnPropertyDescriptor(fakegetter, 'name').get, window );

// Q1: do we really need all this ...

const originalset = descriptor.set; const fakesetter = { set name(newname){ originalset.apply(this, window.Array.from(arguments)); window.name = newname; // Q2: <- is this the right way to do it?? } };

descriptor.set = exportFunction( Object.getOwnPropertyDescriptor(fakesetter, 'name').set, window );

// ... ?? descriptor should already have the original .set, no?

Reflect.defineProperty(unsafeWindow,'name',descriptor);


the 2 questions are in the code.
If the setter part is indeed necessary then at least we could combine fakegetter + setter in a single temp object, right?
kkapsner commented 4 years ago

Regarding 1: they are equivalent. Just a matter of style.

Regarding 2.1: as you always return "" in the getter you do not have to change the setter. But this makes you detectable. As a page could simply set the window.name and immediately check if it was set to the correct value. In CB I track in the name was set by the script and return that. (see https://github.com/kkapsner/CanvasBlocker/blob/master/lib/modifiedWindowAPI.js#L53). So in CB I replaced the setter just to notice if it was set.

2.2: No, you only have to call originalset.

2.??: the descriptor has the original setter at the beginning until you overwrite it.

And yes, you can combine the setter and getter in a single temp object. Didn't do that in CB because of how the managing code works. But in your case it would be much better to read/maintain.

earthlng commented 4 years ago

But this makes you detectable.

ah, that's what windowNames is for, now I get it. Wow, you really thought of everything!

Thanks again for all your help and explanations, I really appreciate it!

kkapsner commented 4 years ago

Wow, you really thought of everything!

Well, I tried to... only thinking like a criminal helps you catch them. So in the back of my head I always think "How can it detect and/or break that?" when I work on the protecting part of CB.

Thanks again for all your help and explanations, I really appreciate it!

You're welcome.

earthlng commented 4 years ago

I say there is zero benefit.

there is some benefit if someone has a pretty unique history.length due to (almost) never restarting FF or using session restore. You could be the only one who keeps loading/refreshing a certain site in a tab with history.length=47 or whatever. And since the pref is broken there's probably also no max length enforcement and if that's the case and your history.length is like 153 or so, you're almost certainly unique.

Obscuring the length by capping it to 2 doesn't do anything to stop push, go, replace.

yeah push could be problematic but only if you assume a site would actually use that trick to try to detect if you're spoofing the history.length. "replace" doesn't affect history.length and the other 3 could end up navigating away from a site, which would probably be less than ideal during any kind of FPing activity ;) The question becomes what's worse: potentially giving away 1 bit of information to sites that use push shenanigans to detect history.length spoofing or being potentially unique to every site that just reads the history.length. IMO it's pretty obvious

window.opener

if we're keeping that wiki page we might as well keep that script too, because it's easy to add exclusions for broken sites. And we can use GM.notification to let users know when the script actually kicks in, which is better than just the console warning in my extension.

earthlng commented 4 years ago

@erosman I take it that's your final decision? You're not going to implement at least exportFunction support? That'd be a real shame because I can't recommend FM when 2 of our 3 scripts won't work with it.

TBH, you're kind of giving users a false sense of security if you claim that FM is safe to run any kind of untrustworthy, unverified userscript just because it's based on the userscripts API and/or doesn't have exportFunction support. There are still plenty of bad things that a malicious script can do, or a malicious website that can exploit unintentionally vulnerable userscripts. And there's really nothing you can do about that.

At least the userScripts API somewhat limits the potential outfall in a worst-case scenario and for that reason alone it'd be nice to be able to recommend FM over any other userscripts manager atm.

Ultimately it's the responsibility of end-users not to run scripts they don't understand and you're not really doing anyone a favor by not giving us the tools we need to write safe scripts, especially when a malicious userscript could achieve the same thing by other means anyways.

If you're that worried about exportFunction etc, you could tie it to some kind of @grant permission which triggers a warning when a user installs, imports, saves or updates a script like that. But you should probably create a new @ value instead of "grant" because in GM etc, scripts don't need special permissions to use exportFunction. fe @exportFunction true or something like that

erosman commented 4 years ago

TBH, you're kind of giving users a false sense of security if you claim that FM is safe to run any kind of untrustworthy, unverified userscript just because it's based on the userscripts API and/or doesn't have exportFunction support.

Safer, not safe. The same way driving with seatbelts, makes it safer (but not 100% safe). If I start pointing out the exact reasons one by one, it may result in discontent. ;)

Mozilla created userScripts API because contentScript API was not safe enough for 3rd party scripts. There should be no argument in userScripts API being safer than contentScript API (or similar other methods) for 3rd party script.

One of the primary safety features of userScripts API is Xray vision.

Developers' desire to bypass this main feature (via unsafewindow, exportFunction() & cloneInto()) will simply bypass the security feature created for this very purpose.

I take it that's your final decision? You're not going to implement at least exportFunction support?

I spoke to Luca and Rob (Security team) at length and they said there isn't any safe method to implement exportFunction. If Mozilla security team say there is a safe method, I will definitely implement it.

Please note that exposing global variables to the user-scripts, works both way.

Out of curiosity ........... Can someone ask Jason Barnabe at Greasy Fork, what percentage of scripts require unsafewindow, exportFunction() & cloneInto()?!!! If the percentage is high, I will try to work something out with Mozilla security team (somehow).

earthlng commented 4 years ago

Safer, not safe.

fair enough :)

There should be no argument in userScripts API being safer than contentScript

there isn't. The userScripts API is definitely an improvement in every aspect AFAIK

One of the primary safety features of userScripts API is Xray vision.

the contentScript API has that too, see https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Content_scripts#Content_script_environment

bypass the security feature created for this very purpose

that's the point you don't seem to understand - it's already possible to bypass those security features with just wrappedJSObject and eval.

If Mozilla security team say there is a safe method

they'll never say that because there probably isn't any. But you're asking the wrong question. Ask them if there's anything you could do with exportFunction that you can't already achieve with wrappedJSObject and eval. Their answer will almost certainly be "no" but if they do say "yes" then please ask them for specifics because I'd really like to know. Thanks

erosman commented 4 years ago

the contentScript API has that too, see https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Content_scripts#Content_script_environment

contentScript API (as explained above) has greater access to browser scope APIs and hence the security risk. userScript API does NOT have access to any browser scope API, unless specifically defined by the script manager (where the script manager developer must make sure there are no security holes).

However, using contentScript API does not resolve the dilemma you have outlined.

Other script manager developers have specifically created an avenue to enable script writers to bypass the Xray vision (since the legacy Firefox was less restrictive and script manager developer decided to bypass the Quantum Firefox Xray vision security features in order to remain back-ward compatible with scripts using those features).

that's the point you don't seem to understand - it's already possible to bypass those security features with just wrappedJSObject and eval.

In that case, why cant those be used then?

Xray vision separates the scopes of running JS e.g. page (web site JS), content (extension JS injected into page). userscript (extension JS injected into page with limitations), & browser scope (the background JS of extension).

eval() is evaluated within its own scope, AFA I am aware. wrappedJSObject should also be the same.

The issue the script is having, is to cross scopes from limited privilege content JS to page JS (& vice versa).

I have not looked at your script in details, but if the intension is to run in page scope, it can be achieved via <script> ... </script>.

While it gets more difficult if the script needs to pass values between scopes, it is not an issue if the code needs to run in page scope by programmatically injecting the code into the page scope with <script> ... </script>.

As previously mentioned, if there is a high percentage of scripts that require such feature, I will do my best to find a way.

PS. Which script do you need it for and what does it do?

kkapsner commented 4 years ago

In that case, why cant those be used then?

Because they are detectable and cumbersome.

eval() is evaluated within its own scope, AFA I am aware. wrappedJSObject should also be the same.

No. eval() evaluates in the scope where it is called. If you have a variable a in that scope it will also be available in eval().

wrappedJSObject should also be the same.

Not exactly sure what you mean by that. The .wrappedJSObject is simply the the object without XRay.

PS. Which script do you need it for and what does it do?

The scripts are located in https://github.com/ghacksuserjs/ghacks-user.js/wiki/4.2.1-User-Scripts They want to protect some fingerprinting/tracking. But if these scripts are detectable (which they easily will be when using <script> and hiding with eval() is error prone) they are kind of useless as they provide a way to fingerprint/track by them.

PS: I think you should definitely stick with the userScript API. As wrappedJSObject is already exposed by Firefox itself I do not think that exportFunction would make a great difference. If a userscript wants to make itself vulnerable to the page scripts it already can. But I'm more a curious bystander - if I want something to be protected I simply include it in CanvasBlocker... ;)

erosman commented 4 years ago

No. eval() evaluates in the scope where it is called. If you have a variable a in that scope it will also be available in eval().

That is what I meant, the scope that it is in e.g. page. content, browser etc

Not exactly sure what you mean by that.

I meant as above.

They want to protect some fingerprinting/tracking. But if these scripts are detectable (which they easily will be when using Githubissues.

  • Githubissues is a development platform for aggregating issues.