MutationObserver too slow to redefine navigator property in a dynamic iframe?

135 views Asked by At

I'm making some updates to a Chrome extension that re-defines navigator.userAgent, and I came across this behavior when replacing DOMNodeInserted with MutationObserver ..

This question is generic and doesn't require testing from a Chrome extension.

The best way to explain this behavior is to navigate to: https://webbrowsertools.com/useragent/

Then ... using the console run the following javacript code, and check the response from iframe> navigator.userAgent:

DOMNodeInserted works fine, the iframe > navigator.userAgent is switched:

document.addEventListener('DOMNodeInserted', function(event)
{
    if (event.target.tagName == 'IFRAME')
        for (var i=0; i<window.frames.length; i++)
            try { Object.defineProperty(window.frames[i].navigator, 'userAgent', {value:'TEST'}); } catch(e) {}
});

MutationObserver doesn't work, the iframe > navigator.userAgent is NOT switched:

var observer = new MutationObserver(function(mutations)
{
    for (var mutation of mutations)
        for (var item of mutation.addedNodes)
            if (item.tagName == 'IFRAME')
                for (var i=0; i<window.frames.length; i++)
                    try { Object.defineProperty(window.frames[i].navigator, 'userAgent', {value:'TEST'}); } catch(e) {}
});
observer.observe(document, { childList:true, subtree:true });

Excuse the crude iteration through window.frames and not the actual event.target

I believe it has to do with MutationObserver being too slow, but I don't know how to fix this ?!

UPDATE --------------------------------------------------------------

I have tried the suggestions without luck:

https://jsfiddle.net/12t5jx38/

See the script in action on the page:

https://gofile.io/d/9VUosT

First what happens without an injected script, then your suggestions, then the original using DOMNodeInserted ...

This is the result I'm trying to achieve, but without using DOMNodeInserted.

UPDATE 2 ------------------------------------------------------------

https://jsfiddle.net/st0ud9zq/

I believe there's no way to achieve the desired UA patch via MutationObserver.

1

There are 1 answers

16
ibrahim tanyalcin On

Note: check the error within catch due to CORS, you might not have access to contentWindow of iframes

If your scripts register the observer after the iframe is there, MutationObserver won't detect anything. In that case go modify the available frames if you have access to their contentWindow. For those that might be added at some point after your script, MutationObserver should work.

I am not able to reproduce the behavior you had, SO snippet complains about CORS iframes so here is a JSFiddle

Here is the code

const daFrame = document.createElement("iframe"),
      body = document.body,
      obs = new MutationObserver(function(muts){
        muts.forEach(mut => {
          if(mut.type !== "childList"){return}
          mut.addedNodes.forEach(n => {
            if(!(n instanceof HTMLIFrameElement)){return}
            Object.defineProperty(
              n.contentWindow.navigator,
              "userAgent",
              {value: "whateva"}
            )
          })
        })
      });

obs.observe(document.body, {childList: true, subtree: true});

//push this to macrotask to make sure it executes after above
setTimeout(() => document.body.appendChild(daFrame), 0);

setTimeout(() => console.log(`userAgent =`, window.frames[0].navigator.userAgent), 3000);

I used setTimeout to introduce a delay to initiate the MutationObserver incase the DOM node (in this case the iframe) might be inserted before the observer starts.

you can also push a Promise to an outer array and resolve those pushed promises if you do not want to use setTimeout. But that's not the topic of this question.

PS: It looks like the Mutation Observer is registered way after the DOM node is there, and on top you are using a text to dynamically set the content of the script and execute it there. In these cases divide the task in 2 parts, first scan the currently available iframes and do whatever you need to do on them and THEN register the Mutation Observer. Here is a fiddle:

https://jsfiddle.net/ibowankenobi/0bxyejga/

const daFrame = document.createElement("iframe"),
            scriptEl = document.createElement("script");

const scriptContent = `
Array.from(window.frames).forEach(frame => {
    Object.defineProperty(
    frame.navigator,
    "userAgent",
    {value: "whateva"}
  )
});
const body = document.body,
      obs = new MutationObserver(function(muts){
        muts.forEach(mut => {
          if(mut.type !== "childList"){return}
          mut.addedNodes.forEach(n => {
            if(!(n instanceof HTMLIFrameElement)){return}
            Object.defineProperty(
              n.contentWindow.navigator,
              "userAgent",
              {value: "whateva"}
            )
          })
        })
         });
obs.observe(document.body, {childList: true, subtree: true});
`
const srcBlob = new Blob([scriptContent], {type: "text/javascript"}),
            contentURL = URL.createObjectURL(srcBlob);

document.body.appendChild(daFrame);

scriptEl.type = "text/javascript";
scriptEl.onload = async function(){
    console.log("script loaded");
  await new Promise(r => setTimeout(r, 1000));
  Array.from(window.frames).forEach(frame => console.log(`userAgent = `, frame.navigator.userAgent))
}

scriptEl.src = contentURL;

document.head.appendChild(scriptEl);