Closed earthlng closed 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?
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
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.
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.
@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)
@earthlng,
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 ...(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.
@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! :)
@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.
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.
@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');
@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?
@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?
@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.
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.
@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.
@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.
Just for Info: I had a chat with Luca Greco and pasted a snippet to erosman/support#103
@kkapsner sorry if I'm starting to annoy you with all my questions :) But if you don't mind here are a few more ...
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?
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?
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.
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!
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.
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.
@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
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).
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
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?
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... ;)
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.
Let's try the history.length spoofer first. This is what it looks like atm:
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:
and a scratchpad script to check if the spoofing is detectable:
The scratchpad should show 5x
true
plus the infono spoofing detected
.What I have so far seems to check all the boxes except that
typeof Object.getPrototypeOf(history).length
is now anumber
(instead of a getter?) and probably as a result of that, callingObject.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 ifwrappedJSObject
works in the Userscripts API sandbox either