This is how I understand the situation so far:
Content scripts are executed under a principle subsuming only the principle of the window they are attached to. The implications of that are quite well documented. Basically this means that you can only do stuff that the page can.
content scripts in previous extensions worked differently.
There are two things a content script can do that the page scripts can’t:
- access values in the ececution environment of the content script, including the (subset of the)
browserAPI - use
XMLHttpRequestandfetchwithout cross-origin restrictions
So what you are observing is by design and likely won’t ever change.
Now, for a way around it:
Instead on fetching the CSS (which should always work, but may cause an additional network request) and injecting it as a <style> (which should work unless the pages CSP disallows style-src 'unasfe-inline', but probably breaks the location of the style even if you append \n/*# sourceURL=${ originalUrl } */ to it, you could also try to set the crossorigin attribute on the link (you may have to clone the link to re-evaluate it).
If the server sets the correct Access-Control-* headers, you should then be able to access the corresponding document.styleSheets entry without restrictions.
Here is some more stuff I originally thought was relevant for the answer. It isn’t really relevant, but I think it is still interesting:
There is some additional some magic happening in content scripts. This is very poorly documented – or I haven’t found the documentation yet, but I don’t think there is any. That means everything that follows is what I analyzed from the resulting behavior and may be inaccurate:
- Much of it starts with the fact that, in content scripts,
this !== window. The very last section on the MDN page about content script hints at that by saying thateval()does something different thanwindow.eval(). But that is it in terms of documentation. So far, what I have found is this:-
Window.prototype === this.__proto__ >>> truewhich would imply that this is a differentWindowinstance - but someone is trying to fool us:
Object.getPrototypeOf(this) === this.__proto__ >>> falsewhich means that someone overwrote the ES6.__proto__getter. I have no idea why that was done and think was is a very bad idea. - so, what is the [[Prototype]] of this? Well,
Object.getPrototypeOf(this) >> Window → https://... - which gives the same output as
window >> Window → https://...so surely those two are the same - nope:
Object.getPrototypeOf(this) === window >>> false-.- - but setting properties on
windowmakes them appear onthis(but not vice versa) which generally means thatwindowshould be in the prototype chain ofthis - so
this instanceof Object.assign(function() { }, { prototype: window, })should return true. It doesn’t. - in conclusion, after about an hour of typing test in the JS console of a content script, I still don’t know what the precise relation between
thisandwindowin content scripts is. In some regards,thisseems to inherit fromwindow, but that is not actually the case. The [[Prototype]] of window seems to be some kind of wired Proxy with a number of actual properties that proxies other properties to thewindow. I am pretty sure this is not ES6 standards compliant.
-
- Why did I mention all the above? Well, I wanted to check if
fetch()(i.e.this.fetch) is the same aswindow.fetch. As function values, they are not, in fact, there are three instances offetch(and many other build-in functions):this.fetch,window.fetchandObject.getPrototypeOf(this).fetchare all three different, as far as the===operator is concerned.
But since all three fetch functions seem to behave the way described in the first section, this doesn’t really matter.