XMLHttpRequest multi-origine

Les pages Web standards peuvent utiliser l'objet XMLHttpRequest pour envoyer et recevoir des données depuis des serveurs distants, mais elles sont limitées par la règle d'origine identique. Les scripts de contenu lancent des requêtes au nom de l'origine Web dans laquelle ils ont été injectés. Ils sont donc également soumis à la règle d'origine identique. (Les scripts de contenu sont soumis à CORB depuis Chrome 73 et à CORS depuis Chrome 83.) Les origines d'extension ne sont pas aussi limitées : un script s'exécutant dans la page d'arrière-plan ou l'onglet de premier plan d'une extension peut communiquer avec des serveurs distants en dehors de son origine, à condition que l'extension demande des autorisations inter-origines.

Origine de l'extension

Chaque extension en cours d'exécution existe dans sa propre origine de sécurité distincte. Sans demander de privilèges supplémentaires, l'extension peut utiliser XMLHttpRequest pour obtenir des ressources dans son installation. Par exemple, si une extension contient un fichier de configuration JSON nommé config.json dans un dossier config_resources, elle peut récupérer le contenu du fichier comme suit :

var xhr = new XMLHttpRequest();
xhr.onreadystatechange = handleStateChange; // Implemented elsewhere.
xhr.open("GET", chrome.extension.getURL('/config_resources/config.json'), true);
xhr.send();

Si l'extension tente d'utiliser une origine de sécurité autre que la sienne, par exemple https://www.google.com, le navigateur l'interdit, sauf si l'extension a demandé les autorisations inter-origines appropriées.

Demander des autorisations inter-origines

En ajoutant des hôtes ou des modèles de correspondance d'hôte (ou les deux) à la section permissions du fichier manifeste, l'extension peut demander l'accès à des serveurs distants en dehors de son origine.

{
  "name": "My extension",
  ...
  "permissions": [
    "https://www.google.com/"
  ],
  ...
}

Les valeurs d'autorisation inter-origines peuvent être des noms d'hôte complets, comme ceux-ci :

  • "https://www.google.com/"
  • "https://www.gmail.com/"

Ou il peut s'agir de modèles de correspondance, comme ceux-ci :

  • "https://*.google.com/"
  • "https://*/"

Un modèle de correspondance "https://*/" autorise l'accès HTTPS à tous les domaines accessibles. Notez que les modèles de correspondance sont semblables aux modèles de correspondance des scripts de contenu, mais que toutes les informations de chemin d'accès suivant l'hôte sont ignorées.

Notez également que l'accès est accordé à la fois par hôte et par schéma. Si une extension souhaite un accès HTTP sécurisé et non sécurisé à un hôte ou à un ensemble d'hôtes donné, elle doit déclarer les autorisations séparément :

"permissions": [
  "http://www.google.com/",
  "https://www.google.com/"
]

Points à noter concernant la sécurité

Éviter les failles de script intersites

Lorsque vous utilisez des ressources récupérées via XMLHttpRequest, votre page d'arrière-plan doit veiller à ne pas être victime de scripts intersites. Plus précisément, évitez d'utiliser des API dangereuses telles que celles ci-dessous :

var xhr = new XMLHttpRequest();
xhr.open("GET", "https://api.example.com/data.json", true);
xhr.onreadystatechange = function() {
  if (xhr.readyState == 4) {
    // WARNING! Might be evaluating an evil script!
    var resp = eval("(" + xhr.responseText + ")");
    ...
  }
}
xhr.send();
var xhr = new XMLHttpRequest();
xhr.open("GET", "https://api.example.com/data.json", true);
xhr.onreadystatechange = function() {
  if (xhr.readyState == 4) {
    // WARNING! Might be injecting a malicious script!
    document.getElementById("resp").innerHTML = xhr.responseText;
    ...
  }
}
xhr.send();

Privilégiez plutôt les API plus sûres qui n'exécutent pas de scripts :

