18

The problem

I've had this issue for months now, but the concept is pretty straightforward: I want to block some Malicious Site™ from programmatically opening tabs or popup windows.

With the chrome.tabs API, I can listen with onCreated when a new tab is created, and I can easily check who (i.e. which tab) opened that particular tab accessing the openerTabId property of the Tab object passed to the callback function.

Now, I would like to do the exact same thing when a new window is created: I would like to know which tab opened the window (if any, because it could have been opened by the user too), check its URL to see if it is the Malicious Site™, and act accordingly (i.e. block the popup). I tried doing it in the exact same way: request the array of tabs in the new window and check their openerTabId property, but unfortunately such property is not defined! I searched the documentation and Googled for hours, but sadly it looks like there's no simple way to check who opened a window.

A very clumsy solution

Stated the above, the only way I was able to do something even remotely close to what I really want, is the following:

  1. Every time a new window is created, its ID is added to an array called windowWatchlist.
  2. Every time a tab is updated (NB: updated, not created), a script is injected inside it to check its document.referrer, which should contain the URL of the site which opened the tab: if the referrer URL contains the address of the Malicious Site™ I want to block popups from, the window is then closed and removed from the windowWatchlist.
  3. Every time a window is closed, if its ID is in the windowWatchlist, it gets removed from it.

Here's the code (which runs in my background.js script):

// Called on chrome.windows.onCreated
function watchPopupWindow(window) {
    windowWatchlist.push(window.id);
    console.log('Added window #' + window.id + ' to watchlist.');
}

// Called on chrome.windows.onRemoved
function unwatchPopupWindow(windowID) {
    var index = windowWatchlist.indexOf(windowID);

    // If the windowID is in the watchlist:
    if (index != -1) {
        // Remove it:
        windowWatchlist.splice(index, 1);
        console.log('Removed window #' + windowID + ' from watchlist.');
    }
}

// Called on chrome.tabs.onUpdated
function blockPopupWindow(tabID, info, tab) {
    // If this tab is in a window which is in the watchlist:
    if (windowWatchlist.indexOf(tab.windowId) != -1 && info.url && info.url != 'about:blank') {
        // Check the referrer of this tab:
        chrome.tabs.executeScript(tabID, {code: 'document.referrer;'}, function(ref) {
            // If the referrer is the malicious site to block:
            if (ref && ref[0] && ref[0].indexOf("http://MALICIOUS-SITE.XXX") != -1) {
                // Close the popup window:
                chrome.windows.remove(tab.windowId, function() {
                    console.log('Blocked popup window #' + tab.windowId + '.');

                    if (chrome.runtime.lastError)
                        console.error(chrome.runtime.lastError.message);
                });;
            }
        });
    }
}

var windowWatchlist = [];

chrome.windows.onCreated.addListener(watchPopupWindow, {windowTypes: ['popup']});
chrome.windows.onRemoved.addListener(unwatchPopupWindow, {windowTypes: ['popup']});
chrome.tabs.onUpdated.addListener(blockPopupWindow);

Now, you may be wondering: why do you need all this mess only to check a referrer? Couldn't you just check the tabs contained in the window when the window is opened and check their referrer directly inside the callback of chrome.window.onCreated? That's a clever question, and the answer is simple: the problem is that I cannot check the referrer of the tabs right when they are created, because they almost always need some time to load, and the referrer isn't loaded until the page starts loading inside the tab. Therefore, I need to check when a tab is updated, see if its window is in my watchlist, and then check its referrer. This is why chrome.tabs.onUpdated is needed, since it fires its listeners whenever a tab changes state (e.g. tab.status changes from "loading" to "complete").

Why this solution doesn't work

The reason why I call this solution "clumsy" and the reason why it doesn't really work should be already clear to anyone with some experience of JavaScript and web developing: document.referrer isn't reliable at all, and is very often undefined or (in case of multiple redirects) not the right one. This makes my script fail about 90% of the times, because it is unable to determine whether the popup window was opened by the Malicious Site™ or not.

Moreover, the Malicious Site™ often opens popups with URL about:blank or no URL at all, and only when they are loaded, injects data into them, making them basically impossible to detect, even with chrome.tabs.onUpdated which doesn't fire any listener in this situation.

I could decide to block any popup with URL about:blank or undefined, and this is what I'm doing right now indeed, but is a pretty bad compromise, since that I end up closing popups opened by any site which uses this method, and not only the Malicious Site™ I want to block.

In conclusion

My question is simple, but I don't know about its solution: does anyone know any other more reliable method which could be used to detect which tab opened a new window? Nothing comes to my mind, maybe something could be possible using the chrome.webRequest API? I don't really know. For months I've been accepting the fact that a simple solution just wasn't possible, and helplessly waited for an update or something, but I never actually thought about asking here, because the problem looked above the competence of an average Chrome Extension programmer, but hopefully I was wrong.


