Volver al blog

Síguenos y suscríbete

Sólo disponible en inglés

Por el momento, esta página solo está disponible en inglés. Lamentamos las molestias. Vuelva a visitar esta página más tarde.

Controlar a terceros con un sitio web de un único origen

Andrew Betts

Principal Developer Advocate, Fastly

Casi todas las páginas web de hoy en día cargan recursos de orígenes diferentes al de donde proviene la página. Estos scripts de terceros ralentizan tu sitio y hacen que sea más complicado elaborar una estricta directiva de seguridad del contenido, además de ofrecer un acceso completo a tu sitio al tercero. Al utilizar Compute@Edge y un proxy basado en el edge, puede que haya una forma mejor.

Nuestro Developer Hub es un ejemplo perfecto de sitio web generado de forma estadística que sirve la mayoría de sus páginas y recursos de un cubo de almacenamiento en la nube (Google Cloud Storage, en nuestro caso). Pero, al igual que muchos otros sitios web, esas páginas toman recursos de otros dominios. Por ejemplo, nosotros utilizamos: 

  • Google Analytics para medir el tráfico.

  • Sentry para elaborar informes y agregar errores de Javascript.

  • FormKeep para recopilar la opinión y los comentarios de los usuarios.

  • Swiftype para buscar en el sitio web.

Estos proveedores son bastante conocidos, por lo que probablemente los estés utilizando en tu propio sitio web. Según un estudio de 2020 llevado a cabo por Ghostery, un asistente de privacidad para navegadores, un sitio web normal de noticias y contenido multimedia contiene más de 10 scripts de terceros solo para realizar el seguimiento.

Problemas de privacidad, seguridad, reglamentación y rendimiento

Lo que es indiscutible es que existen problemas de privacidad con scripts de terceros, especialmente aquellos dedicados al seguimiento de comportamientos; los complementos como Ghostery son la manera perfecta para que los usuarios finales protejan su privacidad. De hecho, estas protecciones se están diseñando cada vez más como navegadores.

Los gobiernos también están adoptando medidas más estrictas.  Un tribunal de primera instancia en Alemania multó hace poco a un operador de sitios web por utilizar Google Fonts, basándose en que, al hacerlo, compartía la dirección IP de los usuarios finales con Google.

Aun así, aunque el propietario del sitio web es el que decide utilizar estos servicios de terceros, realmente no tienen mucho control (si lo hay) sobre qué hace ese tercero o sobre qué datos recopila. De hecho, en algunos casos, puede que el personal de ingeniería no conozca en absoluto lo que se carga en el sitio si se utilizan herramientas como Google Tag Manager para delegar el control sobre scripts de terceros a otros equipos dentro de la organización. 

Quizás, si como desarrolladores tuviéramos un control más directo sobre el comportamiento de los scripts de terceros, podríamos proteger mejor los intereses de nuestros usuarios finales y seguir obteniendo las ventajas de los servicios que nos ofrecen esos terceros.

Además, no solo se trata de la privacidad. Añádele un par de rastreadores, análisis, fuentes y demás, y verás cómo de repente tus usuarios estarán obteniendo cosas de 20, 30, 50 o incluso más dominios. Y eso solo para representar una página web.

En la práctica, esto significa que no puedes elaborar una directiva de seguridad del contenido eficaz; los navegadores deben realizar varias conexiones TCP independientes a diferentes servidores (por lo que no se les podrá dar prioridad de forma eficiente) y la disponibilidad de tu sitio dependerá de la disponibilidad de dichos terceros.  ¿Y qué ocurre si el proveedor de la fuente se cae o se bloquea en algún país y tu sitio web se representa como una página en blanco?

Proxy al rescate

Si pones en servicio un sitio web mediante Fastly, ya dispondrás de un proxy inverso desplegado en el edge con una seguridad adecuada y el protocolo de asistencia más reciente, capaz de presentar un dominio único al mundo y de redirigir peticiones a varios servidores backend diferentes. Muchos de nuestros clientes utilizan esta característica para crear una arquitectura de microservicios en el edge.