var xhr = new XMLHttpRequest();
xhr.open("GET", "https://api.example.com/data.json", true);
xhr.onreadystatechange = function() {
  if (xhr.readyState == 4) {
    // JSON.parse does not evaluate the attacker's scripts.
    var resp = JSON.parse(xhr.responseText);
  }
}
xhr.send();
var xhr = new XMLHttpRequest();
xhr.open("GET", "https://api.example.com/data.json", true);
xhr.onreadystatechange = function() {
  if (xhr.readyState == 4) {
    // innerText does not let the attacker inject HTML elements.
    document.getElementById("resp").innerText = xhr.responseText;
  }
}
xhr.send();

Limiter l'accès des scripts de contenu aux requêtes inter-origines

Lorsque vous effectuez des requêtes inter-origines au nom d'un script de contenu, veillez à vous protéger contre les pages Web malveillantes qui pourraient tenter d'usurper l'identité d'un script de contenu. En particulier, n'autorisez pas les scripts de contenu à demander une URL arbitraire.

Prenons l'exemple d'une extension qui effectue une requête multi-origines pour permettre à un script de contenu de découvrir le prix d'un article. Une approche (non sécurisée) consisterait à ce que le script de contenu spécifie la ressource exacte à récupérer par la page d'arrière-plan.

chrome.runtime.onMessage.addListener(
    function(request, sender, sendResponse) {
      if (request.contentScriptQuery == 'fetchUrl') {
        // WARNING: SECURITY PROBLEM - a malicious web page may abuse
        // the message handler to get access to arbitrary cross-origin
        // resources.
        fetch(request.url)
            .then(response => response.text())
            .then(text => sendResponse(text))
            .catch(error => ...)
        return true;  // Will respond asynchronously.
      }
    });
chrome.runtime.sendMessage(
    {contentScriptQuery: 'fetchUrl',
     url: 'https://another-site.com/price-query?itemId=' +
              encodeURIComponent(request.itemId)},
    response => parsePrice(response.text()));

Dans l'approche ci-dessus, le script de contenu peut demander à l'extension de récupérer n'importe quelle URL à laquelle elle a accès. Une page Web malveillante peut être en mesure de falsifier de tels messages et d'inciter l'extension à donner accès à des ressources inter-origines.

Concevez plutôt des gestionnaires de messages qui limitent les ressources pouvant être récupérées. Ci-dessous, seul l'itemId est fourni par le script de contenu, et non l'URL complète.

chrome.runtime.onMessage.addListener(
    function(request, sender, sendResponse) {
      if (request.contentScriptQuery == 'queryPrice') {
        var url = 'https://another-site.com/price-query?itemId=' +
            encodeURIComponent(request.itemId);
        fetch(url)
            .then(response => response.text())
            .then(text => parsePrice(text))
            .then(price => sendResponse(price))
            .catch(error => ...)
        return true;  // Will respond asynchronously.
      }
    });
chrome.runtime.sendMessage(
    {contentScriptQuery: 'queryPrice', itemId: 12345},
    price => ...);

Préférer le HTTPS au HTTP

De plus, soyez particulièrement vigilant quant aux ressources récupérées via HTTP. Si votre extension est utilisée sur un réseau hostile, un attaquant réseau (également appelé "man-in-the-middle") peut modifier la réponse et, potentiellement, attaquer votre extension. Privilégiez plutôt le HTTPS dans la mesure du possible.

Ajuster la règle de sécurité du contenu

Si vous modifiez la règle de sécurité du contenu par défaut Content Security Policy pour les applications ou les extensions en ajoutant un content_security_policy attribut à votre fichier manifeste, vous devez vous assurer que tous les hôtes auxquels vous souhaitez vous connecter sont autorisés. Bien que la règle par défaut ne limite pas les connexions aux hôtes, soyez prudent lorsque vous ajoutez explicitement les directives connect-src ou default-src.