Why Is Window (And Unsafewindow) Not the Same from a Userscript as from a <Script> Tag

Why is window (and unsafeWindow) not the same from a userscript as from a script tag?

See "Are Chrome user-scripts separated from the global namespace like Greasemonkey scripts?". Both Chrome userscripts/content-scripts and Greasemonkey scripts are isolated from the page's javascript. This is done to help keep you from being hacked, but it also reduces conflicts and unexpected side-effects.

However, the methods are different for each browser...

Firefox:

  1. Runs scripts in an XPCNativeWrapper sandbox, unless @grant none is in effect (as of GM 1.0).
  2. Wraps the script in an anonymous function by default.
  3. Provides unsafeWindow to access the target page's javascript. But beware that it is possible for hostile webmasters to follow unsafeWindow usage back to the script's context and thus gain elevated privileges to pwn you with.

Chrome:

  1. Runs scripts in an "isolated world".
  2. Wraps the script in an anonymous function.
  3. Strictly blocks any access to the page's JS by the script and vice-versa.

    Recent versions of Chrome now provide an object named unsafeWindow, for very-limited compatibility, but this object does not provide any access to the target page's JS. It is the same as window in the script scope (which is not window in the page scope).

That said, the version of your script that used unsafeWindow should work on/in Firefox if implemented correctly. It might work using the Tampermonkey extension on Chrome, but I'm not going to double-check that right now.

When you do that "trick" (var script = document.createElement("script"); ...), you are injecting code into the target page. This bypasses the sandbox and is the only way on a normal Chrome userscript for a script to interact with the page's JS.

Injection advantages:

  1. The only way for non-Tampermonkey userscripts to access objects or functions provided by the target page.
  2. Almost always fully compatible between Chrome, Firefox, Opera, etc. (IE is, as always, something else.)
  3. Often easier to debug the whole script; developer tools work normally.

Injection drawbacks:

  1. The script, at least the injected parts, cannot use the enhanced privileges (especially cross-domain) provided by the GM_ functions -- especially GM_xmlhttpRequest().

    Note that currently Chrome only supports GM_addStyle, GM_xmlhttpRequest, GM_log and GM_openInTab, fully, natively.

    Tampermonkey supports GM_ functions almost fully, however.

  2. Can cause side effects or conflicts with the page's JS.

  3. Using external libraries introduces even more conflicts and timing issues. It's nowhere near as easy as @require.

    @require, also runs the external JS from a local copy -- speeding execution and all but eliminating reliance on an external server.

  4. The page can see, use, change, or block the script.

  5. Requires JS to be enabled. Firefox Greasemonkey, especially, can run on a page which has JS blocked. This can be godsend on bloated, crappy, and/or intrusive pages.

Why do userscript managers still suport the use of unsafeWindow?

Let's call the creation of a <script> tag and inserting it into the page script injection.

Script injection has some drawbacks. To quote Brock Adams:

  1. The script, at least the injected parts, cannot use the enhanced privileges (especially cross-domain) provided by the GM_ functions -- especially GM_xmlhttpRequest().

  1. Can cause side effects or conflicts with the page's JS.

  1. Using external libraries introduces even more conflicts and timing issues. It's nowhere near as easy as @require.

    @require, also runs the external JS from a local copy -- speeding execution and all but eliminating reliance on an external server.

  1. The page can see, use, change, or block the script.

  1. Requires JS to be enabled. Firefox Greasemonkey, especially, can run on a page which has JS blocked. This can be godsend on bloated, crappy, and/or intrusive pages.

So, some developers may prefer to use unsafeWindow than to use script injection. Removing unsafeWindow would make things harder for them.

unsafeWindow often just works, and it can be less cumbersome than the creation and injection of a script tag.

Another issue is that, on the web, backwards compatibility is often one of the most important factors that is rarely, if ever, given up. If something works in a 2015 version of something, the general philosophy is that it should work in the 2020 version too, without requiring whoever worked on the 2015 version to come back and fix it (since they may not be around to do so anymore). Related discussion here. While userscript managers don't have to care as much about backwards compatibility as other things on the web, the same sort of reasoning applies - avoid breaking things that are currently working unless you have a really good reason.

Is a Greasemonkey script that uses unsafeWindow secure as long as the @grant type is 'none'?

I'm concerned about the security concerns raised with the use of unsafeWindow.

In GreaseMonkey, userscripts run in content script context while webpage JavaScript runs in page context.

The Xray vision separation is made specially to prevent page JavaScript gaining access to privileged functions.

unsafeWindow creates a bridge between the 2 contexts which if not careful, can expose some of the content context functions to the page JavaScript.

Note: In GreaseMokey (& FireMonkey) unsafeWindow is an alias for window.wrappedJSObject. Therefore, there is no difference between using either.
TamperMonkey & ViolnetMonkey implementation of unsafeWindow is different.

In your example code, if you don't need to use any of the GM functions, you can simply inject the whole code into the page context without the need for unsafeWindow e.g.

const script = document.createElement('script');
script.textContent = '...code....';
document.body.appendChild(script);
script.remove(); // if you want, makes no difference