Puede aplicarse el mismo principio a muchos scripts de terceros del proxy. Veamos cómo funciona:

  1. El script de tercero (p. ej., www.google-analytics.com) se añade a tu servicio Fastly como un nuevo backend.

  2. La etiqueta <script> en el código HTML se modifica para cargarse desde una ruta local como,por ejemplo, /services/analytics.

  3. Fastly transforma las peticiones para esa ruta en la ruta de backend correcta y se redirigen a ese backend.

  4. Fastly accede al código de biblioteca que sirve el tercero y lo transforma como sea necesario; por ejemplo, para buscar y reemplazar la URL recopiladora de datos de este tercero con el punto de conexión de tu proxy (esto puede ser algo arriesgado, pero hablaremos de ello más tarde).

  5. Las posteriores peticiones que realice el tercero se enviarán a tu servicio de Fastly, se inspeccionarán y se filtrarán según tus necesidades, y luego se reenviarán al tercero.

Con este patrón en marcha, podremos obtener las siguientes ventajas:

  • Una directiva de seguridad del contenido estricta.

  • Una efectividad máxima del establecimiento de prioridades de HTTP.

  • La protección ante interrupciones causadas por terceros.

  • El control del intercambio de datos con el tercero.

  • La elusión de complementos de bloqueo/filtrado por parte del cliente.

Y la última es algo… polémica. Pero voy a presuponer que, ya que estás dispuesto a meterte en todo el meollo del proxy con terceros, te preocupas de minimizar el impacto en tus usuarios finales. Echémosle un vistazo a cómo podemos implementar esto para algunos de los terceros que utilizamos en Developer Hub.

Developer Hub es una aplicación GatsbyJS ofrecida por un servicio Compute@Edge desarrollado en JavaScript. Si quieres saber cómo la migramos a Compute@Edge, puedes ver esta entrada anterior del blog.

API de HTTP (FormKeep y Swiftype)

Digámoslo de forma sencilla: algunos terceros no disponen de scripts, sino que son simples puntos de conexión API a los que consultamos desde nuestro propio script del lado del cliente. Por ejemplo, FormKeep recibe datos de nuestro formulario de comentarios en un POST HTTP y Swiftype muestra los resultados de las consultas de búsqueda. Trasladarlos al dominio principal es bastante directo.

Empieza por modificar tu programa Compute@Edge para que reconozca una ruta específica y las peticiones directas sobre esa ruta a un nuevo backend (le llamaremos «formkeep» en este ejemplo):

// src/index.js (Fastly Compute@Edge app)

const req = event.request
const reqUrl = new URL(req.url)
const reqPath = reqUrl.pathname

let backendName;

if (reqPath === "/api/internal/feedback") {
    backendName = "formkeep";
    reqUrl.pathname = "/f/xxxxxxxxxxxx"
} else {
    backendName = "gcs";
}

let beReq = new Request(reqUrl, req);

let beResp = await fetch(beReq, { backend: backendName });

return beResp;

Luego, modifica el comportamiento de tu aplicación frontend o página HTML para que envíe la petición API a la nueva ruta:

// feedback.html (client side HTML page)

async function handleFormSubmit(evt) {
  const data = new FormData(evt.target)
  buttonEl.current.disabled = true
  await fetch("/api/internal/feedback", {
    method: "post",
    body: data,
    headers: { accept: "application/json" },
  })
  setIsSubmitted(true)
}

Finalmente, añade el backend, ya sea en la interfaz web o utilizando la interfaz de la línea de comandos de Fastly y despliega la aplicación actualizada.

fastly backend create --name=formkeep --host=formkeep.com --version=active --autoclone
fastly compute publish

Las marcas --version=active y --autoclone harán que la versión activa actual del servicio sea clonada y que se añada el nuevo backend al clon, pero este no estará activado. El comando compute publish cargará el código actualizado a la versión de borrador del servicio y luego la activará.

Este tipo de integración de terceros es tan sencilla de asociar a Fastly que realmente no tendrás motivos para no hacerla.

Clientes configurables (Sentry)

