erosman / support

Support Location for all my extensions
Mozilla Public License 2.0
175 stars 12 forks source link

exportFunction() & cloneInto() not available in userscripts #103

Closed kekkc closed 4 years ago

kekkc commented 5 years ago

Hi,

seems exportFunction() & cloneInto() are not available in a userscript ( https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Sharing_objects_with_page_scripts#exportFunction ).

This seems to be because the userscript requires the extension to make this available for the actual user, which was described by Luca in a bugzilla bugreport: https://bugzilla.mozilla.org/show_bug.cgi?id=1516356#c1

Any chance that this will be implemented so that both function can be used in scripts?

erosman commented 5 years ago

FireMonkey uses the userScripts API which was specially created for security and privacy with Xray vision to prevent malware userscripts gaining access to privileged functions via the extension.

Scripts can use any JS function in the page-scope. Why does the content script require exportFunction() & cloneInto()?

For interacting with page JS you can also check out: Interacting With The Page

kekkc commented 5 years ago

Why does the content script require exportFunction() & cloneInto()?

Because I need to rewrite some functions, e.g.: exportFunction(function (){return true;}, a, {defineAs:'confirm'}); exportFunction(function (){return false;}, a, {defineAs:'opener'});

Reason for that is to prevent that my tabs get "hacked": https://mathiasbynens.github.io/rel-noopener/

erosman commented 5 years ago

Where is your userscript?

kekkc commented 5 years ago

Where? It's my own local script, I can PM it to you. But it currently doesn't contain much else other than a list of these functions that I nullify.

erosman commented 5 years ago

Have you tried window.eval(......) ?

exportFunction() & cloneInto() are not JS. They are Firefox internal functions not suitable for content script.

This seems to be because the userscript requires the extension to make this available for the actual user, which was described by Luca in a bugzilla bug report: https://bugzilla.mozilla.org/show_bug.cgi?id=1516356#c1

That is to make GM* functions available not to make the exportFunction() & cloneInto() available to users. That would be a great security risk if user-script can interact with internal browser functions. Other script managers should not make these function available to user-scripts.

kekkc commented 5 years ago

eval doesn't work for me reliably.

exportFunction() & cloneInto() are not JS. They are Firefox internal functions not suitable for content script.

FM is a FF extension ;)

But seriously: your linked GM documention leads to https://wiki.greasespot.net/Content_Script_Injection where it's cleary stated: "Warning: The contents of this page are not accurate when used with Greasemonkey 4.0." In GM 4.0 exportFunction() & cloneInto() are the only way to exchange data with the website, because unsafeWindow was disabled (I guess this includes eval).

That would be a great security risk if user-script can interact with internal browser functions.

No, the documentation on https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XPCOM/Language_Bindings/Components.utils.exportFunction describes it:

This function provides a safe way to expose a function from a privileged scope to a less-privileged scope. In this way privileged code, such as an extension, can share code with less-privileged code like a normal web page script.

erosman commented 5 years ago

FM is a FF extension ;)

Internal API are not content JavaScript. The code designed to run in Firefox background. It is not the same as the JavaScript that runs inside a web page.

For example, none of the browser.***.***() API are available inside JS in a web-page.

But seriously: your linked GM documention leads to https://wiki.greasespot.net/Content_Script_Injection where it's cleary stated: "Warning: The contents of this page are not accurate when used with Greasemonkey 4.0."

True.. that is a bad practice but some people still do it. I did not include the unsafeWindow in FireMonkey for that reason. I was showing you alternative methods of communication between userscript & page script.

In GM 4.0 exportFunction() & cloneInto() are the only way to exchange data with the website, because unsafeWindow was disabled (I guess this includes eval).

eval() is a JavaScript function while unsafeWindow is a GreaseMonkey made-up function and exportFunction() & cloneInto() are core Firefox functions designed to run in the browser-scope.

Page/content-scope functions should not be mistaken for browser-scope functions.

FireMonkey does not include unsafe methods that allows malware to cause harm to users by using browser-scope functions (such as exportFunction() & cloneInto() )