UPDATE: The solution to inject a script inside the site and replace the window.open function with something else isn't viable: if an <iframe> is loaded without a src attribute, but with an already written DOM inside the srcdoc attribute, Chrome will not execute a content script inside it, even if the call to chrome.tabs.executeScript is made with allFrames: true, and even if the content script is declared inside the extension's manifest.

Marco Bonelli
  • 63,369
  • 21
  • 118
  • 128
  • In `windows.onCreated`, you get the list of tabs in the new window; don't they have an `openerTabId` set? – Xan Sep 25 '17 at 14:06
  • If it's a site, I usually insert a script element from a content script that runs on `document_start` (i.e. before the site scripts) and spoof `window.open` with a dummy function. – wOxxOm Sep 25 '17 at 14:07
  • @Xan forgot to add it to the question, no `openerTabId` is set on the tabs of the new window, sadly. – Marco Bonelli Sep 25 '17 at 14:12
  • @wOxxOm that's a clever solution... I thought about it but I actually didn't try it. Replacing the open method with a closure that checks what's going on could be a valid option. I was trying not to inject anything in the site, but I'll probably have to if I don't find any other solution. – Marco Bonelli Sep 25 '17 at 14:16
  • UPDATE: Sadly it looks like is not possible to use the approach suggested by @wOxxOm, since that iframes that are loaded without a `src="..."` attribute and with a `srcdoc="..."` attribute (containing the DOM) are not affected by `chrome.tabs.executeScript`, even with the option `allFrames: true`. Sites that use this kind of frames can do whatever they want inside the DOM of such frames, and an extension is not able to touch the code or make anything before it gets executed, so if some Malicious Site™ puts a `window.open` inside an ` – Marco Bonelli Sep 25 '17 at 22:45
  • Does `"match_about_blank": true` make any difference? – wOxxOm Sep 25 '17 at 23:05
  • Also, you need a content script declared in manifest.json with "run_at": "document_start" or declarativeContent with RequestContentScript, not executeScript, which is too late even if you specify runAt: 'document_start'. – wOxxOm Sep 25 '17 at 23:07
  • @wOxxOm already tried declaring the content script inside the manifest with `"run_at": "document_start"` without any luck: it gets injected in every frame except frames without `src` which use `srcdoc="..."`. Can you elaborate on `declarativeContent`? Never used this API before. – Marco Bonelli Sep 25 '17 at 23:35
  • declarativeContent won't help if match_about_blank makes no difference. – wOxxOm Sep 26 '17 at 08:05
  • One question: do you want the malicious site to be still fully functional and totally unaltered, but just not allowed to open new windows? – artur grzesiak Sep 30 '17 at 12:52
  • @arturgrzesiak Yes, that's *basically* what I would like to accomplish. – Marco Bonelli Sep 30 '17 at 13:07
  • one remark: overwriting `window.open` from a content script is probably not an option in your case as extension scrips are executed in a different context from page scripts and their `window` objects are different. – artur grzesiak Sep 30 '17 at 13:24
  • @arturgrzesiak no, that is not the reason at all. It is extremely easy to [inject a script inside the context of the page](https://stackoverflow.com/questions/9515704). I already explained why `window.open` is not an option. – Marco Bonelli Sep 30 '17 at 13:29
  • yes you can inject a script, but you cannot change execution context of the page scripts, so running `window.open = () => {}` in your content script will not change `window.open` accessible from page scripts. https://developer.chrome.com/extensions/content_scripts#execution-environment `It's worth noting what happens with JavaScript objects that are shared by the page and the extension - for example, the window.onload event. Each isolated world sees its own version of the object. Assigning to the object affects your independent copy of the object.` – artur grzesiak Sep 30 '17 at 13:40
  • yes, you are right - sincerely thought it was impossible sorry for confusion. Have no time to test it myself right now, but one thing you could try is to set `sandbox` attribute on the iframes - especially not to `allow-popups` as described here: https://developer.mozilla.org/pl/docs/Web/HTML/Element/iframe. – artur grzesiak Sep 30 '17 at 15:18
  • @arturgrzesiak I think this *could* be impossible because of not being able to "catch" the frame and add the attribute, but is definitely something to try, thank you for the link, I'll see if this can help. – Marco Bonelli Sep 30 '17 at 15:22
  • Is the purpose of the extension to block specific sites? – guest271314 Oct 04 '17 at 01:52
  • @guest271314 more or less, yes. – Marco Bonelli Oct 04 '17 at 05:09
  • You can use `PerformanceObserver` at each `window` where extension is loaded to check each request at that `window` [Detect ajax requests from raw HTML](https://stackoverflow.com/questions/45406906/detect-ajax-requests-from-raw-html), though to achieve requirement for browser itself the user can set one or more `Policies` [chrome Pop-up blocker when to re-check after allowing page](https://stackoverflow.com/questions/40282861/chrome-pop-up-blocker-when-to-re-check-after-allowing-page/) – guest271314 Oct 04 '17 at 05:14
  • @guest271314 could you elaborate on this? Maybe with an answer if you think this could work. How can I set policies with an extension? Do I need an app? I don't know much about this, but it looks promising... – Marco Bonelli Oct 04 '17 at 09:50
  • It is probably beyond the scope of an extension to set `Policies`. – guest271314 Oct 04 '17 at 23:41
  • @guest271314 yes, that's probably true, but even without that, I tried setting policies to block popups and I verified they were set in the chrome://policy tab, **but the damn Malicious Site™ managed to open popups anyway**... maybe because they were opened by an event listener? I don't know. I really don't. I'm doomed, this is just hopeless. – Marco Bonelli Oct 05 '17 at 06:20
  • @MarcoBonelli Can you reproduce at plnkr https://plnkr.co? Can you include the policies that you tried at Question? Have you read the code used to open the `window`? Did you create a policy for all of the domains used by the code? – guest271314 Oct 05 '17 at 06:33
  • @guest271314 I don't see how plnkr could be useful here, it's just an HTML/JS/CSS fiddler. About the opening of the popups, the problem is that I don't actually know which function calls the `open` method. The site I tested it with is `www.flashx.to`, any page like [this one](https://www.flashx.to/6yduxk1pw6u9.html) opens a bunch of popups when clicking basically anywhere on the page and even injecting the code to remove the `open` function in all the frames it still manages to open them. I added that site to the `PopupsBlockedForUrls` policy, but it didn't work. – Marco Bonelli Oct 05 '17 at 06:45
  • Did you list all twenty-six requested JavaScript files at `Policies`? – guest271314 Oct 05 '17 at 06:54
  • @guest271314 no I didn't: that would nullify my efforts to "block" some site, since that I cannot know which files the site is using (and from where), so if the Policy only blocks popups explicitly generated by the url of the frame (and not the top frame)... well... that's an useless policy. – Marco Bonelli Oct 05 '17 at 08:06
  • _"no I didn't: that would nullify my efforts to "block" some site"_ If you are visiting a site which makes numerous requests, including cross domain requests, that is exactly the policy which should be implemented; that is specifically making a policy for each domain where pop ups should be blocked. You cannot expect a policy for a single URL to apply to multiple URLs – guest271314 Oct 06 '17 at 01:57
  • @guest271314 yes, I know that's what the policy is for, you're right, but you also understand that: 1) policies aren't that easy to deal with, not for an extension; 2) I would need to add every single frame URL to the policies every time I open a page of the Malicious Site™, and remove them when I close it, because I don't want to block those URLs on every single site. Policies are what they are, but for the purpose of my extension they are useless. – Marco Bonelli Oct 06 '17 at 08:45