Algunos servicios de terceros ofrecen un cliente JavaScript que necesita ejecutarse en el navegador, como el servicio de agregación de errores Sentry. Si tienes suerte, el proveedor permitirá que el nombre del host y la ruta de las peticiones realizadas por su cliente sean configurables.

Sentry es un buen ejemplo de uno que sí lo permite, utilizando la opción tunnel. Puedes configurarla donde fijes tu configuración de Sentry. Para Developer Hub, utilizamos el complemento Sentry para Gatsby y la configuración se encuentra en la matriz de plugins de nuestra gatsby-config.js:

// gatsby-config.js

{
  resolve: "@sentry/gatsby",
  options: {
    dsn: "https://#######@###.ingest.sentry.io/######",
    tunnel: "/api/internal/errors",
    sampleRate: 0.7,
    tracesSampleRate: 0.7,
    release: process.env.COMMIT_SHA,
  }
}

Si utilizas Sentry fuera de un marco de aplicaciones como Gatsby, lo más probable es que sitúes la opción tunnel donde realices la llamada Sentry.init.

Ahora, modifica tu aplicación Compute@Edge para añadir la nueva ruta y vuelve a asignarla a la ruta que espera Sentry:

// src/index.js (Fastly Compute@Edge app)

if (reqPath === "/api/internal/feedback") {
    backendName = "formkeep";
    reqUrl.pathname = "/f/xxxxxxxxxxx"
} else if (reqPath === "/api/internal/errors") {
    backendName = "sentry";
    reqUrl.pathname = "/api/" + SENTRY_PROJECT_ID + "/envelope/"
} else {
    backendName = "gcs";
}

Como antes, necesitas añadir el nuevo backend, haciendo que coincida con el nombre que utilizaste en el código, y luego despliega una nueva versión de tu programa:

fastly backend create --name=sentry --host=oXXXXXXXX.ingest.sentry.io --version=active --autoclone
fastly compute publish

Otra ventaja del complemento Gatsby de Sentry es que aúna el código de cliente de Sentry en el paquete de nuestro sitio, por lo que ya no tendremos que preocuparnos por la petición que carga la propia biblioteca, solo por la petición que envía datos a los recopiladores de Sentry.

Reescritura de clientes de forma dinámica (Google Analytics)

Otros scripts requieren algo más de asistencia. Google Analytics (GA) integra la URL de destino como parte del código en su script de seguimiento y el complemento Gatsby para GA carga la biblioteca directamente desde Google. En estos casos, podrías probar internamente una versión modificada de la biblioteca del cliente, pero luego no recibirías las actualizaciones que el proveedor llevara a cabo en el código de su cliente.

En lugar de esto, podemos utilizar una transformación de envíos en Compute@Edge para modificar estas URL sobre la marcha. 

Podemos utilizar esta misma técnica con Google Fonts, ya que el CSS que devuelve Google carga los archivos de fuente y estos también deben redirigirse mediante el dominio principal. El cliente de Fastly Houzz utiliza esta solución para crear un método de conservación de la privacidad al cargar fuentes de Google.

En primer lugar, añade una función para que realice un sencillo buscar y reemplazar en una secuencia:

// src/index.js (Fastly Compute@Edge app)

const streamReplace = (inputStream, targetStr, replacementStr) => {
  let buffer = ""
  const decoder = new TextDecoder()
  const encoder = new TextEncoder()
  const inputReader = inputStream.getReader()
  const outputStream = new ReadableStream({
    start() {
      buffer = ""
    },
    pull(controller) {
      return inputReader.read().then(({ value: chunk, done: readerDone }) => {
        buffer += decoder.decode(chunk)

        if (buffer.length > targetStr.length) {
          buffer = buffer.replaceAll(targetStr, replacementStr)
          controller.enqueue(encoder.encode(buffer.slice(0, buffer.length - targetStr.length)))
          buffer = buffer.slice(0 - targetStr.length)
        }

        // Flush the queue, and close the stream if we're done
        if (readerDone) {
          controller.enqueue(encoder.encode(buffer))
          controller.close()
        } else {
          controller.enqueue(encoder.encode(""))
        }
      })
    },
  })
  return outputStream
}

