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 les mêmes règles d'origine. Les scripts de contenu lancent des requêtes pour le compte de l'origine Web dans laquelle ils ont été injectés. Ils sont donc également soumis aux mêmes règles relatives à l'origine. Les origines des extensions ne sont pas aussi 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 multi-origines.

Origine de l'extension

Chaque extension en cours d'exécution possède sa propre origine de sécurité. Sans demander de droits supplémentaires, l'extension peut appeler fetch() pour obtenir les ressources incluses dans son installation. Par exemple, si une extension contient un fichier de configuration JSON appelé config.json situé 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 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 multi-origines appropriées.

Demander des autorisations multi-origines

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

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

Les valeurs d'autorisation multi-origine peuvent être des noms d'hôte complets, comme suit:

  • "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://*/"

Le format de correspondance "https://*/" autorise l'accès HTTPS à tous les domaines accessibles. Notez qu'ici, les modèles de correspondance sont semblables aux modèles de correspondance de script de contenu, mais 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:

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

Comparaison entre Fetch() et XMLHttpRequest()

fetch() a été créé spécifiquement pour les service workers et suit une tendance Web plus large qui s'éloigne des opérations synchrones. L'API XMLHttpRequest() est compatible avec les extensions autres que le service worker. Son appel déclenche le gestionnaire d'extraction du service worker de l'extension. Les nouvelles œuvres doivent privilégier fetch() dans la mesure du possible.

Points à noter concernant la sécurité

Éviter les failles de script intersites (XSS)

Lorsque vous utilisez des ressources récupérées via fetch(), votre document, panneau latéral ou pop-up hors écran ne doivent pas être victimes d'un 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 des 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 au script de contenu pour les demandes multi-origines

Lorsque vous effectuez des requêtes multi-origines au nom d'un script de contenu, veillez à vous prémunir 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-origine pour permettre à un script de contenu de découvrir le prix d'un article. Une approche moins sécurisée consiste à faire en sorte que le script de contenu spécifie 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 de récupérer n'importe quelle URL à laquelle elle a accès. Une page Web malveillante peut falsifier de tels messages et inciter l'extension à accéder à des ressources multi-origines.

À la place, concevez des gestionnaires de messages qui limitent les ressources pouvant être extraites. Dans l'exemple 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 HTTPS à HTTP

En outre, faites particulièrement attention aux ressources récupérées via HTTP. Si votre extension est utilisée sur un réseau hostile, un pirate informatique ("man-in-the-middle") pourrait modifier la réponse et potentiellement attaquer votre extension. Dans la mesure du possible, préférez le protocole HTTPS.

Ajuster la Content Security Policy

Si vous modifiez la règle Content Security Policy 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, faites preuve de prudence lorsque vous ajoutez explicitement les instructions connect-src ou default-src.