No, the documentation on https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XPCOM/Language_Bindings/Components.utils.exportFunction describes it:

That is used for extension's OWN code which is owned by the extension and checked and has privilege. Extension code is the code that is inside the addon when you download the addon.

It is not to be used for userscript which is not written by the extension developer and not checked by AMO and has unknown code & origin.

userscript is content-scope JavaScript code and can use any function that are part of JavaScript.

kekkc commented 4 years ago

Old thread, but just some addition:

exportFunction() & cloneInto() are core Firefox functions designed to run in the browser-scope.

No, seems this was not the intention, according to the bugzilla ticket, this use case was considered primarily and should be alot safer & more reliable than e.g. eval:

The userScripts API already allows the extension itself to provide this kind of helpers (if they are needed and they should be available to the particular user script), because the userScripts's apiScript has these helpers available

erosman commented 4 years ago

exportFunction() & cloneInto() are not standard JavaScript. In order to make them available, background script has to create an API for them specifically.

I have to check with Mozilla security team to see if there are the security risks. If there are no security risks, then it can be considered.

erosman commented 4 years ago

Ref: Chat with Luca Greco (userScript API Developer)

erosman: I have always tried to make FireMonkey as secure as possible and avoid risky situations. Global window functions like the unsafeWindow has been known to be a risk. window.wrappedJSObject&exportFunction&cloneIntoare similar tounsafeWindow`. I don't want to open an avenue for exploitation even if that means losing users.

Luca Greco: yeah, exposing those (unsafeWindow, exportFunction and cloneInto) will make the userScript potentially more exposed to the content, this is definitely true, and so potentially more open to exploitation. We decided not to provide those helpers in the userScripts global by default for the same reasons.

erosman commented 4 years ago

I have updated the Help to provide more info on the subject.

Also ..... quoting from above linked "update the userscripts"

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).

kekkc commented 4 years ago

Thanks for digging into this and consolidating the info here ;) Would be cool if there's an "official" way.

I only found this old blog post that announced ""unsafe-content-script": true" as workaround and exportFunction() & cloneInto() as the "real fix" for userscripts (in 2014 these were content scripts / GM scripts)( https://blog.mozilla.org/addons/2014/04/10/changes-to-unsafewindow-for-the-add-on-sdk/ ).

Also GM needs a way to turn xrays of, if that is related: https://bugzilla.mozilla.org/show_bug.cgi?id=1437098#c22

erosman commented 4 years ago

Firefox implemented Xray vision to make users safer. Turning off or bypassing Xray vision is a risk to the user security and will open up security holes.

exportFunction() & cloneInto() create security holes. The MDN document explains the issue with reference to extensions own code.

It is even more dangerous if that is allowed for unknown 3rd party scripts (e.g. user-script) as it will create all sorts of problems.

👉 There are methods to interacts with page scope JS without the need for exportFunction() & cloneInto() as listed on the referenced pages.

Here are some snippet that I have added to the Help document for the next release. ⬇⬇⬇⬇⬇

unsafeWindow

Source: unsafeWindow

This API object allows a User script to access "custom" properties--variable and functions defined in the page--set by the web page. The unsafeWindow object is shorthand for window.wrappedJSObject. It is the raw window object inside the XPCNativeWrapper provided by the Greasemonkey Sandbox.

USE OF UNSAFEWINDOW IS INSECURE, AND IT SHOULD BE AVOIDED WHENEVER POSSIBLE.

Safer alternatives to unsafeWindow are also listed in above page.

Source: unsafeWindow

This command can open certain security holes in your user script, and it is recommended to use this command sparingly.
Please be sure to read the entire article and understand it before using it in a script.

More Info: Why is window (and unsafeWindow) not the same from a userscript as from a <script> tag?

Sharing objects with page scripts

Keywords: Xray vision in Firefox, exportFunction(), cloneInto(), Constructors from the page context

Source: Sharing objects with page scripts As an extension developer you should consider that scripts running in arbitrary web pages are hostile code whose aim is to steal the user's personal information, damage their computer, or attack them in some other way. The isolation between content scripts and scripts loaded by web pages is intended to make it more difficult for hostile web pages to do this. Since the techniques described in this section break down that isolation, they are inherently dangerous and should be used with great care. ... Note that once you do this, you can no longer rely on any of this object's properties or functions being, or doing, what you expect. Any of them, even setters and getters, could have been redefined by untrusted code.

Xray Vision & JavaScript Scopes

  1. Browser Scope (Trusted privileged code to interact with browser)
  2. Extension Content Scope (Trusted extension JS injected into web page with API privileges)
  3. User-Script Scope (Untrusted unverified 3rd party JS injected into web page without direct API privileges)
  4. Page Scope (JS injected in a web page by the website)
erosman commented 4 years ago

Note:

TemperMonkey unsafeWindow seems to be implemented via injecting <script>...</script>

GreaseMonkey unsafeWindow seems to be implemented via eval() & window.wrappedJSObject

ViolentMonkey unsafeWindow seems to be implemented via eval()

erosman commented 4 years ago

v1,24 Added unsafeWindow support

kekkc commented 4 years ago

Wow, pretty cool, thx ;)

Realized only that this code produces an "Error: Permission denied to access object" on FF 72.02: Userscript: window.wrappedJSObject.focus = function (){}; Website:

<SCRIPT language=JavaScript><!--
self.focus();
//--></SCRIPT>

Strangely this error is not displayed in FF developer 73b12

erosman commented 4 years ago

FM unsafeWindow also uses window.wrappedJSObject (similar to GM).

Let me know how it works out.

kekkc commented 4 years ago

Gotcha, unsafeWindow works great, no errors. Many many thanks erosman ;)

Sxderp commented 4 years ago

I could be mis-remembering (my testing was done so long ago and I haven't touched scripts in a while), but I think exportFunction retains line numbers on errors. A lot of things exportFunction does can be mimiced with a window.eval or new window.Function('arg1', 'arg2', 'return arg1 + arg2'), but you lose some debuggability.

Not saying it's worth the security risks (assuming my memory is working properly), just thought I'd point it out.

erosman commented 4 years ago

@Sxderp Thanks for the info. I will test it out.

userScript API does log errors to the console with clickable reference to the error location (e.g. script blob), although sometimes errors are generated elsewhere for example if GM.*/GM_* API is used.

Sxderp commented 4 years ago

Here's a sample. Assuming exportFunction is available.

// ==UserScript==
// @name         meh
// @version      1.0
// @match        https://example.com/*
// ==/UserScript==
function exportFunc() {
  let unknown;
  unknown.error;
}
exportFunction(exportFunc, window, {defineAs: 'exportFunc'});

let funcFunc = new window.Function(`
let unknown;
unknown.error;
`);
window.wrappedJSObject.funcFunc = funcFunc;

window.eval(`
window.evalFunc = function() {
let unknown;
unknown.error;
}
`);

Go to https://example.com and open the debug window (Ctrl+Shift+K). In the interpreter tool (I don't know what it's actually called), run window.exportFunc(), window.funcFunc(), window.evalFunc(). The export is the only one with a clickable error line.

erosman commented 4 years ago

Test 1

// ==UserScript==
// @name         meh
// @version      1.0
// @match        https://example.com/*
// ==/UserScript==

function exportFunc() {
  let unknown;
  unknown.error;
}

typeof exportFunction !== 'undefined' &&  
       exportFunction(exportFunc, window, {defineAs: 'exportFunc'});

let funcFunc = new window.Function(`
  let unknown;
  unknown.error;
