document.styleSheets throws exception when add-on injects styles to a page

I ran into an issue with my add-on on some websites that are making use of the document.styleSheets Web API. Because my add-on injects a <style> element into the page using a content script, when the website uses the document.styleSheets to loop through the stylesheets on the page, it causes a DOM Exception about accessing a cross-site stylesheet.

Uncaught DOMException: CSSStyleSheet.cssRules getter: Not allowed to access cross-origin stylesheet

It looks like this functionality is used often by websites using React.

I think it’s a great idea to stop websites from viewing stylesheets that have been added by an extension (in fact, it’s kind of worrying that Chrome doesn’t do this), but because the DOM exception is thrown, the page doesn’t render at all.

Is there some way for the content script to add a style element to the page without it being included in the document.styleSheets result?

Or, alternatively, is there a way for my content script to override the document.styleSheets property so that I can filter my extension’s stylesheet out of the list?

I started working on the latter, using this code:

unfilteredStyleSheets = document.styleSheets;
document.styleSheets = () => {
    const result = [];
    const sheets = unfilteredStyleSheets ();

    for (i = 0; i < sheets.length; i++) {
        if (sheets[i].ownerNode.id != MY_STYLESHEET_ID) {
            result.push(sheet[i]);
        }
    }

    return result;
};

It worked… but then I realized that document.styleSheets is not a function, so I was basically setting unfilteredStyleSheets to the list of stylesheets at that specific moment in time and essentially breaking the functionality of the document.styleSheets API.

To be honest, this kind of seems like a Firefox bug. Shouldn’t Firefox throw a warning instead of an exception, like it does for other cross-site issues.

When I attach an external css file using browser.tabs.insertCSS it seems to be invisible to document.styleSheets. Could that be a workaround to using inline style elements?

1 Like

How exactly are you injecting the style?
I know there is a several methods and I have a feeling some of them will work.

I’m using these two (typescript) methods:

  1. by adding a “link” element:
import {NumberGenerator} from '../number/number_generator';

export async function loadCSS(cssLink: string, isStylesText?: boolean) {
  const id = `id_file_${isStylesText ? NumberGenerator() : cssLink}`;
  if (document.getElementById(id)) return;   // prevent duplicates
  const link = document.createElement('link');
  link.href = isStylesText ? `data:text/css;base64,${btoa(cssLink)}` : cssLink;
  link.id = id;
  link.type = 'text/css';
  link.rel = 'stylesheet';
  return new Promise((resolve, reject) => {
    link.onload = resolve;
    link.onerror = reject;
    document.head.appendChild(link);
  });
}
  1. by appending “style” element into document head:
import {DOMContentLoadedPromise} from '../html/dom_ready';
import {LazyValFunction} from '../executors/lazy_val';

export class DynamicStyle {
  private readonly _style = document.createElement('style');   // we will modify this style in order to block "iframe" and set custom scroll cursor
  // instead of injecting script to the body right away, I will simply wait for the first usage
  private readonly injectToBody = LazyValFunction(() => this.onDomLoaded());

  constructor() {
  }

  private async onDomLoaded() {
    await DOMContentLoadedPromise;
    // document.head can be "null" in some XML
    document.head?.appendChild(this._style);
  }

  async loadRule(style: string) {
    await this.injectToBody();
    const _styleSheet = this._style.sheet;
    // in some XML the "sheet" is "null"
    _styleSheet?.insertRule(style);
  }

  async unloadAll() {
    await this.injectToBody();
    const _styleSheet = this._style.sheet;
    if (_styleSheet) {
      while (_styleSheet.cssRules.length > 0)
        _styleSheet.deleteRule(0);
    }
  }
}

PS: Looking at your function that filters elements from an array - this can be done in a single line:

Array.from(document.styleSheets).filter(x => x.ownerNode.id != MY_STYLESHEET_ID)

BTW, great out of box idea! And I think it would work if you use Proxy instead. Not exactly trivial though. I’ve build many proxy and I’m still not sure how to create a new one when I need one! But by replacing document.styleSheets with a proxy object you should be able to change the returned value even if it’s not a function (by defining a special get function).

I’m creating a new style element in the document head and giving it an ID so I can edit the rules within the style. The issue with that is if the website uses the document.styleSheets call and tries to read the rules, it will get a DOM exception. A good example of this is the StreamElements website.

The specific implementation on that website does have a check to prevent it from trying to load cross-site origin stylesheets, but it’s entirely dependant on the href value, which I don’t have on the style element because it’s inline.

Looking around, that seems to be a common issue on a lot of websites that are using React.

Honestly, I had never considered this. Long ago, I tried doing it that way, but it didn’t work for my use case. However, I’ve tested a bit today and it seems like thanks to the way my add-on has evolved, it’s now possible (and actually much simpler) to use the Tabs API.

Thanks.