why isn't Chrome unsafeWindow trick supporting XMLHttpRequest send override?

This:

/*--- Create a proper unsafeWindow object on browsers where it doesn't exist
(Chrome, mainly).
Chrome now defines unsafeWindow, but does not give it the same access to
a page's javascript that a properly unsafe, unsafeWindow has.
This code remedies that.
*/
var bGreasemonkeyServiceDefined = false;

try {
if (typeof Components.interfaces.gmIGreasemonkeyService === "object") {
bGreasemonkeyServiceDefined = true;
}
}
catch (err) {
//Ignore.
}

if ( typeof unsafeWindow === "undefined" || ! bGreasemonkeyServiceDefined) {
unsafeWindow = ( function () {
var dummyElem = document.createElement('p');
dummyElem.setAttribute ('onclick', 'return window;');
return dummyElem.onclick ();
} ) ();
}

followed by this:

unsafeWindow.document.title = 'testing';

Works just fine from my test userscripts.

These also work following the unsafeWindow trick:

unsafeWindow.foo = function () {
console.log ("In foo().");
};

unsafeWindow.alert = function (s) {
console.log ("Alert: ", s);
};

(On a page where the script has run, entering foo() in the console yields: "In foo().". alert() does not generate a popup but prints to the console.)

I don't know why (yet) that overriding XMLHttpRequest.prototype.send doesn't work like that from a Chrome userscript, but I don't recommend the unsafeWindow approach for that anyway.

Inject the override code. Use postMessage() (which works on Chrome as well) to communicate between the page scope and the script scope, if you don't (or can't) inject the whole script.

Are Chrome user-scripts separated from the global namespace like Greasemonkey scripts?

Yes, Greasemonkey scripts are normally wrapped in an anonymous function. And, Chrome userscripts apparently are too.

But, more importantly, Greasemonkey scripts are usually1 wrapped in an XPCNativeWrapper sandbox, while Google Chrome converts userscripts into extensions, and they operate in an arena that Google calls an "isolated world"2.

So, you don't need to wrap your script code in anonymous functions for security purposes, they're already protected.

Just beware that:

  1. If you inject code directly into the page (create a <script> tag), then that code can be seen by the page's JS.
  2. If you use unsafeWindow, then the page could theoretically follow it back and gain slightly elevated privileges.

The risk is very low, and I haven't been able to find any documented exploits in the wild.

~~~

Bottom line, scripts are isolated to different degrees in both browsers. (And not merely by being wrapped in anonymous functions.)

Greasemonkey has a nice set of privileged features available, in Firefox. While userscripts in Chrome are much more restricted.

However, much of GM's functionality is restored to Chrome via use of the Tampermonkey extension.



1 As of Greasemonkey version 1.0 (August 24, 2012), the sandbox is controlled by the @grant directive. If the script runs with (or defaults to) @grant none, then the sandbox isn't used. The script merely runs in a private scope and the normal GM_, API functions will not work.

2 Doesn't that sound so much bigger/nicer than some nasty sandbox? (^_^)

.

How can I mimic Greasemonkey/Firefox's unsafeWindow functionality in Chrome?

contentWindow was available in Chrome 3, but removed in Chrome 4. Only possible solution for Chrome 4:

location.href="javascript:(function(){ alert('Hello'); })()"

Code works as an Opera UserJS, but gives undefined errors in Chrome and Greasemonkey userscripts

That code won't work in Chrome because Chrome userscripts operate in a sandbox ("isolated world"), and you cannot set or use page-scope javascript objects in Chrome. And, Chrome does not fully/properly support unsafeWindow.

