violentmonkey / violentmonkey

Violentmonkey provides userscripts support for browsers. It works on browsers with WebExtensions support.

Home Page:https://violentmonkey.github.io/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

[Firefox] Can't modify page properties on sites which use CSP

chocolateboy opened this issue · comments

Re-opening this as a new issue as the last one got derailed...

This is my abiding issue with Violentmonkey (which I ❤️ - thank you!), but I can't see an open issue for it. There are related closed issues, and this issue may point the way to a fix, but I thought it'd be better to track the bug explicitly rather than inferring it from the documentation[1] and scattered comments.

What is the problem?

It's not possible to modify/mutate direct or nested properties of a page's window object with the following combination:

  • Violentmonkey for Firefox
  • sites which use CSP (e.g. GitHub, Google, Twitter)

Sites which use CSP don't run in Violentmonkey for Firefox unless @inject-into content is enabled, but @inject-into content is not compatible with unsafeWindow (or @grant none), which is needed to modify page objects.

There is a workaround for this on Firefox, but its use is not always obvious, and it requires browser/engine-specific code, which userscripts are meant to eliminate.

How to reproduce it?

// ==UserScript==
// @name          Hook XHR#open
// @version       0.0.1
// @include       https://twitter.com/*
// @include       https://github.com/*
// @include       https://*.google.tld/*
// @inject-into   content
// ==/UserScript==

const xhrProto = unsafeWindow.XMLHttpRequest.prototype

function hookXHROpen (oldOpen) {
    return function open () {
        console.warn('inside XHR#open')
        return oldOpen.apply(this, arguments)
    }
}

xhrProto.open = hookXHROpen(xhrProto.open)

What is the expected result?

XHR#open should be hooked and the message should be logged on those sites.

What is the actual result?

XHR#open isn't hooked and the message isn't logged.

Compatibility

Userscript engines this works in:

  • Tampermonkey (tested on Firefox)

Userscript engines this doesn't work in:

Related issues

Environment

  • Browser: Firefox v76.0.1
  • Violentmonkey: v2.12.7
  • OS: Linux (Arch)

Footnotes

  1. "Scripts requiring access to JavaScript objects in the web page will not work in [@inject-into content] mode."
  2. "GM4 does not yet support @grant none."
  3. @grant none isn't supported. unsafeWindow is but I couldn't get the XHR#open hook to work.
commented

The underlying reason is a bug or an architectural limitation in Firefox, see https://bugzil.la/1267027 ("Page CSP should not apply to content inserted by content scripts").

  1. We already tried to set unsafeWindow to unsafeWindow.wrappedJSObject but if a script exposes a function directly into unsafeWindow it'll break the page. There are scripts like that, including the popular ones - not that it matters because the only solution is for the authors to use exportFunction properly, but that is an unnecessary complication in 99.9999% of cases if we include all sites and all browsers like Chrome where this is not a problem. Realistically, we can't enforce that and unlike Greasemonkey4 we can't "move forward" either as Violentmonkey's purpose is to support the classic scripts.

  2. Using browser.userScripts API won't help because this API runs the scripts in what essentially is a content mode so the above paragraph applies.

  3. So, the only available workaround for extensions is to remove the CSP header entirely like Tampermonkey does. Turns out, Firefox doesn't allow graceful relaxation of CSP, it can't add a nonce or unsafe-inline, it can only make CSP stricter or remove it entirely. Yeah, ridiculous, but probably caused by some architectural limitation as well.

Implementation-wise, removing the CSP header is simple and since Tampermonkey is apparently allowed to do it by the AMO reviewers, we can add such an option as well.

On the other hand, the same may be achieved by any user by setting security.csp.enable to false in about:config so I'm not sure we should implement the workaround ourselves.

Thoughts, @gera2ld?

On the other hand, the same may be achieved by any user by setting security.csp.enable to false in about:config

Does the header solution disable CSP for all requests to an affected site? Or is it somehow scoped/sandboxed to a particular userscript?

If the former, it sounds like an insecure and dangerous workaround rather than a solution.

Ditto, of course, the user disabling it globally.

commented

CSP header is present only on the initial page response (the one that creates the main document or an iframe). It's not scoped to a script. It's global to the page/iframe environment. It defines what the environment can or cannot do. Removing the header on all URLs is the same as disabling CSP in about:config. The only advantage of doing it in Violentmonkey is that we can remove the header only if the site has active userscripts. Admittedly, this advantage is worth adding the workaround, but on the other hand this workaround is so lame that I'm reluctant to add it.

P.S. only Firefox devs can fix https://bugzil.la/1267027 so there is no solution to this problem that we can implement, only workarounds.

It is also worth noting that the hack with removing the CSP can cause a conflict with other addons that use CSP to block content (like Ublock Origin).

I encountered similar problems, but I used Chrome and Edge
#172
The difference is that I cannot execute new function (), and an error message
EvalError: Refused to evaluate a string as JavaScript because 'unsafe-eval' is not an allowed source of script in the following Content Security Policy directive: "script-src github.githubassets.com".

This is the script I use
https://greasyfork.org/en/scripts/419215-%E8%87%AA%E5%8A%A8%E6%97%A0%E7%BC%9D%E7%BF%BB%E9%A1%B5

