La piedra angular de la seguridad del navegador es la "política del mismo origen " . Muchos desarrolladores lo saben, pero no del todo.
Descripción general
significado
En 1995, Netscape introdujo la política del mismo origen en los navegadores. Actualmente, todos los navegadores implementan esta política.
Inicialmente, su significado es que la cookie establecida por la página web A no puede ser abierta por la página web B a menos que las dos páginas web tengan el mismo origen. El llamado "mismo origen" se refiere a "tres similitudes".
El acuerdo es el mismo.
Mismo nombre de dominio
El puerto es el mismo (esto se puede ignorar, consulte los detalles a continuación)
Por ejemplo, para http://www.example.com/dir/page.html
esta URL, el protocolo es http://
, el nombre de dominio es www.example.com
, el puerto es 80
(se puede omitir el puerto predeterminado) y su homología es la siguiente.
-
http://www.example.com/dir2/other.html
:Mismo origen -
http://example.com/dir/other.html
: Diferentes fuentes (diferentes nombres de dominio) -
http://v2.www.example.com/dir/other.html
: Diferentes fuentes (diferentes nombres de dominio) -
http://www.example.com:81/dir/other.html
: Diferentes fuentes (diferentes puertos) -
https://www.example.com/dir/page.html
: Diferentes fuentes (diferentes protocolos)
Tenga en cuenta que el estándar estipula que las URL con diferentes puertos no tienen el mismo origen (por ejemplo, el puerto 8000 y el puerto 8001 no tienen el mismo origen), pero los navegadores no cumplen con esta regla. De hecho, diferentes puertos del mismo dominio pueden leer las cookies de los demás.
Objetivo
El propósito de la política del mismo origen es garantizar la seguridad de la información del usuario y evitar que sitios web maliciosos roben datos.
Imagine esta situación: el sitio web A es un banco. Después de que el usuario inicia sesión, el sitio web A establece una cookie en la máquina del usuario, que contiene información privada. Después de que el usuario abandona el sitio web A, vuelve a visitar el sitio web B. Si no hay restricción de origen, el sitio web B puede leer las cookies del sitio web A y luego se filtra la privacidad. Lo que es aún más aterrador es que las cookies se utilizan a menudo para guardar el estado de inicio de sesión del usuario. Si el usuario no cierra la sesión, otros sitios web pueden hacerse pasar por él y hacer lo que quieran. Porque el navegador también estipula que el envío de formularios no está restringido por la política del mismo origen.
Se puede ver que es necesaria la misma política de origen; de lo contrario, las cookies se pueden compartir y Internet no será seguro en absoluto.
rango límite
Con el desarrollo de Internet, las políticas del mismo origen se han vuelto cada vez más estrictas. Actualmente, existen tres conductas que se restringen si no son del mismo origen.
(1) No se pueden leer las cookies, LocalStorage e IndexedDB de páginas web no originales.
(2) No se puede acceder al DOM de páginas web no homogéneas.
(3) No se puede enviar una solicitud AJAX a una dirección no original (se puede enviar, pero el navegador se negará a aceptar la respuesta).
Además, se pueden obtener objetos de otras ventanas mediante scripts JavaScript window
. Si se trata de una página web no original, actualmente se permite que una ventana acceda a window
nueve propiedades y cuatro métodos de objetos de otras páginas web.
-
ventana.cerrada
-
Marcos de ventana
-
ventana.longitud
-
ventana.ubicación
-
abridor de ventana
-
ventana.padre
-
ventana.yo
-
ventana.arriba
-
ventana.ventana
-
ventana.desenfoque()
-
ventana.cerrar()
-
ventana.enfoque()
-
ventana.postMessage()
Entre los nueve atributos anteriores, sólo uno window.location
es legible y escribible, y los otros ocho son todos de sólo lectura. Además, incluso si location
el objeto no es del mismo origen, solo se permiten llamar location.replace()
a métodos y escribir propiedades.location.href
Aunque estas restricciones son necesarias, a veces resultan inconvenientes y comprometen fines legítimos. A continuación se explica cómo eludir las restricciones anteriores.
Galleta
Una cookie es una pequeña porción de información escrita por el servidor en el navegador, que solo puede ser compartida por páginas web con el mismo origen. Si los nombres de dominio de primer nivel de dos páginas web son iguales pero solo los nombres de dominio de segundo nivel son diferentes, el navegador permite document.domain
compartir cookies a través de la configuración.
Por ejemplo, si la URL de la página web A es http://w1.example.com/a.html
y la URL de la página web B es http://w2.example.com/b.html
, siempre que la configuración sea la misma document.domain
, las dos páginas web pueden compartir cookies. Porque el navegador document.domain
comprueba si tiene el mismo origen mediante atributos.
// Ambas páginas web deben configurarse document.domain = 'example.com';
Tenga en cuenta que ambas páginas web A y B deben establecer document.domain
atributos para lograr el mismo origen. Debido a que document.domain
el puerto se restablecerá al configurar null
, si solo configura una página web document.domain
, los puertos de las dos URL serán diferentes y no se logrará el propósito de la homología.
Ahora, la página web A establece una cookie mediante un script.
document.cookie = "prueba1=hola";
La página web B puede leer esta cookie.
var allCookie = documento.cookie;
Tenga en cuenta que este método solo se aplica a ventanas de cookies y iframe. LocalStorage e IndexedDB no pueden eludir la política del mismo origen a través de este método. En su lugar, utilice la API PostMessage que se presenta a continuación.
Además, el servidor también puede especificar el nombre de dominio de la cookie como nombre de dominio de primer nivel al configurar la cookie, por ejemplo example.com
.
Establecer-Cookie: clave=valor; dominio=ejemplo.com; ruta=/
En este caso, tanto el nombre de dominio de segundo nivel como el nombre de dominio de tercer nivel pueden leer esta cookie sin ninguna configuración.
iframe y comunicación multiventana
iframe
Los elementos se pueden incrustar en otras páginas web dentro de la página web actual. Cada iframe
elemento forma su propia ventana, es decir, tiene su propio window
objeto. iframe
El script en la ventana puede obtener la ventana principal y la ventana secundaria. Sin embargo, la ventana principal y la ventana secundaria solo pueden comunicarse si tienen el mismo origen; si cruzan dominios, no pueden obtener el DOM de la otra parte.
Por ejemplo, si la ventana principal ejecuta el siguiente comando, si iframe
la ventana no proviene de la misma fuente, se informará un error.
document .getElementById("myIFrame") .contentWindow .document // DOMException no detectada: se bloqueó el acceso de un marco a un marco de origen cruzado.
En el comando anterior, la ventana principal quiere obtener el DOM de la ventana secundaria, pero se informa un error debido a dominios cruzados.
Viceversa, la ventana secundaria también informará un error al obtener el DOM de la ventana principal.
window.parent.document.body // Informe de error
Esta situación se aplica no solo a iframe
las ventanas, sino también a window.open
las ventanas abiertas por métodos: mientras crucen dominios, no habrá comunicación entre la ventana principal y la ventana secundaria.
Si los nombres de dominio de primer nivel de las dos ventanas son iguales pero solo los nombres de dominio de segundo nivel son diferentes, entonces configurando los atributos introducidos en la sección anterior, puede document.domain
eludir la política del mismo origen y obtener el DOM.
Para sitios web con orígenes completamente diferentes, actualmente existen dos métodos para resolver el problema de comunicación de las ventanas entre dominios.
identificador de fragmento
API de mensajería entre documentos (mensajería entre documentos)
identificador de fragmento
El identificador de fragmento se refiere a #
la parte que sigue al número de URL, como por http://example.com/x.html#fragment
ejemplo #fragment
. Si simplemente cambia el identificador del fragmento, la página no se actualizará.
La ventana principal puede escribir información en el identificador de fragmento de la ventana secundaria.
var src = origenURL + '#' + datos; document.getElementById('myIFrame').src = src;
En el código anterior, la ventana principal escribe la información que se transmitirá en el identificador de fragmento de la ventana iframe.
Las ventanas secundarias hashchange
reciben notificaciones escuchando eventos.
window.onhashchange = checkMessage; function checkMessage() { var mensaje = window.location.hash; //... }
Asimismo, una ventana secundaria puede cambiar el identificador de fragmento de la ventana principal.
parent.ubicación.href = objetivo + '#' + hash;
ventana.postMessage()
El método anterior es un crack. Para resolver este problema, HTML5 introduce una nueva API: API de mensajería entre documentos (mensajería entre documentos).
Esta API window
agrega un nuevo window.postMessage
método al objeto que permite la comunicación entre ventanas, independientemente de si las dos ventanas tienen el mismo origen. Por ejemplo, si la ventana principal envía un mensaje aaa.com
a la ventana secundaria bbb.com
, postMessage
simplemente llame al método.
// La ventana principal abre una ventana secundaria var popup = window.open('http://bbb.com', 'title'); // La ventana principal envía un mensaje a la ventana secundaria popup.postMessage('Hola mundo !', 'http://bbb.com');
postMessage
El primer parámetro del método es el contenido de información específico y el segundo parámetro es el origen de la ventana que recibe el mensaje, es decir, "protocolo + nombre de dominio + puerto". También se puede configurar *
para que los nombres de dominio no estén restringidos y se envíen a todas las ventanas.
La forma en que la ventana secundaria envía un mensaje a la ventana principal es similar.
//La ventana secundaria envía un mensaje a la ventana principal window.opener.postMessage('Encantado de verte', 'http://aaa.com');
Tanto la ventana principal como la secundaria pueden message
escuchar los mensajes de cada una a través de eventos.
// Tanto la ventana principal como la secundaria pueden usar el siguiente código, // para escuchar los mensajes window.addEventListener('message', function (e) { console.log(e.data); },false);
message
El parámetro del evento es el objeto del evento event
, que proporciona las siguientes tres propiedades.
event.source
: Ventana para enviar mensajes
event.origin
: URL a la que se envía el mensaje
event.data
: Contenido del mensaje
En el siguiente ejemplo, la ventana secundaria event.source
hace referencia a la ventana principal a través de propiedades y luego envía un mensaje.
window.addEventListener('mensaje', recibirMensaje); function recibirMensaje(evento) { event.source.postMessage('¡Qué bueno verte!', '*'); }
Hay varias cosas a tener en cuenta sobre el código anterior. En primer lugar, receiveMessage
no hay ninguna fuente de información filtrada en la función y se procesará la información enviada desde cualquier URL. En segundo lugar, postMessage
la URL de la ventana de destino especificada en el método es un asterisco, lo que indica que la información se puede enviar a cualquier URL. En términos generales, estos dos métodos no se recomiendan porque no son lo suficientemente seguros y pueden explotarse de forma maliciosa.
event.origin
Las propiedades pueden filtrar los mensajes no enviados a esta ventana.
window.addEventListener('mensaje', recibirMensaje); función recibirMensaje(evento) { if (evento.origen!== 'http://aaa.com') retorno; if (event.data === 'Hola mundo') { event.source.postMessage('Hola', event.origin); } más { console.log(event.data); } }
Almacenamiento local
A través de esto window.postMessage
, también es posible leer y escribir el Almacenamiento Local de otras ventanas.
A continuación se muestra un ejemplo en el que la ventana principal se escribe en una ventana secundaria de iframe localStorage
.
ventana.onmessage = función(e) { if (e.origin!== 'http://bbb.com') { retorno; } var carga útil = JSON.parse(e.data); localStorage.setItem(carga útil.clave, JSON.stringify(carga útil.datos)); };
En el código anterior, la ventana secundaria escribe el mensaje enviado por la ventana principal en su propio LocalStorage.
El código para enviar mensajes desde la ventana principal es el siguiente.
var win = document.getElementsByTagName('iframe')[0].contentWindow; var obj = { nombre: 'Jack' }; win.postMessage ( JSON.stringify ({clave: 'almacenamiento', datos: obj}), 'http://bbb.com' );
El código de la versión mejorada de la ventana secundaria para recibir mensajes es el siguiente.
ventana.onmessage = función(e) { if (e.origin!== 'http://bbb.com') retorno; carga útil var = JSON.parse(e.data); switch (carga útil.método) { caso 'conjunto': localStorage.setItem(carga útil.clave, JSON.stringify(carga útil.datos)); romper; caso 'obtener': var padre = ventana.padre; var datos = localStorage.getItem(payload.key); parent.postMessage(datos, 'http://aaa.com'); romper; caso 'eliminar': localStorage.removeItem(payload.key); romper; } };
La versión mejorada del código de envío de mensajes para la ventana principal es la siguiente.
var win = document.getElementsByTagName('iframe')[0].contentWindow; var obj = { nombre: 'Jack' }; // 存入对象 win.postMessage( JSON.stringify({clave: 'almacenamiento', método: 'set', datos: obj}), 'http://bbb.com' ); // 读取对象 win.postMessage( JSON.stringify({clave: 'almacenamiento', método: "get"}), "*" ); ventana.onmessage = función(e) { if (e.origin!= 'http://aaa.com') retorno; console.log(JSON.parse(e.data).nombre); };
AJAX
La política del mismo origen estipula que las solicitudes AJAX solo se pueden enviar a URL con el mismo origen; de lo contrario, se informará un error.
Además de configurar un servidor proxy (el navegador solicita el mismo servidor de origen, que luego solicita servicios externos), existen tres formas de eludir esta restricción.
JSONP
WebSocket
CORS
JSONP
JSONP es un método común para la comunicación entre orígenes entre servidores y clientes. La característica más importante es que es simple y fácil de usar, no hay problemas de compatibilidad, todos los navegadores antiguos lo admiten y la modificación del lado del servidor es muy pequeña.
Así es como se hace.
En el primer paso, la página web agrega un <script>
elemento y solicita un script al servidor, que no está restringido por la política del mismo origen y se puede solicitar en todos los dominios.
<script src="http://api.foo.com?callback=bar"></script>
Tenga en cuenta que la URL del script solicitado tiene un callback
parámetro ( ?callback=bar
), que se utiliza para indicarle al servidor el nombre de la función de devolución de llamada del cliente ( bar
).
En el segundo paso, después de recibir la solicitud, el servidor concatena una cadena, coloca los datos JSON en el nombre de la función y los devuelve como una cadena ( bar({...})
).
En el tercer paso, el cliente analizará la cadena devuelta por el servidor como un código, porque el navegador cree que este es el <script>
contenido del script solicitado por la etiqueta. En este momento, siempre que el cliente defina bar()
la función, puede obtener los datos JSON devueltos por el servidor en el cuerpo de la función.
Veamos un ejemplo a continuación. Primero, la página web inserta dinámicamente <script>
un elemento que realiza una solicitud a la URL entre dominios.
función addScriptTag(src) { var script = document.createElement('script'); script.setAttribute('tipo', 'texto/javascript'); script.src = src; documento.body.appendChild(guión); } foo({}) ventana.onload = función () { addScriptTag('http://example.com/ip?callback=foo'); } function foo(data) { console.log('Su dirección IP pública es: ' + data.ip); };
El código anterior realiza una solicitud al servidor agregando <script>
elementos dinámicamente example.com
. Tenga en cuenta que la cadena de consulta de esta solicitud tiene un callback
parámetro que especifica el nombre de la función de devolución de llamada, que es necesaria para JSONP.
Después de que el servidor reciba esta solicitud, colocará los datos en la posición del parámetro de la función de devolución de llamada y los devolverá.
foo({ 'ip': '8.8.8.8' });
Dado que <script>
el script solicitado por el elemento se ejecuta directamente como código. En este momento, siempre que el navegador defina foo
la función, la función se llamará inmediatamente. Los datos JSON como parámetros se tratan como objetos JavaScript en lugar de cadenas, evitando así el JSON.parse
paso de uso.
WebSocket
WebSocket es un protocolo de comunicación que utiliza ws://
(sin cifrar) y wss://
(cifrado) como prefijos de protocolo. Este protocolo no implementa una política del mismo origen y se puede realizar comunicación entre orígenes a través de él siempre que el servidor lo admita.
A continuación se muestra un ejemplo de la información del encabezado de una solicitud WebSocket emitida por el navegador (tomada de Wikipedia ).
GET /chat HTTP/1.1 Host: server.example.com Actualización: websocket Conexión: Actualización Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw== Sec-WebSocket-Protocol: chat, superchat Sec-WebSocket-Version: 13 Origen: http:// ejemplo.com
En el código anterior, hay un campo que Origin
indica el origen de la solicitud (origen), es decir, el nombre de dominio desde el que se envió.
Es precisamente por Origin
este campo que WebSocket no implementa la misma política de origen. Porque el servidor puede determinar si permite esta comunicación en función de este campo. Si el nombre de dominio está en la lista blanca, el servidor responderá de la siguiente manera.
HTTP/1.1 101 Protocolos de conmutación Actualización: conexión websocket: Actualización Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk= Sec-WebSocket-Protocol: chat
CORS
CORS es la abreviatura de Cross-Origin Resource Sharing. Es un estándar del W3C y es la solución fundamental para solicitudes AJAX de origen cruzado. En comparación con JSONP, que solo puede enviar GET
solicitudes, CORS permite cualquier tipo de solicitud.
El próximo capítulo presentará en detalle cómo completar solicitudes AJAX de origen cruzado a través de CORS.
Link de referencia
-
Red de desarrolladores de Mozilla, Window.postMessage
-
Jakub Jankiewicz, almacenamiento local entre dominios
-
David Baron, setTimeout con un retraso más corto : use window.postMessage para activar la función de devolución de llamada en 0 milisegundos