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..
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.