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 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. (Las secuencias de comandos de contenido están sujetas a CORB desde Chrome 73 y CORS desde Chrome 83). Los orígenes de las extensiones no son tan limitados: una secuencia de comandos que se ejecuta en la página en segundo plano o en la pestaña de primer plano de una extensión puede comunicarse con los 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 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 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.

Cómo solicitar permisos de origen cruzado

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

{
  "name": "My extension",
  ...
  "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:

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

Consideraciones de seguridad

Evita vulnerabilidades de secuencias de comandos entre sitios

Cuando uses recursos recuperados a través de XMLHttpRequest, tu página de fondo debe tener cuidado de no ser víctima de secuencias de comandos entre sitios. En particular, 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 su lugar, es preferible usar APIs más seguras que no ejecuten 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();

Limita el acceso a las secuencias 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 seguro) sería que la secuencia de comandos de contenido especifique el recurso exacto que 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 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') {
        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 => ...);

Prioriza 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 agregas un atributo content_security_policy a tu manifiesto para modificar la Política de Seguridad del Contenido predeterminada de las apps o extensiones, 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.