Subdominios, puertos y protocolos de comodines Access-Control-Allow-Origin

312

Estoy tratando de habilitar CORS para todos los subdominios, puertos y protocolos.

Por ejemplo, quiero poder ejecutar una solicitud XHR desde http://sub.mywebsite.com:8080/ a https://www.mywebsite.com/ *

Normalmente, me gustaría habilitar la solicitud de coincidencia de orígenes (y limitada a):

//*.mywebsite.com:*/*

Elie
fuente

Respuestas:

207

Basado en la respuesta de DaveRandom , también estaba jugando y encontré una solución de Apache un poco más simple que produce el mismo resultado ( Access-Control-Allow-Originse establece en el protocolo específico actual + dominio + puerto dinámicamente) sin usar ninguna regla de reescritura:

SetEnvIf Origin ^(https?://.+\.mywebsite\.com(?::\d{1,5})?)$   CORS_ALLOW_ORIGIN=$1
Header append Access-Control-Allow-Origin  %{CORS_ALLOW_ORIGIN}e   env=CORS_ALLOW_ORIGIN
Header merge  Vary "Origin"

Y eso es.

Aquellos que quieran habilitar CORS en el dominio principal (por ejemplo, mywebsite.com) además de todos sus subdominios pueden simplemente reemplazar la expresión regular en la primera línea con esta:

^(https?://(?:.+\.)?mywebsite\.com(?::\d{1,5})?)$.

Nota: Para el cumplimiento de las especificaciones y el comportamiento de almacenamiento en caché correcto, SIEMPRE agregue el Vary: Originencabezado de respuesta para los recursos habilitados para CORS, incluso para las solicitudes que no son de CORS y aquellas de un origen no permitido (ver ejemplo por qué ).

No yo
fuente
1
Tuvimos casi esto (no Vary Origin) y obtuvimos un mal comportamiento cuando los visitantes saltaron entre múltiples subdominios usando la misma fuente. La fuente y el encabezado de origen de control de acceso también se almacenaron en caché. He hecho un pequeño cambio en este caso: uso "Access-Control-Allow-Origin *" si la solicitud es de uno de nuestros dominios permitidos. Tal vez esto se resolvió con "Vary Origin" que no teníamos antes ... ahora también agregó eso.
Erik Melkersson
2
No funciona para el dominio principal 'mywebsite.com'
biology.info
1
@pgmann, no hay necesidad de escapar //en este contexto, ya que la configuración de Apache no utiliza expresiones regulares delimitadas por barras. Regexr se queja porque, en ese contexto, las barras tienen un significado especial como delimitadores.
Noyo
1
The 'Access-Control-Allow-Origin' header contains multiple values '^(https?://(?:.+.)?aerofotea.com(?::d{1,5})?)$', but only one is allowed. Origin 'http://local.aerofotea.com' is therefore not allowed access.
Aero Wang
3
¿Dónde colocas este código? .htaccess o en la configuración de host virtual apache?
Glen
252

La especificación CORS es todo o nada. Sólo es compatible con *, nullo la exacta protocolo + dominio + puerto: http://www.w3.org/TR/cors/#access-control-allow-origin-response-header

Su servidor necesitará validar el encabezado de origen utilizando la expresión regular, y luego puede hacer eco del valor de origen en el encabezado de respuesta Access-Control-Allow-Origin.

Monsur
fuente
77
@Dexter "nulo" se puede utilizar en respuesta a un origen "nulo", por ejemplo, cuando se realiza una solicitud CORS desde un esquema file: //.
Monsur
130
Es profundamente miope que la especificación CORS no admitiría el caso de uso exacto del OP .
Aroth
66
@aroth: No realmente, la especificación permite que las implementaciones usen cualquier sintaxis coincidente que deseen; solo sería profundamente miope para una implementación que no sea compatible con este caso de uso. En otras palabras, lo que especifica a su servidor no es el valor ACAO, este último es solo un detalle de protocolo. Mi suposición es que hay un escenario de seguridad que requiere o se beneficia de que el origen se repita, pero una implementación ingenua funciona simplemente diciendo "OK" o no.
TNE
3
Actualización de 2015: esta respuesta debe considerarse dañina, ya que está incompleta y puede provocar problemas de almacenamiento en caché. Consulte mi respuesta a continuación para obtener una implementación adecuada (para Apache) y una explicación: stackoverflow.com/a/27990162/357774 . Además, @aroth, como señala TNE, la especificación de hecho lo hace permitir caso de uso exacto de la OP: w3.org/TR/cors/#resource-implementation . Y como señala esta respuesta, depende del servidor implementarla. Esto se puede hacer en 3 líneas, como se ve en la respuesta mencionada anteriormente.
Noyo
19
@Noyo - Aclararé mi significado original entonces. Es profundamente miope que la especificación CORS no requiere estrictamente que todos los servidores que implementan CORS brinden soporte automático e integrado para el caso de uso exacto del OP . Dejar que cada usuario individual construya su propio calce usando código PHP personalizado, reescribir reglas o lo que sea que tenga es una receta para la fragmentación, errores y desastres. Los desarrolladores de servidores deberían saberlo mejor que eso; y si no lo hacen, la especificación CORS debería obligarlos a hacerlo.
Aroth
59

EDITAR : Use la solución de @ Noyo en lugar de esta. Es más simple, más claro y probablemente mucho más eficiente bajo carga.

¡RESPUESTA ORIGINAL DEJADA AQUÍ SOLO PARA FINES HISTÓRICOS!


Jugué un poco con este problema y se me ocurrió esta solución reutilizable .htaccess (o httpd.conf) que funciona con Apache:

<IfModule mod_rewrite.c>
<IfModule mod_headers.c>
    # Define the root domain that is allowed
    SetEnvIf Origin .+ ACCESS_CONTROL_ROOT=yourdomain.com

    # Check that the Origin: matches the defined root domain and capture it in
    # an environment var if it does
    RewriteEngine On
    RewriteCond %{ENV:ACCESS_CONTROL_ROOT} !=""
    RewriteCond %{ENV:ACCESS_CONTROL_ORIGIN} =""
    RewriteCond %{ENV:ACCESS_CONTROL_ROOT}&%{HTTP:Origin} ^([^&]+)&(https?://(?:.+?\.)?\1(?::\d{1,5})?)$
    RewriteRule .* - [E=ACCESS_CONTROL_ORIGIN:%2]

    # Set the response header to the captured value if there was a match
    Header set Access-Control-Allow-Origin %{ACCESS_CONTROL_ORIGIN}e env=ACCESS_CONTROL_ORIGIN
</IfModule>
</IfModule>

Simplemente configure la ACCESS_CONTROL_ROOTvariable en la parte superior del bloque para su dominio raíz y se hará eco del Origin:valor del encabezado de la solicitud al cliente en elAccess-Control-Allow-Origin: valor del encabezado de respuesta si coincide con su dominio.

Tenga en cuenta también que puede usar sub.mydomain.comcomo ACCESS_CONTROL_ROOTy limitará los orígenes a sub.mydomain.comy *.sub.mydomain.com(es decir, no tiene que ser la raíz del dominio). Los elementos que pueden variar (protocolo, puerto) se pueden controlar modificando la porción de coincidencia de URI de la expresión regular.

DaveRandom
fuente
22

Estoy respondiendo esta pregunta, porque la respuesta aceptada no puede seguir

  1. la agrupación de expresiones regulares es un éxito en el rendimiento , que no es necesario.
  2. no puede coincidir con el dominio primario y solo funciona para el subdominio.

Por ejemplo: no enviará encabezados CORS para http://mywebsite.com mientras funcione para http://somedomain.mywebsite.com/

SetEnvIf Origin "http(s)?://(.+\.)?mywebsite\.com(:\d{1,5})?$" CORS=$0

Header set Access-Control-Allow-Origin "%{CORS}e" env=CORS
Header merge  Vary "Origin"

Para habilitar su sitio, simplemente coloque su sitio en lugar de "mywebsite.com" en la Configuración de Apache anterior.

Para permitir múltiples sitios:

SetEnvIf Origin "http(s)?://(.+\.)?(othersite\.com|mywebsite\.com)(:\d{1,5})?$" CORS=$0

Prueba después de la implementación:

La siguiente respuesta curl debe tener el encabezado "Access-Control-Allow-Origin" después del cambio.

curl -X GET -H "Origin: http://examplesite1.com" --verbose http://examplesite2.com/query
Pratap Koritala
fuente
12

Necesitaba una solución solo para PHP, así que en caso de que alguien también la necesite. Toma una cadena de entrada permitida como "* .example.com" y devuelve el nombre del servidor del encabezado de la solicitud, si la entrada coincide.

function getCORSHeaderOrigin($allowed, $input)
{
    if ($allowed == '*') {
        return '*';
    }

    $allowed = preg_quote($allowed, '/');

    if (($wildcardPos = strpos($allowed, '*')) !== false) {
        $allowed = str_replace('*', '(.*)', $allowed);
    }

    $regexp = '/^' . $allowed . '$/';

    if (!preg_match($regexp, $input, $matches)) {
        return 'none';
    }

    return $input;
}

Y aquí están los casos de prueba para un proveedor de datos phpunit:

//    <description>                            <allowed>          <input>                   <expected>
array('Allow Subdomain',                       'www.example.com', 'www.example.com',        'www.example.com'),
array('Disallow wrong Subdomain',              'www.example.com', 'ws.example.com',         'none'),
array('Allow All',                             '*',               'ws.example.com',         '*'),
array('Allow Subdomain Wildcard',              '*.example.com',   'ws.example.com',         'ws.example.com'),
array('Disallow Wrong Subdomain no Wildcard',  '*.example.com',   'example.com',            'none'),
array('Allow Double Subdomain for Wildcard',   '*.example.com',   'a.b.example.com',        'a.b.example.com'),
array('Don\'t fall for incorrect position',    '*.example.com',   'a.example.com.evil.com', 'none'),
array('Allow Subdomain in the middle',         'a.*.example.com', 'a.bc.example.com',       'a.bc.example.com'),
array('Disallow wrong Subdomain',              'a.*.example.com', 'b.bc.example.com',       'none'),
array('Correctly handle dots in allowed',      'example.com',     'exampleXcom',            'none'),
Lars
fuente
1
+1, editado para usar preg_quote()porque esa es la forma correcta de hacerlo (aunque .es el único meta char de regexp válido en un nombre DNS, preg_quote()describe mejor la operación prevista)
DaveRandom
1
Debe aclararse que noneno es un valor semánticamente válido para el encabezado (o al menos, no hace lo que implica) de acuerdo con la especificación. Como tal, return null;puede tener más sentido para esa rama, y ​​en ese caso no se debe enviar ningún encabezado al cliente, por lo que el llamador debe verificarlo.
DaveRandom
preg_quote()citará el signo * y, str_replace()por ejemplo, deja un "\" huérfano.
Christoffer Bubach
1
esto es útil, pasé tiempo en el problema de CORS hasta que me di cuenta de que mi sitio tenía "www" en el ajax, pero no en la estructura de enlaces permanentes: su solución me ayudó a comprender dónde estaba el problema y lo resolvió por mí.
Sol
3

Al configurar Access-Control-Allow-Originen .htaccess, solo lo siguiente funcionó:

SetEnvIf Origin "http(s)?://(.+\.)?domain\.com(:\d{1,5})?$" CRS=$0
Header always set Access-Control-Allow-Origin "%{CRS}e" env=CRS

He intentado varias otras palabras clave sugeridas Header append, Header set, ninguno funcionaba como se sugiere en muchas respuestas en SO, aunque no tengo ni idea de si estas palabras clave son obsoletos o no válidos para nginx .

Aquí está mi solución completa:

SetEnvIf Origin "http(s)?://(.+\.)?domain\.com(:\d{1,5})?$" CRS=$0
Header always set Access-Control-Allow-Origin "%{CRS}e" env=CRS
Header merge Vary "Origin"

Header always set Access-Control-Allow-Methods "GET, POST"
Header always set Access-Control-Allow-Headers: *

# Cached for a day
Header always set Access-Control-Max-Age: 86400

RewriteEngine On

# Respond with 200OK for OPTIONS
RewriteCond %{REQUEST_METHOD} OPTIONS
RewriteRule ^(.*)$ $1 [R=200,L]
AamirR
fuente
2

Tuvimos problemas similares con Font Awesome en un dominio estático "sin cookies" al leer fuentes del "dominio de cookies" (www.domain.tld) ​​y esta publicación fue nuestro héroe. Consulte aquí: ¿Cómo puedo solucionar el problema de fuente web 'Encabezado de respuesta de recursos cruzados faltantes (CORS)'?

Para los tipos de copiar / pegar-r (y para dar algunos accesorios), reconstruí esto de todas las contribuciones y lo agregué a la parte superior del archivo .htaccess de la raíz del sitio:

<IfModule mod_headers.c>
 <IfModule mod_rewrite.c>
    SetEnvIf Origin "http(s)?://(.+\.)?(othersite\.com|mywebsite\.com)(:\d{1,5})?$" CORS=$0
    Header set Access-Control-Allow-Origin "%{CORS}e" env=CORS
    Header merge  Vary "Origin"
 </IfModule>
</IfModule>

Súper seguro, súper elegante. Me encanta: no tiene que abrir el ancho de banda de sus servidores a los ladrones de recursos / tipos de enlace activo.

Atrezzo a: @Noyo @DaveRandom @ pratap-koritala

(Traté de dejar esto como un comentario a la respuesta aceptada, pero aún no puedo hacerlo)

kanidrive
fuente
0

Parece que la respuesta original fue para pre Apache 2.4. A mi no me sirvió. Esto es lo que tuve que cambiar para que funcione en 2.4. Esto funcionará para cualquier profundidad de subdominio de yourcompany.com .

SetEnvIf Host ^((?:.+\.)*yourcompany\.com?)$    CORS_ALLOW_ORIGIN=$1
Header append Access-Control-Allow-Origin  %{REQUEST_SCHEME}e://%{CORS_ALLOW_ORIGIN}e    env=CORS_ALLOW_ORIGIN
Header merge  Vary "Origin"
Jack K
fuente
0

Tuve que modificar un poco la respuesta de Lars , como un huérfano\ terminó en la expresión regular, para comparar solo el host real (sin prestar atención al protocolo o puerto) y quería admitir el localhostdominio además de mi dominio de producción. Por lo tanto, cambié el $allowedparámetro para ser una matriz.

function getCORSHeaderOrigin($allowed, $input)
{
    if ($allowed == '*') {
        return '*';
    }

    if (!is_array($allowed)) {
        $allowed = array($allowed);
    }

    foreach ($allowed as &$value) {
        $value = preg_quote($value, '/');

        if (($wildcardPos = strpos($value, '\*')) !== false) {
            $value = str_replace('\*', '(.*)', $value);
        }
    }

    $regexp = '/^(' . implode('|', $allowed) . ')$/';

    $inputHost = parse_url($input, PHP_URL_HOST);

    if ($inputHost === null || !preg_match($regexp, $inputHost, $matches)) {
        return 'none';
    }

    return $input;
}

Uso de la siguiente manera:

if (isset($_SERVER['HTTP_ORIGIN'])) {
    header("Access-Control-Allow-Origin: " . getCORSHeaderOrigin(array("*.myproduction.com", "localhost"), $_SERVER['HTTP_ORIGIN']));
}
Capricornio
fuente
0

en mi caso usando angular

en mi interceptor HTTP, configuré

with Credentials: true.

en el encabezado de la solicitud

hosam hemaily
fuente