Justo después de acceder al backend, beResp.body será una secuencia legible. Al utilizar la función de reemplazo de secuencias, podremos sustituir el dominio de GA con el nuestro:

let beResp = await fetch(beReq, { backend: backendName });

const respContentType = beResp.headers.get("content-type") || ""
if (respContentType.startsWith("text/")) {
  const newRespStream = streamReplace(
    beResp.body,
    "www.google-analytics.com",
    "developer.fastly.com/api/internal/analytics"
  )
  beResp = new Response(newRespStream, { headers: beResp.headers })
}

return beResp;

El complemento de Gatsby para GA integrará la etiqueta <script> como parte del código en cada página, por lo que será necesario aplicar esto a todas las respuestas de texto, ya que cada página HTML contiene tanto los nombres de host integrados en el código en la propia biblioteca analytics.js como el marcado para cargar la biblioteca. La etiqueta gatsby-plugin-google-gtag parece ser una alternativa que permite que la etiqueta <script> se pueda reescribir en una ruta local, pero, para el fin de este artículo, pensé que era más importante hablar de una solución alternativa definitiva que funcionara casi para cualquier cosa.

Ten en cuenta que reescribir código de terceros como este conlleva un riesgo intrínseco claro. Algunas bibliotecas de terceros pueden acceder a varios nombres de host; estas pueden cambiar el nombre del host al que acceden. Y algunas pueden incluso ofuscar la construcción de URL para evitar exactamente este tipo de reescritura. Hemos tenido buenas experiencias haciendo esto en Google Analytics y Google Fonts.

Si solo necesitas aplicar la transformación a una URL, podrías cambiar la instrucción if para que compruebe la variable reqPath que definimos anteriormente.

Ahora, añade la nueva ruta al código de enrutamiento:

if (reqPath === "/api/internal/feedback") {
    backendName = "formkeep";
    reqUrl.pathname = "/f/xxxxxxxxxxx"
} else if (reqPath === "/api/internal/errors") {
    backendName = "sentry";
    reqUrl.pathname = "/api/" + SENTRY_PROJECT_ID + "/envelope/"
} else if (reqPath.startsWith("/api/internal/analytics")) {
    backendName = "ga";
    reqUrl.pathname = reqPath.replace("/api/internal/analytics/", "/")
} else {
    backendName = "gcs";
}

Y, por supuesto, también tenemos que añadir el backend de Google Analytics, cargar el nuevo código y activar la nueva versión del servicio:

fastly backend create --name=ga --host=www.google-analytics.com --version=active --autoclone
fastly compute publish

Eliminar las cookies de las peticiones

Al dirigir todas las peticiones a tu dominio, ya estarás ejerciendo mucho más control sobre el comportamiento de los terceros. Por ejemplo, el tercero ya no verá la dirección IP de tus usuarios finales y, en su lugar, todas las peticiones que les envíes provendrán de Fastly.

También puedes despojarte de forma proactiva de los datos innecesarios de la petición. Probablemente, lo más importante que debes tener en cuenta son los encabezados X-Forwarded-For, Fastly-Client-IP y Cookie, que de lo contrario filtrarán datos personales al tercero y anularán todas las ventajas de privacidad del proxy. Es muy sencillo eliminar esto justo antes de enviar la petición al backend:

beReq.headers.delete("cookie");
beReq.headers.delete("x-forwarded-for");
beReq.headers.delete("fastly-client-ip");

Hay muchas otras cosas que puedes hacer además de esto, incluso filtrar el contenido del cuerpo de la petición o copiar una muestra de este en un punto de conexión del registro para su inspección.

Conclusión

La consolidación de todos los recursos y peticiones de tu sitio en un solo dominio conlleva algunas ventajas importantes y ayuda a mantener a raya un rendimiento accidental y la involución en la privacidad. Compute@Edge y el edge computing en general prometen hacer de estas tareas algo cada vez más fácil, pero ya podemos aprovechar algunas potentes formas de modelar cómo se cargan nuestros sitios.