How can I identy which iframe node is running which content script?

251 views
Skip to first unread message

Juraj M.

unread,
Sep 15, 2025, 4:01:11 AMSep 15
to Chromium Extensions
Imagine you have a code A that creates "iframe" node, and at the same time code B creates similar "iframe" node.

Let's say both iframes loads the same 3rd party website.
And both iframes have the same content script running inside.

Now, let's say I want to send a unique message to each iframe, how do I do that?

What I'm using now, is setting a unique "name" attribute on the "iframe" node, and then in the content script I can read the name with "window.name".
This way when the content script receives a message, it can check whether the name is matching and process the message only if it does.

However, I'm facing now a page that resets the "window.name" to an empty string.

NOTE: 
I see the rubber duck effect works as expected because as I was writing this, I came up with two new workarounds :).
1. I could load them synchronously, then each content script would send a message (or open a port) and I could then access the "sender.frameId". But I'm not happy about loosing the async load of multiple iframes.

2. I could use the "window.postMessage", which I'm always avoiding due to security, but I guess it's fine if I send it only some ID.

Now that I think about it even more, all these solutions are terrible/spaghetti.
This feels like a gap in the API.
Extensions should be able to assign a frameId to iframe node, especially when the iframe has the content script running.
And ideally without the "webNavigation" permission.
What do you think?

woxxom

unread,
Sep 16, 2025, 1:54:07 AMSep 16
to Chromium Extensions, Juraj M.
For a reliable identification you would create the iframe sequentially, waiting for each one to run the content script in the iframe (to speed it up you can use document_start), that content script sends a message at its start using chrome.runtime.sendMessage with a random id that will be remembered for this frame. The message will be received by the background script, which will then re-send it to the main document in the tab: chrome.tabs.sendMessage(sender.tab.id, msg, {frameId: 0})

A fragile workaround is to use web platform messaging which can be seen by the page or any other extension: iframeElem.postMessage('foo', '*') and window.addEventListener('message', ....) in the content script, but it won't know if the message is authentic.

woxxom

unread,
Sep 16, 2025, 1:55:53 AMSep 16
to Chromium Extensions, woxxom, Juraj M.
...BTW you can also use sender.frameId directly in the background script: chrome.tabs.sendMessage(sender.tab.id, msg, {frameId: sender.frameId})

Juraj M.

unread,
Sep 16, 2025, 4:51:51 AMSep 16
to Chromium Extensions, woxxom, Juraj M.
Thanks woxxom, 

The postMessage actually sounds fine, something like this seems to work:
iframe.contentWindow.postMessage({type: 'name', yourName: 'some_random_id'}, '*');

BUT, with that asterisk it's almost guaranteed to be rejected by Firefox reviewer, it happened to me before some years ago when I didn't know what it does.
I could argue that the "data leak" is only a random ID, but I guess the reviewer could argue that the malicious page can now detect that user has your extension installed which surely breaks some privacy policy...
UPDATE: But wait, I don't have to use the star, I can target the iframe origin and my script can still receive it!

The sequential load sounds OK, but it means I need need to change my iframe creation, which won't be trivial.
Plus this brings huge complexity and makes the code flow totally unreadable. And I don't even want to think about edge cases like page load timeouts, errors or redirects.

If I may have a minute more of your time, what would the "best" solution look like? (if I wanted to raise this in w3c repository)
For example, imagine this code:
const {resolve, promise} = Promise.withResolvers();
const iframe = Object.assign(document.createElement('iframe'), {
  src: 'about:blank',
  onload: resolve,
});
document.body.appendChild(iframe);
await promise;
// At this point, I want to send it a message, but how do I target this specific iframe? I need it's "frameId"!
// Maybe something like this?
const frameId = await browser.webNavigation.getFrameByElement(iframe);

Although I would love to avoid "webNavigation" permission, but I can't of a better namespace where this would fit better.
But anyway, you see how nice and readable this code is? :)

woxxom

unread,
Sep 16, 2025, 10:17:47 AMSep 16
to Chromium Extensions, Juraj M., woxxom
Looks like you add the iframes inside some of your extension page, while I incorrectly assumed you're doing it inside a web page where the main content script adds iframes.

The solution is simpler then, because you can use a webRequest.onBeforeRequest listener which means you don't have to wait for one iframe to completely load, the sequential delay will be usually just a millisecond or so. The webRequest permission doesn't require any user confirmation.

