Solicitudes de red de origen cruzado

Las páginas web normales pueden usar las APIs de fetch() o XMLHttpRequest para enviar y recibir datos de servidores remotos, pero están limitadas por la misma política de origen. Las secuencias de comandos de contenido inician solicitudes en nombre del origen web en el que se insertó la secuencia de comandos de contenido y, por lo tanto, las secuencias de comandos de contenido también están sujetas a la misma política de origen. Los orígenes de las extensiones no son tan limitados. Una secuencia de comandos que se ejecuta en un service worker de extensión o en una pestaña en primer plano puede comunicarse con servidores remotos fuera de su origen, siempre que la extensión solicite permisos de origen cruzado.

Origen de la extensión

Cada extensión en ejecución existe dentro de su propio origen de seguridad. Sin solicitar privilegios adicionales, la extensión puede llamar a fetch() para obtener recursos dentro de su instalación. Por ejemplo, si una extensión contiene un archivo de configuración JSON llamado config.json en una carpeta config_resources/, la extensión puede recuperar el contenido del archivo de la siguiente manera:

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

Si la extensión intenta usar un origen de seguridad distinto de sí mismo, por ejemplo, https://www.google.com, el navegador no la permitirá, a menos que la extensión haya solicitado los permisos de origen cruzado correspondientes.

Solicita permisos de origen cruzado

Para solicitar acceso a servidores remotos fuera del origen de una extensión, agrega hosts, patrones de coincidencia o ambos a la sección host_permissions del archivo de manifiesto.

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

Los valores de permisos de origen cruzado pueden ser nombres de host completamente calificados, como los siguientes:

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

O pueden ser patrones de coincidencia, como los siguientes:

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

Un patrón de coincidencia de "https://*/" permite el acceso HTTPS a todos los dominios accesibles. Ten en cuenta que, en este caso, los patrones de coincidencia son similares a los patrones de coincidencia de secuencias de comandos de contenido, pero se ignora cualquier información de la ruta de acceso que siga al host.

Además, ten en cuenta que el acceso se otorga por host y por esquema. Si una extensión desea un acceso HTTP seguro y no seguro a un host o conjunto de hosts determinado, debe declarar los permisos por separado:

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

Fetch() frente a XMLHttpRequest()

fetch() se creó específicamente para service workers y sigue una tendencia web más amplia que va más allá de las operaciones síncronas. La API de XMLHttpRequest() es compatible con extensiones fuera del service worker, y cuando se la llama, se activa el controlador de recuperación del service worker de la extensión. El trabajo nuevo debe favorecer fetch() siempre que sea posible.

Consideraciones de seguridad

Evita vulnerabilidades de secuencias de comandos entre sitios

Cuando uses recursos recuperados a través de fetch(), el documento fuera de pantalla, el panel lateral o la ventana emergente deben tener cuidado de no ser víctimas de secuencias de comandos entre sitios. En particular, evita usar APIs peligrosas, como innerHTML. Por ejemplo:

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;
    ...

En su lugar, es preferible usar APIs más seguras que no ejecuten secuencias de comandos:

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;

Limita el acceso a la secuencia de comandos del contenido a solicitudes de origen cruzado

Cuando realices solicitudes de origen cruzado en nombre de una secuencia de comandos de contenido, ten cuidado de protegerte contra páginas web maliciosas que podrían intentar suplantar la identidad de una secuencia de comandos de contenido. En particular, no permitas que las secuencias de comandos de contenido soliciten una URL arbitraria.

Considera un ejemplo en el que una extensión realiza una solicitud de origen cruzado para permitir que una secuencia de comandos de contenido descubra el precio de un artículo. Un enfoque no tan seguro sería que la secuencia de comandos de contenido especifique el recurso exacto que debe obtener la página en segundo plano.

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())
);

En el enfoque anterior, la secuencia de comandos de contenido puede pedirle a la extensión que recupere cualquier URL a la que tenga acceso. Una página web maliciosa podría falsificar estos mensajes y engañar a la extensión para que otorgue acceso a recursos de origen cruzado.

En su lugar, diseña controladores de mensajes que limiten los recursos que se pueden recuperar. A continuación, solo se proporciona itemId en la secuencia de comandos del contenido, no la URL completa.

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 => ...
);

Opta por HTTPS en lugar de HTTP

Además, ten mucho cuidado con los recursos recuperados a través de HTTP. Si tu extensión se usa en una red hostil, un atacante (también conocido como "man-in-the-middle") podría modificar la respuesta y, potencialmente, atacar tu extensión. En su lugar, es preferible usar HTTPS siempre que sea posible.

Ajusta la política de seguridad del contenido

Si modificas la Política de Seguridad del Contenido predeterminada para tu extensión y agregas un atributo content_security_policy a tu manifiesto, deberás asegurarte de que se permitan los hosts a los que quieras conectarte. Si bien la política predeterminada no restringe las conexiones a los hosts, ten cuidado cuando agregues de forma explícita las directivas connect-src o default-src.