2 Answers2

3

I came across the same problem and found the webNavigation.onCreatedNavigationTarget event that yields the source tab/frame id when a new window is opened.

Solution found from this post: Is it possible to determine a tab's opener within a Google Chrome extension?

MRousse
  • 526
  • 1
  • 4
  • 9
  • If this is true you're my savior. I'm gonna check this later today, thanks for the answer! – Marco Bonelli Jun 04 '18 at 10:00
  • 1
    Well, turns out this actually works quite well. It's really strange to find this answer only now since this method has been around for **seven years**. I personally don't know how I never came across that answer during my endless searches. The fact that something like this has to be detected through the `webNavigation` API is really strange, but whatever, it works. Thanks again! – Marco Bonelli Jun 04 '18 at 14:49
1

Since you are already doing code injection this is what I would do.

Inject code to override window.open and have it window.postMessage to child window telling them who opened them. Also will need to inject code to listen to the effect of window.addEventListener('message', messageHandler) which will decided if they should window.close().

On second though I think I would just override window.open and not even open the child windows if you don't want to allow a give site to open windows.

Xan
  • 74,770
  • 16
  • 179
  • 206
Michael Hobbs
  • 1,663
  • 1
  • 15
  • 26
  • Thanks for the answer, I'll test it out. – Marco Bonelli Sep 25 '17 at 15:27
  • 1
    Sadly it looks like is not possible to use this approach, since that iframes that are loaded without a src="..." attribute and with a srcdoc="..." attribute (containing the DOM) are not affected by `chrome.tabs.executeScript`, even with the option `allFrames: true`. Sites that use this kind of frames can do whatever they want inside the DOM of such frames, and an extension is not able to touch the code or make anything before it gets executed, so if some Malicious Site™ puts a `window.open` inside an ` – Marco Bonelli Sep 25 '17 at 22:44
  • 1
    As a sidenote for people who think this is a valid answer and in case it wasn't clear from my comment: it is not. This is just wat @wOxxOm suggested in the comments of my question, just re-formatted and a little bit clearer. – Marco Bonelli Sep 30 '17 at 13:09