async function addFrame(url) {
  const fp = Promise.withResolvers();
  const wrp = Promise.withResolvers();

  const iframe = Object.assign(document.createElement('iframe'), {
    src: url, // some real URL, not about:blank
    onload: fp.resolve,
    onerror: fp.reject,
  });
  const onReq = e => {
    if (url === e.url && (
        location.origin === e.initiator ||
        location.href === e.originUrl
    )) wrp.resolve(e.frameId);
  };
  browser.webRequest.onBeforeRequest.addListener(onReq, {
    types: ['sub_frame'],
    urls: [url],
  });
  document.body.appendChild(iframe);
  let frameId;
  try {
    frameId = await Promise.all([wrp.promise, fp.promise]);
  } finally {
    browser.webRequest.onBeforeRequest.removeListener(onReq);
  }
  return frameId;
}


As for contentWindow.postMessage note that any other script in the iframe can fake it, although maybe Firefox shows the extension id of the sender now somewhere in the event parameter, I haven't checked and anyway, Chrome doesn't.

woxxom

unread,
Sep 16, 2025, 10:19:26 AMSep 16
to Chromium Extensions, woxxom, Juraj M.
correction:

  return frameId[0];

woxxom

unread,
Sep 16, 2025, 11:57:47 PMSep 16
to Chromium Extensions, woxxom, Juraj M.
Another caveat is that you'll most likely have to wait for the listener to be registered in the browser process first:

  browser.webRequest.onBeforeRequest.addListener(.........);
  // making a roundtrip to the browser process to ensure the listener is registered
  await browser.runtime.getPlatformInfo();
  document.body.appendChild(iframe);

Juraj M.

unread,
Sep 17, 2025, 5:36:35 AMSep 17
to Chromium Extensions, woxxom, Juraj M.
Wait a second...
You've mentioned a "roundtrip to the browser process", this sounds like something I should know about more!
Because there are cases exactly like this, that I need to wait for something to happen, but there is nothing obvious to await, and using "setTimeout" is killing me.
For example this one:

Do you know if this trick (getPlatformInfo) will work also in Firefox?
And is there a similar trick I could use in code running in content scripts? (where getPlatformInfo doesn't exists)

OK back to the original issue...
Using "webRequest" is amazing idea! I can't believe I didn't think of that. Thanks a lot!
I'm actually already using it in my "NetworkIdleMonitor" which helps me wait for the iframe to fully load (this is in my other extension, but I have there the same issue with identifying iframes).

In my original extension I've implemented the postMessage workaround:
const message: IframeSetNameMessage = {type: 'setIframeName', iframeName: this.iframeName};
iframe.contentWindow!.postMessage(message, new URL(this.url).origin);

And then in the content script, I can check the "origin" property to match my extension origin, this should make it pretty safe:
// parent window will send us unique random "name", this will allow us to identify messages from/to this iframe
export const iframeNamePromise = new Promise<string>((resolve) => {
  window.addEventListener('message', e => {
    const data = e.data as IframeSetNameMessage | undefined;
    if (
      e.source === window.parent &&
      e.origin === browser.runtime.getURL('').slice(0, -1) && // only react to messages from THIS extension origin
      data?.type === 'setIframeName'
    ) {
      resolve(data.iframeName);
    }
  });
});

This seems to work OK in both Chrome / Firefox.
In this extension the iframes are actually created in a loop by Vue framework, so making it sequential (yet async) is a bit tricky so I'm OK with the post message workaround.
In my other extension I'll use the webRequest API. 
Message has been deleted

woxxom

unread,
Sep 18, 2025, 2:24:14 AMSep 18
to Chromium Extensions, Juraj M., woxxom
> (getPlatformInfo) will work also in Firefox?

Probably yes, because the architecture should be similar.

> in content scripts? (where getPlatformInfo doesn't exists)

Maybe chrome.storage.local.get('') or chrome.i18n.detectLanguage('')

David Amber “WebDUH LLC” Weatherspoon

unread,
Sep 22, 2025, 11:29:52 AM (14 days ago) Sep 22
to Chromium Extensions, woxxom, Juraj M.
I think you all are discussing this in the wrong place. You might be looking to do something better served using the VPR Very Persitant Framework - to send messages from the child frame of a container to a parent container - create a pesistant error that causes a crash that is easily overcome by calling the wrong or a non exisant model in an api call that temp crashes an app like bolt.new and then create an auto error triage that when it crashes fixes the specific error until the next crash occurs - that will allow the parent -> child communication you want and replace the api call to a real model when you doN'T Want the lotl crash to continue.
Reply all
Reply to author
Forward
0 new messages