Can a Site Invoke a Browser Extension

Can a site invoke a browser extension?

Since Chrome introduced externally_connectable, this is quite easy to do in Chrome. First, specify the allowed domain in your manifest.json file:

"externally_connectable": {
"matches": ["*://*.example.com/*"]
}

Use chrome.runtime.sendMessage to send a message from the page:

chrome.runtime.sendMessage(editorExtensionId, {openUrlInEditor: url},
function(response) {
// ...
});

Finally, listen in your background page with chrome.runtime.onMessageExternal:

chrome.runtime.onMessageExternal.addListener(
function(request, sender, sendResponse) {
// verify `sender.url`, read `request` object, reply with `sednResponse(...)`...
});

If you don't have access to externally_connectable support, the original answer follows:

I'll answer from a Chrome-centric perspective, although the principles described here (webpage script injections, long-running background scripts, message passing) are applicable to virtually all browser extension frameworks.

From a high level, what you want to do is inject a content script into every web page, which adds an API, accessible to the web page. When the site calls the API, the API triggers the content script to do something, like sending messages to the background page and/or send a result back to the content script, via asynchronous callback.

The main difficulty here is that content scripts which are "injected" into a web page cannot directly alter the JavaScript execution environment of a page. They share the DOM, so events and changes to DOM structure are shared between the content script and the web page, but functions and variables are not shared. Examples:

  • DOM manipulation: If a content script adds a <div> element to a page, that will work as expected. Both content script and page will see the new <div>.

  • Events: If a content script sets up an event listener, e.g., for clicks on an element, the listener will successfully fire when the event occurs. If the page sets up a listener for custom events fired from the content script, they will be successfully received when the content script fires those events.

  • Functions: If the content script defines a new global function foo() (as you might try when setting up a new API). The page cannot see or execute foo, because foo exists only in the content script's execution environment, not in the page's environment.

So, how can you set up a proper API? The answer comes in many steps:

  1. At a low-level, make your API event-based. The web page fires custom DOM events with dispatchEvent, and the content scripts listens for them with addEventListener, taking action when they are received. Here's a simple event-based storage API which a web page can use to have the extension to store data for it:

    content_script.js (in your extension):

    // an object used to store things passed in from the API
    internalStorage = {};

    // listen for myStoreEvent fired from the page with key/value pair data
    document.addEventListener('myStoreEvent', function(event) {
    var dataFromPage = event.detail;
    internalStorage[dataFromPage.key] = dataFromPage.value
    });

    Non-extension web page, using your event-based API:

    function sendDataToExtension(key, value) {
    var dataObj = {"key":key, "value":value};
    var storeEvent = new CustomEvent('myStoreEvent', {"detail":dataObj});
    document.dispatchEvent(storeEvent);
    }
    sendDataToExtension("hello", "world");

    As you can see, the ordinary web page is firing events that the content script can see and react to, because they share the DOM. The events have data attached, added in the CustomEvent constructor. My example here is pitifully simple -- you can obviously do much more in your content script once it has the data from the page (most likely pass it to the background page for further processing).

  2. However, this is only half the battle. In my example above, the ordinary web page had to create sendDataToExtension itself. Creating and firing custom events is quite verbose (my code takes up 3 lines and is relatively brief). You don't want to force a site to write arcane event-firing code just to use your API. The solution is a bit of a nasty hack: append a <script> tag to your shared DOM which adds the event-firing code to the main page's execution environment.

    Inside content_script.js:

    // inject a script from the extension's files
    // into the execution environment of the main page
    var s = document.createElement('script');
    s.src = chrome.extension.getURL("myapi.js");
    document.documentElement.appendChild(s);

    Any functions that are defined in myapi.js will become accessible to the main page. (If you are using "manifest_version":2, you'll need to include myapi.js in your manifest's list of web_accessible_resources).

    myapi.js:

    function sendDataToExtension(key, value) {
    var dataObj = {"key":key, "value":value};
    var storeEvent = new CustomEvent('myStoreEvent', {"detail":dataObj});
    document.dispatchEvent(storeEvent);
    }

    Now the plain web page can simply do:

    sendDataToExtension("hello", "world");
  3. There is one further wrinkle to our API process: the myapi.js script will not be available exactly at load time. Instead, it will be loaded some time after page-load time. Therefore, the plain web page needs to know when it can safely call your API. You can solve this by having myapi.js fire an "API ready" event, which your page listens for.

    myapi.js:

    function sendDataToExtension(key, value) {
    // as above
    }

    // since this script is running, myapi.js has loaded, so let the page know
    var customAPILoaded = new CustomEvent('customAPILoaded');
    document.dispatchEvent(customAPILoaded);

    Plain web page using API:

    document.addEventListener('customAPILoaded', function() {
    sendDataToExtension("hello", "world");
    // all API interaction goes in here, now that the API is loaded...
    });
  4. Another solution to the problem of script availability at load time is setting run_at property of content script in manifest to "document_start" like this:

    manifest.json:

        "content_scripts": [
    {
    "matches": ["https://example.com/*"],
    "js": [
    "myapi.js"
    ],
    "run_at": "document_start"
    }
    ],

    Excerpt from docs:

    In the case of "document_start", the files are injected after any files from css, but before any other DOM is constructed or any other script is run.

    For some contentscripts that could be more appropriate and of less effort than having "API loaded" event.

  5. In order to send results back to the page, you need to provide an asynchronous callback function. There is no way to synchronously return a result from your API, because event firing/listening is inherently asynchronous (i.e., your site-side API function terminates before the content script ever gets the event with the API request).

    myapi.js:

    function getDataFromExtension(key, callback) {
    var reqId = Math.random().toString(); // unique ID for this request
    var dataObj = {"key":key, "reqId":reqId};
    var fetchEvent = new CustomEvent('myFetchEvent', {"detail":dataObj});
    document.dispatchEvent(fetchEvent);

    // get ready for a reply from the content script
    document.addEventListener('fetchResponse', function respListener(event) {
    var data = event.detail;

    // check if this response is for this request
    if(data.reqId == reqId) {
    callback(data.value);
    document.removeEventListener('fetchResponse', respListener);
    }
    }
    }

    content_script.js (in your extension):

    // listen for myFetchEvent fired from the page with key
    // then fire a fetchResponse event with the reply
    document.addEventListener('myStoreEvent', function(event) {
    var dataFromPage = event.detail;
    var responseData = {"value":internalStorage[dataFromPage.key], "reqId":data.reqId};
    var fetchResponse = new CustomEvent('fetchResponse', {"detail":responseData});
    document.dispatchEvent(fetchResponse);
    });

    ordinary web page:

    document.addEventListener('customAPILoaded', function() {
    getDataFromExtension("hello", function(val) {
    alert("extension says " + val);
    });
    });

    The reqId is necessary in case you have multiple requests out at once, so that they don't read the wrong responses.

And I think that's everything! So, not for the faint of heart, and possibly not worth it, when you consider that other extensions can also bind listeners to your events to eavesdrop on how a page is using your API. I only know all this because I made made a proof-of-concept cryptography API for a school project (and subsequently learned the major security pitfalls associated with it).

In sum: A content script can listen for custom events from an ordinary web page, and the script can also inject a script file with functions that makes it easier for web pages to fire those events. The content script can pass messages to a background page, which then stores, transforms, or transmits data from the message.

Trigger/invoke a Chrome extension from a web page

You can inject your content-script to every page (register it in extension manifest) and alter the page html to add your button or a with your custom id.

The execution environment example explains it pretty good how you will trigger an event from the page to your content script. After you manage the trigger you can do anything you want as the extension logic.

Also keep in mind that this will require your extension's content-script to be injected to every page the user visits. It is not possible to actually trigger the execution of your content-script from the page if thats what you were asking.

Convert browser extension to regular website

Following wOxxOm's helpful comments, I realised some API calls can be intercepted using a shim or polyfill. This shim should re-implement the functions without making use of extension-specific API calls.

I created a simple Git repository containing such a Javascript shim.

Can a website block a Chrome Extension?

For the short Answer to the question goto the 4th Edit:

You need to know the extensionId from the Extension you want to block, so that it works.

Here is a Testsite from the Prove of Concept
Testsite

and here is the information behind the Solution:
Intro to Chrome addons hacking: fingerprinting

Now that you know what Extensions are Running you can, redirect/block/...

I hope it helps.

Edit:

Tested with (Chrome Version 27.0.1453.94) on Windows XP

Edit 2:

This technique will only work if:

  1. You know the extensionid :)
  2. IMPORTANT! at least one Ressource(like the manifest.json, some image, script, ...)
    is set as "web_accessible_resources" (in the manifest) OR the
    extension still uses a manifest version 1 and has no "web_accessible_resources" set. (Ressource from chrome dev site Link)

