Requêtes réseau multi-origines

Les pages Web standards peuvent utiliser les API fetch() ou XMLHttpRequest pour envoyer et recevoir des données à partir de serveurs distants, mais elles sont limitées par la règle de même origine. Les scripts de contenu envoient des requêtes au nom de l'origine Web dans laquelle le script de contenu a été injecté. Par conséquent, les scripts de contenu sont également soumis à la règle de même origine. Les origines des extensions ne sont pas si limitées. Un script exécuté 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 droits supplémentaires, l'extension peut appeler fetch() pour obtenir des ressources lors de son installation. Par exemple, si une extension contient un fichier de configuration JSON appelé config.json, dans un dossier config_resources/, l'extension 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 à une origine de sécurité autre que la sienne, par exemple https://www.google.com, elle sera traitée comme une requête inter-origine, 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'origines multiples

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

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

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

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

Ils peuvent également correspondre à des modèles, comme ceux-ci:

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

Un format de correspondance "https://*/" autorise l'accès HTTPS à tous les domaines accessibles. Notez que, ici, les formats d'identification sont similaires aux formats d'identification des scripts de contenu, mais 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 un accès HTTP sécurisé et non sécurisé à un hôte ou à un ensemble d'hôtes donnés, elle doit déclarer les autorisations séparément:

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

Fetch() par rapport à XMLHttpRequest()

fetch() a été créé spécifiquement pour les services workers et suit 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. L'appel de l'API déclenche le gestionnaire de récupération du service worker de l'extension. Dans la mesure du possible, les nouvelles tâches 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 faire attention à ne pas être victimes de scripts intersites. 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 des API plus sécurisées 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 du script 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 d'origine croisée pour permettre à un script de contenu de découvrir le prix d'un article. Une approche moins sécurisée consisterait à demander au script de contenu de spécifier la ressource exacte à extraire par la page en 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 elle a accès. Une page Web malveillante peut être en mesure de créer de tels messages et de duper l'extension pour qu'elle accorde l'accès à des ressources multi-origines.

Concevez plutôt des gestionnaires de messages qui limitent les ressources pouvant être récupérées. Ci-dessous, seul le 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 protocole HTTPS au protocole 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 pirate informatique (é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 règle de sécurité du contenu par défaut de votre extension en ajoutant un attribut content_security_policy à votre fichier manifeste, vous devez vous assurer que tous les hôtes auxquels vous souhaitez vous connecter sont autorisés. Bien que la stratégie par défaut ne limite pas les connexions aux hôtes, soyez prudent lorsque vous ajoutez explicitement les directives connect-src ou default-src.