The code would work in Firefox+Greasemonkey with careful use of unsafeWindow, but that is not recommended here (and won't help with Chrome).

The classic approach, when one needs to interact with page-scope javascript in a cross-browser way is to use Script Injection. This is the only thing that works well in Chrome.

However, the smartest thing to do is not use page-scope JS at all if you don't have to. And, for what's in this question, you don't need to. (Hint: never use onclick or similar attributes! Always use addEventListener(), or equivalent.)

Refactoring the code to avoid leaving the sandbox scope, it becomes:

function hideImdbRatings () {
var oldStarBoxHTML;
var starBox = document.getElementsByClassName ("star-box");
if (starBox.length) {
starBox = starBox[0];
oldStarBoxHTML = starBox.innerHTML;
starBox.innerHTML = '<a href="#" id="showVote">Rate!</a>';

document.getElementById ("showVote").addEventListener (
"click",
function () {
//-- "this" is a special javascript scope.
this.innerHTML = oldStarBoxHTML;
}
);
}
}

window.addEventListener ('load', hideImdbRatings, false);


However 2, you'll notice that all of the code so far, busts the interaction of IMDB's rating widget. This is because it's overwriting innerHTML, which trashes the widget's event handlers. Don't use innerHTML like that.

The smartest-er thing to do is to hide the block, similar to Geo's answer, like so:

 function hideImdbRatings () {
var starBox = document.getElementsByClassName ("star-box");
if (starBox.length) {
starBox = starBox[0];
starBox.style.display = 'none';
var rateLink = document.createElement ('a');
rateLink.id = 'showVote';
rateLink.href = '#';
rateLink.textContent = 'Rate!';

starBox.parentNode.insertBefore (rateLink, starBox);

document.getElementById ("showVote").addEventListener (
"click",
function () {
//-- "this" is a special javascript scope.
this.style.display = 'none';
starBox.style.display = 'block';
}
);
}
}

window.addEventListener ('load', hideImdbRatings, false);


There is an additional factor that may stop the script from working in Chrome. By default, Chrome userscripts may run after the load event. To work around that, specify @run-at document-end in the metadata block of your script.

Why can't a script block contain a string constant with the word /script inside?

The HTML engine and the JavaScript engine are two different things. First, the HTML engine parses the document. It then passes along chunks of JavaScript code to the JavaScript engine. (And CSS code to the CSS engine, and so on.)

Since HTML doesn't know JavaScript syntax, it sees the closing tag as part of the overall content and merrily passes along the contents of the tags to the JavaScript engine:

<script>
var test = "<script>why?
</script>

More specifically, if you follow the parsing rules starting here...

  1. The first <script> put us in "Script data state"
  2. We remained there until the first <, where we entered "Script data less-than sign state"
  3. We encountered an s, where we returned to "Script data state"
  4. Some characters later, we encounter another <, where we enter "Script data less-than sign state"
  5. We then encounter a /, where we enter "Script data end tag open state"
  6. etc.

You can keep following along in the spec, but essentially it describes the step-by-step details of how the HTML engine is parsing each character as HTML syntax, regardless of how we intuitively interpret those characters.

How do I access an iframe's javascript from a userscript?

  1. unsafeWindow doesn't play nice with frames/iframes on Chrome, Tampermonkey, or Firefox.
  2. Trying to access global (to the frame) JS with jQuery, like that, will not work.
  3. userscripts will run on iframes that meet the @include, @exclude, and/or @match requirements.

So, you need to account for the multiple script runs and then you have two basic approaches, depending on what you are trying to accomplish. You can:

(A) Tailor the script to specific frame(s), as in this answer.

or (B) inject your JS and use the special frames object to grab the specific function you want.

The following script demonstrates both. Install it in Tampermonkey1 (or Firefox Greasemonkey), and then visit this test page at jsBin.

// ==UserScript==
// @name _Calling iframe functions
// @namespace _pc
// @include http://jsbin.com/ugoruz/*
// @include http://jsbin.com/okequw/*
// ==/UserScript==

console.log ("Script start...");

/*--- This next function call will work in Firefox or Tampermonkey ONLY,
not pure Chrome userscript.
*/
console.log ("calling functionOfInterest ()...");
unsafeWindow.functionOfInterest ();

if (window.top === window.self) {
//--- Code to run when page is the main site...
console.log ("Userscript is in the MAIN page.");

//--- The frames object does not play nice with unsafeWindow.
/*--- These next three work in Firefox, but not Tampermonkey, nor pure Chrome.
console.log ("1", frames[1].variableOfInterest); // undefined
console.log ("2", unsafeWindow.frames[1].variableOfInterest); // undefined
console.log ("3", frames[1].unsafeWindow); // undefined
*/
/*--- This next would cause a silent crash, all browsers...
console.log ("4", unsafeWindow.frames[1].unsafeWindow.variableOfInterest);
*/

//--- To get at iFramed JS, we must inject our JS.
withPages_jQuery (demoAccessToFramedJS);
}
else {
//--- Code to run when page is in an iframe...
console.log ("Userscript is in the FRAMED page.");
console.log ("The frame's ID is:", window.self.frameElement.id);
}

function demoAccessToFramedJS ($) {
$("body").prepend (
'<button id="gmMain">Run JS on main window</button>'
+ '<button id="gmFrame">Run JS on iframe</button>'
);

$("#gmMain, #gmFrame").click ( function () {
if (this.id === "gmMain") {
functionOfInterest ();
}
else {
frames[1].functionOfInterest ();
}
console.log (this.id + "was clicked.");
} );
}

function withPages_jQuery (NAMED_FunctionToRun) {
//--- Use named functions for clarity and debugging...
var funcText = NAMED_FunctionToRun.toString ();
var funcName = funcText.replace (/^function\s+(\w+)\s*\((.|\n|\r)+$/, "$1");
var script = document.createElement ("script");
script.textContent = funcText + "\n\n";
script.textContent += 'jQuery(document).ready(function() {'+funcName+'(jQuery);});';
document.body.appendChild (script);
};

console.log ("Script end");



You will see that the script runs a function from both the main page and from the iframe. The console output (Tampermonkey) will be:


Tampermonkey started
Script start...
calling functionOfInterest ()...
Userscript is in the MAIN page.
Script end
Tampermonkey started
Script start...
calling functionOfInterest ()...
Userscript is in the FRAMED page.
The frame's ID is: iframe2
Script end

1 It will also work as a straight-up Chrome userscript if you remove the unsafeWindow line(s).



Related Topics



Leave a reply



Submit