XMLHttpRequest de origen cruzado

Las páginas web normales pueden usar el objeto XMLHttpRequest para enviar y recibir datos de servidores remotos, pero están limitadas por la política de mismo origen. Las secuencias de comandos de contenido inician solicitudes en nombre del origen web en el que se insertaron y, por lo tanto, también están sujetas a la política del mismo origen. (Las secuencias de comandos de contenido están sujetas a CORB desde Chrome 73 y a CORS desde Chrome 83). Los orígenes de las extensiones no están tan limitados: una secuencia de comandos que se ejecuta en la página en segundo plano o en la pestaña en primer plano de una extensión puede comunicarse con servidores remotos fuera de su origen, siempre y cuando 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 independiente. Sin solicitar privilegios adicionales, la extensión puede usar XMLHttpRequest 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:

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

Si la extensión intenta usar un origen de seguridad que no sea el suyo, por ejemplo, https://www.google.com, el navegador no lo permite, a menos que la extensión haya solicitado los permisos de origen cruzado adecuados.

Cómo solicitar permisos de origen cruzado

Si agregas hosts o patrones de coincidencia de hosts (o ambos) a la sección permissions del archivo manifest, la extensión puede solicitar acceso a servidores remotos fuera de su origen.

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

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

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

También 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 las secuencias de comandos de contenido, pero se ignora cualquier información de ruta de acceso que siga al host.

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

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

Consideraciones de seguridad

Cómo evitar vulnerabilidades de secuencias de comandos entre sitios

Cuando uses recursos recuperados a través de XMLHttpRequest, tu página en segundo plano debe tener cuidado de no ser víctima de secuencias de comandos entre sitios. Específicamente, evita usar APIs peligrosas como las siguientes:

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

En cambio, prefiere las APIs más seguras que no ejecutan secuencias de comandos:

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

Cómo limitar el acceso de las secuencias de comandos de contenido a las 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 una secuencia de comandos de contenido. En particular, no permitas que los 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 elemento. Un enfoque (inseguro) sería que la secuencia de comandos de contenido especifique el recurso exacto que debe recuperar 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 solicitarle a la extensión que recupere cualquier URL a la que tenga acceso. Una página web maliciosa podría falsificar esos mensajes y engañar a la extensión para que otorgue acceso a recursos de origen cruzado.

En cambio, diseña controladores de mensajes que limiten los recursos que se pueden recuperar. A continuación, el fragmento de código de contenido solo proporciona itemId, no la URL completa.

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

Preferir HTTPS en lugar de HTTP

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

Cómo ajustar la política de seguridad del contenido

Si modificas la Política de Seguridad del Contenido predeterminada para las apps o extensiones agregando un atributo content_security_policy a tu manifiesto, deberás asegurarte de que se permitan todos los hosts a los que desees conectarte. Si bien la política predeterminada no restringe las conexiones a los hosts, ten cuidado cuando agregues explícitamente las directivas connect-src o default-src.