Requêtes réseau multi-origines

Les pages Web standards peuvent utiliser les API fetch() ou 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 politique d'origine identique. Les origines des extensions ne sont pas aussi limitées. Un script s'exécutant dans un service worker d'extension ou un onglet de premier plan peut communiquer avec des serveurs distants en dehors de son origine, à condition que l'extension demande des autorisations d'hôte.

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 appeler fetch() pour obtenir des ressources dans son installation. Par exemple, si une extension contient un fichier de configuration JSON appelé config.json dans un dossier config_resources/, elle peut récupérer le contenu du fichier comme suit :

const response = await fetch('/config_resources/config.json');
const jsonData = await response.json();

Si l'extension tente de demander du contenu à partir d'une origine de sécurité autre que la sienne, par exemple https://www.google.com, cela sera traité comme une requête multi-origines, sauf si l'extension dispose d'autorisations d'hôte. Les requêtes d'origine croisée sont toujours traitées comme telles dans les scripts de contenu, même si l'extension dispose d'autorisations d'hôte.

Demander des autorisations d'origine croisée

Pour demander l'accès à des serveurs distants en dehors de l'origine d'une extension, ajoutez des hôtes, des modèles de correspondance ou les deux à la section host_permissions du fichier manifest.

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

Les valeurs d'autorisation d'origine croisée peuvent être des noms d'hôte complets, comme ceux-ci :

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

Il peut également s'agir de modèles de correspondance, comme ceux-ci :

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

Un format de correspondance "https://*/" autorise l'accès HTTPS à tous les domaines accessibles. Notez que les formats d'identification sont ici semblables aux formats d'identification des scripts de contenu, mais que toute information de chemin d'accès suivant l'hôte est ignorée.

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

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

Fetch() vs XMLHttpRequest()

fetch() a été créé spécifiquement pour les service workers et s'inscrit dans une tendance Web plus large qui s'éloigne des opérations synchrones. L'API XMLHttpRequest() est compatible avec les extensions en dehors du service worker, et son appel déclenche le gestionnaire de récupération du service worker de l'extension. Dans la mesure du possible, les nouveaux travaux doivent privilégier fetch().

Considérations de sécurité

Éviter les failles de script intersites

Lorsque vous utilisez des ressources récupérées via fetch(), votre document hors écran, votre panneau latéral ou votre pop-up doivent veiller à ne pas être victimes de script intersite. Plus précisément, évitez d'utiliser des API dangereuses telles que innerHTML. Exemple :

const response = await fetch("https://api.example.com/data.json");
const jsonData = await response.json();
// WARNING! Might be injecting a malicious script!
document.getElementById("resp").innerHTML = jsonData;
    ...

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

const response = await fetch("https://api.example.com/data.json");
const jsonData = await response.json();
// JSON.parse does not evaluate the attacker's scripts.
let resp = JSON.parse(jsonData);

const response = await fetch("https://api.example.com/data.json");
const jsonData = response.json();
// textContent does not let the attacker inject HTML elements.
document.getElementById("resp").textContent = jsonData;

Limiter l'accès des scripts de contenu aux requêtes d'origines multiples

Lorsque vous effectuez des requêtes d'origine croisée 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 peu 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 d'extraire n'importe quelle URL à laquelle l'extension a accès. Une page Web malveillante peut être en mesure de créer de faux messages et d'inciter l'extension à donner accès à des ressources d'origine croisée.

Concevez plutôt des gestionnaires de messages qui limitent les ressources pouvant être récupérées. Ci-dessous, seul 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') {
      const 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 avec les 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 HTTPS dans la mesure du possible.

Ajuster la stratégie de sécurité du contenu

Si vous modifiez la Content Security Policy par défaut de votre extension en ajoutant un attribut content_security_policy à votre fichier manifeste, vous devrez 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.