javascriptiframegoogle-chrome-extension

Chrome extension fails to inject script into all types of iframes, even using matchOriginAsFallback=true


In my chrome extension I need a script to be injected into all iFrames. I am using matchOriginAsFallback=true (as per this SO thread: Can't inject content script into all IFRAMEs from my Chrome Extension) This injects into all iFrames I have tested but one, "same origin iFrame"

manifest.json:

{
"manifest_version": 3,
"name": "Helper",
"description": "Base Level Extension",
"version": "1.0",
"host_permissions": [ "<all_urls>" ],
"permissions": [
    "scripting",
    "contentSettings",
    "tabs",
    "activeTab",
    "storage",
    "declarativeNetRequest",
    "declarativeNetRequestWithHostAccess",
    "declarativeNetRequestFeedback"
],
"declarative_net_request": {
    "rule_resources": [{
        "id": "ruleset",
        "enabled": true,
        "path": "rules_test_version.json"
    }]
},
"action": {
    "default_popup": "helper.html",
    "default_icon": "helper.png"
},
"background": {
    "service_worker": "bg.js"
},
"web_accessible_resources": [{
    "resources": ["config.json"],
    "matches": ["<all_urls>"]
}]

}

in my bg.js

await chrome.scripting.registerContentScripts([{
  id: 'proxy',
  js: ['proxy.js'],
  matches: ['<all_urls>'],
  runAt: 'document_start',
  world: 'MAIN',
  matchOriginAsFallback: true,
  //matchAboutBlank: true,
  allFrames: true,
}]);

The site I am testing against is CreepJS https://abrahamjuliot.github.io/creepjs/tests/iframes.html where the iFrames that fail is created like this:

const getSameSourceIframe = async () => {
    try {
        const iframe = document.createElement('iframe')
        iframe.setAttribute('style', 'display:none')
        iframe.src = location.href
        document.body.append(iframe)
        const data = await getData(iframe.contentWindow)
        iframe.parentNode.removeChild(iframe)
        return data
    } catch (error) {
        console.error(error)
        return
    }
}

https://github.com/abrahamjuliot/creepjs/blob/master/docs/tests/iframes.js

As per the comment from @woxxom I verified if the content script was ran with a console log, and it indeed seems like the content script is run, but too late to affect the output from the iFrame.

I tried to proxy the HTMLIFrameElement.prototype.contentWindow to override the getter but I have no idea how to do that since HTMLIFrameElement is an interface, and I cannot find any info on how to do that. If I try to do it like I have for properties and functions I only get Uncaught TypeError: Illegal invocation

In my content javascript:

const contentWindowTarget = HTMLIFrameElement.prototype.contentWindow;

const contentWindowHandler = {
    get(target, prop, receiver) {
        if (prop === "window") {
            console.log('HTMLIFrameElement.prototype.contentWindow:' + prop);
            return target[prop];
        }
        else{
          return target[prop];
        }
      },
};

const contentWindowProxy = new Proxy(contentWindowTarget, contentWindowHandler);

HTMLIFrameElement.prototype.contentWindow = contentWindowProxy;

This throws: Uncaught TypeError: Illegal invocation I tried to bind() to this and window but I feel like I really have no idea what im doing..


Solution

  • With a lot of help from woxxom I came up with this:

      const contentWindowGetter = Object.getOwnPropertyDescriptor(HTMLIFrameElement.prototype, 'contentWindow').get;
    
      Object.defineProperty(HTMLIFrameElement.prototype, 'contentWindow', {
          get() {
              let contentWindow = contentWindowGetter.call(this, arguments);
              console.log('getter called:');
              var script = contentWindow.document.createElement('script');
              script.textContent = actualCode;
              contentWindow.document.documentElement.appendChild(script);
              return contentWindow;
          },
      });
    

    This combined with matchOriginAsFallback: true will cause the script to be injected twice in all iFrames that was already hooked. Using matchOriginAsFallback: false combined with the contentWindow proxy will not inject into nested or "dead" iFrames. I solved this by using the first approach and adding an element, if the element does not exist when the contentWindow getter is called I inject the script.