commented

@mzzsfy, this issue is only about Firefox because in Chromium-based browsers the userscript authors can use GM_addElement to create DOM scripts.

ok, i will open a new issue to ask this question

commented

@mzzsfy, there's no need. You can suggest the author to use GM_addElement or you can install a separate extension to disable CSP of the site or you can switch to Tampermonkey, which has a built-in option to disable site's CSP.

Thank you. I will try to use 'GM_addElement' and contact the author

Not sure if this is relevant, but just in case:

The following change makes uBlock immune to the page's CSP

commented

Ah, I doubt though we can use it because it's asynchronous due to src and not textContent which we use now to pass the command channel securely so that the site doesn't gain full control of it.

Edit: yep, I don't see any way to use it securely, which means we can enable this trick only for grant-none scripts because they don't communicate with the extension, but that'd be a mostly useless and inconsistent half-measure. We should probably just cave and add the option to strip CSP...

commented

We can add a global option "Bypass site's CSP for scripts with page injection mode", enabled by default.

The injection mode can be set in the script code via @inject-into page by its author or in script's settings page by a user or via the global injection mode option in Violentmonkey's advanced settings (this one will effectively remove CSP for all scripts).

@gera2ld?

commented

Does that also mean the injection mode content can actually be removed? It looks great to me.

p.s. IIRC we have done this trick to inject scripts into Firefox in an early version and successfully caught the reviewer's attention, leaving me with the impression that such practices are not welcome in Firefox, especially when injecting code from outside this extension.

p.s. IIRC we have done this trick to inject scripts into Firefox in an early version and successfully caught the reviewer's attention, leaving me with the impression that such practices are not welcome in Firefox, especially when injecting code from outside this extension.

For reference:

Add-on Policies: Development Practices

  • Add-ons must not relax web page security headers, such as the Content Security Policy.
commented

We should keep content mode because the option may be disabled or the script may want to run in content for reliability on a hostile site. When MV3 implements userscripts API we'll only allow page and userscript (or another name) worlds and content will be equal to userscript.

Tampermonkey removes the CSP header and is allowed in AMO, so I guess it's okay as long as it's optional and user-controlled.

I noticed one of my scripts was breaking again on github with the message "Could not inject some scripts", even though I have removed all CSP headers via an extension, maybe Firefox has started blocking this ability and doesn't respect its modifications, or loads it after ViolentMonkey.

I tried TamperMonkey and that "just works" without modification. But I am quite used to ViolentMonkey and prefer it, so I think ViolentMonkey should include the same option to modify CSP headers itself.

However, I did manage to fix my script to work, by changing a few things, maybe this will help anyone else who is looking:

// This was previously "@inject-into page"
// @inject-into  content
// @grant        none

// I did not change this, but include it so you know what "win" is below.
const win = (typeof unsafeWindow === 'object' && unsafeWindow != null) ? unsafeWindow : window;

// I had to comment most of this out, or got an error "can't access dead object"
// In the content context, sites should not be able to override URL() anyway.
/* hack for sites that override URL() */
const _URL = win.URL; /*(() => {
    let urlDefinition;
    try {
        const iframe = doc.createElement('iframe');
        doc.documentElement.appendChild(iframe);
        urlDefinition = iframe.contentWindow.URL;
        iframe.remove();
    } catch (e) {}
    return (urlDefinition || win.URL);
})();*/

// I had to change this to use exportFunction, because I can not use deinfeProperty on window/unsafeWindow
/*Object.defineProperty(win, 'myExportedFunction', {value: myExportedFunction});*/
exportFunction(function () {
    let result = myExportedFunction();
    /* result will look like:
    {
      "url": "https://github.com/violentmonkey/violentmonkey-oex",
      "title": "violentmonkey/violentmonkey-oex - GitHub",
      "description": "violentmonkey/violentmonkey-oex - Violentmonkey, userscripts support for Opera (Presto).",
      "tags": ["github", "gh-archived"]
    }
    */
    return cloneInto(result, window);
}, window, {defineAs: 'myExportedFunction'});

The purpose of my script is to export this function that's used by a bookmarklet. It's far too large to just paste into a bookmarklet, which is why I'm using the userscript to inject a function, which is in turn called by the bookmarklet.

I also found that cloneInto does not work with some objects. I originally had the url property as an object created by new _URL(), which caused another error "Encountered unsupported value type writing stack-scoped structured clone".
At first I tried returning JSON.parse(JSON.stringify(myExportedFunction())) - this worked, but only by converting the URL to a string. I ended up just changing my bookmarklet to construct a URL object itself from the URL string, and changed the "url" property to a string. So there are some limitations with cloneInto to be aware of.

So is this getting fixed? I have a simple bookmarklet that I can't use because of firefoxes stupid CSP. the bookmarklet does nothing but use fetch to a local sever to transfer the url. It was working with tampermonkey but I switched to VM because TM seems to have performance issues in some cases and now the bookmarklet doesn't work.

I can't add functions to window with unsafeWindow with VM which would allow me to move the bookmarklet code inside a script. I guess I'm stuck with TM ;/