Search code examples
javascriptsecuritygoogle-chromegoogle-chrome-extensionpostmessage

How to do secure communication from a(ny) web page to a Chrome extension


I want to have secure communication between a web page to a Chrome extension. After quite some checking and hacking this seems to at least be non-trivial, if not outright impossible.

I want to message an extension's background page (while it is running) from (some JavaScript) in a webpage (note that the webpage initiates communication, which is pretty untypical!). I send the extension some credentials (hence the need for a secured channel!) and then if these credentials are valid (as determined by logic IN the background page) then the background page should return some (JSON) values, which the web page can then display.

I already have some code in place that checks whether the current browser is Chrome and whether the user has installed said extension (and some UI for him/her to install it if it is not, etc...)

I also got some communication running using window.postMessage. It posts a message to itself from the webpage script to the extensions content script, and that is then propagated to the background page from there. However that is NOT secure as this MDN page quotes:

.. it is recommended that postMessage not be used to communicate with chrome: pages for now; use a different method (such as a query string when the window is opened) to communicate with Chrome windows.

(this comment is in the section Using window.postMessage in extensions near the bottom of the page)

This seems like sound advise, as there seems no way to exclude the possibility that data send using postMessage can be intercepted by another window/tab in the browser, instead of getting to the extension. So this vulnerability could be exploited.

Using a query string as the MDN article suggests is not possible in my situation since the background page is loaded at the start (no URL params present yet).

Is my reasoning sound/clear? Is there any Chrome extension expert who has a suggestion?

Note: A suggestion is to use externally_connectable docs but as specified in the docs the matchessection for specifying websites only allows subdomain wildcards, and the problem domain requires allowing a dynamic list of websites, which cannot be hardcoded in the extension's manifest.


Solution

  • You need to use "externally_connectable" method of communication.

    If your extension declares that it wants to communicate with a certain domain (that you need to list in the manifest), then chrome.runtime.sendMessage will be exposed to the page context.

    Then, on your page, you can pass the information in this manner:

    var extensionID = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
    
    if(chrome && chrome.runtime && chrome.runtime.sendMessage) {
      // There is an extension that declared this page in externally_connectable
      chrome.runtime.sendMessage(extensionID, data);
    } else {
      // Either this is not Chrome, or no extension wants to listen
    }
    

    On the extension side, you will get the data in the chrome.runtime.onMessageExternal event.

    You may, if you wish, establish a two-way long-lived connection with chrome.runtime.connect.

    Chrome will make sure only the extension with a given ID will get your message. This will protect you from eavesdropping from other web pages, but do note that any extension can have enough power to attach to / modify your page's scripts and get that data.


    Since the question was updated with "no fixed list of sites" requirement, there is an alternative solution. A content script can communicate with the page script using DOM events. They won't leak into other pages, but again, that does not protect against other extensions.

    See this answer for more details; I'll leave a code snippet here as an example.

    // Content script
    //Listen for the event
    window.addEventListener("PassToBackground", function(evt) {
      chrome.runtime.sendMessage(evt.detail);
    }, false);
    
    // Page context
    var message = {/* whatever */};
    var event = new CustomEvent("PassToBackground", {detail: message});
    window.dispatchEvent(event);