`);
window.wrappedJSObject.funcFunc = funcFunc;

window.eval(`
  window.evalFunc = function() {
  let unknown;
  unknown.error;
  }
`);

console.log('window.exportFunc', typeof window.exportFunc);
console.log('window.funcFunc', typeof window.funcFunc);
console.log('window.evalFunc', typeof window.evalFunc);

image

All above console.log are clickable to the blob.

erosman commented 4 years ago

Note: code errors also have clickable links

image

erosman commented 4 years ago

Test 2

// ==UserScript==
// @name         meh
// @version      1.0
// @match        https://example.com/*
// ==/UserScript==

function exportFunc() { console.log('this is exportFunc'); }

typeof exportFunction !== 'undefined' &&
    exportFunction(exportFunc, window, {defineAs: 'exportFunc'});
console.log('exportFunc', typeof window.exportFunc);

let funcFunc = new window.Function(`console.log('this is funcFunc');`);
console.log('funcFunc', typeof window.funcFunc);

window.wrappedJSObject.funcFunc = funcFunc;
console.log('funcFunc', typeof window.funcFunc);

window.eval(`function evalFunc() { console.log('this is evalFunc'); }`);
console.log('window.evalFunc', typeof window.evalFunc);

image

AFA how to actually export function without exportFunction() I have to do more testing.

It seems unsafeWindow/window.wrappedJSObject can get/set variables in the page window and can get functions but not set them !!? (It could be the Xray Vision blocking it).

window.eval doest work either

window.eval(`var foo = "I'm defined in a page script!";`);
console.log('window.foo', typeof window.foo); // window.foo undefined
console.log(window.foo); // undefined
console.log(foo);
//Console
//ReferenceError: foo is not defined            ae3ddbd7-5284-49d1-be8a-a72925516b9d:15:1

Note: these are tested with FM & userScript API that has Xray Vision.

erosman commented 4 years ago

Test 3

// --- Page JavaScript
var foo = "I'm defined in a page script!";
function runTest() {
  console.log('runTest', foo); // runTest new unsafeWindow foo
  console.log('testFunc', typeof window.testFunc); //  testFunc function
  window.testFunc(); // Error: Permission denied to access object
 testFunc(); // Error: Permission denied to access object
}
// --- user-script
console.log('unsafeWindow.foo', unsafeWindow.foo); // unsafeWindow.foo I'm defined in a page script!
unsafeWindow.foo = 'new unsafeWindow foo'; // changing foo on page
unsafeWindow.testFunc = function() { console.log('this is testFunc'); } // creating new function
unsafeWindow.runTest(); 
unsafeWindow.testFunc(); // this is testFunc
testFunc(); // Error: Permission denied to access object

It seems...

re: cloneInto() I will test object creation Note: All data relates to FireMonkey only.

erosman commented 4 years ago

Test 4

Accessing Page DOM

unsafeWindow.testFunc = function() { console.log(location.href); }
unsafeWindow.testFunc(); // https://example.com/
erosman commented 4 years ago

Test 5

Normal cloneInto()

const obj = {text: 'hello from user-script'};
unsafeWindow.obj = cloneInto(obj, unsafeWindow);
unsafeWindow.obj = {text: 'hello from user-script'};
console.log(unsafeWindow.obj.text); // hello from user-script
Sxderp commented 4 years ago

After an eval you must use window.wrappedJSObject (assuming this is unsafeWindow in FM) to access the object in the UserScript.

window.eval(`
window.evalFunc = function() {
let unknown;
unknown.error;
}
`);
console.log(window.wrappedJSObject.evalFunc); // function evalFunc()

My concern is that you can't debug the content that was evaled. More complete example. Page code

function runTestEval() {
  console.log('runTestEval');
  console.log('evalFunc', typeof window.evalFunc);
  window.evalFunc(); // This errors with no clickable bit
}

Userscript

// ==UserScript==
//
// ==/UserScript==
window.eval(`
window.evalFunc = function() {
console.log('in eval func')
let unknown;
unknown.error;
console.log('out eval func')
}
`);

window.wrappedJSObject.runTestEval();

No error message appears in the web page console. An error does appear in the browser console, but it is not clickable.

Now, this is generally pretty edge case. I'd say it only occurs when you're purposely piggy-backing off of the page content, or trying to overwrite very specific functions within the page content.

erosman commented 4 years ago

I actually get a clickable reference however, since the actual function in on the page, it refers to the page (not the user-script that called it) Note: I didn't define it to force an error and see the result

image

unknown.error;

image

Sxderp commented 4 years ago

That's weird, how do you get evalFunc is not a function? Using Firemonkey with the above code blocks this is what I get in the page console; the eval link (console.log) is clickable, and sends you to the blob. But as you can see the page doesn't indicate any errors.

Screenshot from 2020-02-08 13-47-33

And in the browser console; the eval link is not clickable.

Screenshot from 2020-02-08 13-48-37

Sxderp commented 4 years ago

That creates a no-reference error

It's supposed to. In order to force an error.

erosman commented 4 years ago

Page Script

function runTestEval() {
  console.log('runTestEval');
  console.log('evalFunc', typeof window.evalFunc);
  window.evalFunc(); // This errors with no clickable bit
}

User script (I commented out the error)

window.eval(`
window.evalFunc = function() {
console.log('in eval func')
let unknown;
//unknown.error;
console.log('out eval func')
}
`);

window.wrappedJSObject.runTestEval();

All are clickable to the page or debugger

Clipboard01

Sxderp commented 4 years ago

All are clickable to the page or debugger

Yes. My concern was that errors are not clickable (nor do they show up in the page console). I would not call it a major issue, but it does make debugging a little harder.

erosman commented 4 years ago

That's weird, how do you get evalFunc is not a function?

Sorry... That was me. :p I was testing and didnt define it to see how the error comes up.

My concern was that errors are not clickable (nor do they show up in the page console)

Sadly, it is out of our hands ;)

kekkc commented 4 years ago

Sry to bother (again), but seems there's still some glitch, if the following userscript is used (all frames have to be used):

// ==UserScript==
// @name        asdf
// @run-at      document-start
// @match       <all_urls>
// @allFrames   true
// ==/UserScript==
unsafeWindow.document.hasFocus = function() {return true;};

on the following site: https://www.w3schools.com/jsref/tryit.asp?filename=tryjsref_document_hasfocus webconsole gets flooded with "Error: Permission denied to access object" that is called repeatedly & endlessly. In general, I guess there shouldn't be any issue, since the script should be inserted in the frame and therefore same-origin.

Sxderp commented 4 years ago

Try:

// ==UserScript==
// ...
// ==/UserScript==
let hasFocus = new window.Function('return true');
unsafeWindow.document.hasFocus = hasFocus;
erosman commented 4 years ago

@kekkc To start with, that is a really poor code on w3schools.com

setInterval("myFunction()", 1);

function myFunction() {
  var x = document.getElementById("demo");
  if (document.hasFocus()) {
    x.innerHTML = "The document has focus.";
  } else {
    x.innerHTML = "The document DOES NOT have focus.";
  }
}

That setInterval() is running 1000 times per second. Who wrote that!?? :-1:

Then the error comes from that page (not the user-script)

Error: Permission denied to access object               tryit.asp:14:16
->  if (document.hasFocus())

I tested it with GM and the same error come up, although the error only comes up when clicking as it breaks codemirror code.

Error: Permission denied to access object 5               codemirror.js:3702:90
-> var takeFocus = op.focus && op.focus == activeElt() && (!document.hasFocus || document.hasFocus())
kekkc commented 4 years ago

Try:

// ==UserScript==
// ...
// ==/UserScript==
let hasFocus = new window.Function('return true');
unsafeWindow.document.hasFocus = hasFocus;

Many thanks, that fixed it. BTW: I also included: Object.defineProperty(unsafeWindow.document, 'hidden', {value: false});

Maybe these methods of "overwriting" methods can also be included in the help file ;)

I tested it with GM and the same error come up, although the error only comes up when clicking as it breaks codemirror code.

Thanks, I realized the same. Know that w3schools.com is often bad, but at least good for tests.

OT: Nevertheless Sxderp's suggestion fixed the "Error: Permission denied to access object" (and many other issues I had with my old method of replacing methods. To be exact 20 years old, coming from an old program Proxomitron that focused on filtering / modifying webpages to get rid of ActiveX phone dialers)

erosman commented 4 years ago

@Sxderp @kekkc Thank you. I will add it to the Help. :+1:

PS. OT. I do recall Proxomitron from that time and used it before switching to Privoxy ;)

erosman commented 3 years ago

Support for exportFunction() & cloneInto() will be added to v2.19