Edit 3:

Case Extension: JSONView

You could detect the extension with this code(only example code):

<script src="chrome-extension://chklaanhfefbnpoihckbnefhakgolnmc/error.gif" onerror="console.info('Extension Not Found')" onload="console.info('Extension Found')"></script>
<!-- since the the file error.gif is allowed in the manifest "web_accessible_resources" (any other file mentioned there would also be fine) -->
<!-- the block code should come in the onload of the script tag -->
<!-- tested with Chrome 27+ WinXp -->

Some Context:
The JSONView Extension has a version 2 Manifest:

...
"manifest_version": 2,
"name": "JSONView",
...

so by default you cannot access the manifest file as mentioned in the "Prove of Concept" above.

BUT it uses the "web_accessible_resources" attribute in the Manifest, which allows websites to access files from the Extension.

...
"web_accessible_resources": [ "jsonview.css", "jsonview-core.css", "content_error.css", "options.png", "close_icon.gif", "error.gif" ]
...

So now you can call any of this files from your webpage.

example:

chrome-extension://chklaanhfefbnpoihckbnefhakgolnmc/error.gif
chrome-extension://chklaanhfefbnpoihckbnefhakgolnmc/jsonview.css
...

And with this url in an Image/Script/.. -Tag you can know if the extension is there, if the onload Event fires.

P.s.: i only tested this with Chrome Version 27.0.1453.94) on Windows XP, in other Versions it might not work. (see comment from T.J. Crowder)

P.P.s.: For More Details check the Chrome Developer Ressources. Here is the Link to the Extension on the Chrome Ressource Page "Finger printing" Stuff)

Edit 4:

I don't think it can be blocked per se, but if you can detect the extension as mentioned above you could:

  • redirect away from your Page
  • or Popup a message(every few seconds) saying, "disable the extension for this Site"
  • or you could check the Extension code to see if you maybe could "break" or hinder its functionality.
  • or you could use some Code like in the answer of BeardFist


Related Topics



Leave a reply



Submit