Si se abre la URL de la imagen directamente, se muestra correctamente. Al solicitar la misma dirección desde el servidor, se devuelve un HTTP 200 y el cuerpo de la respuesta es un JPEG real. Sin embargo, en el elemento de imagen de la página web se produce un error de carga.

Para explicar este fenómeno, es necesario dividir el éxito de la solicitud en varias etapas. Si se recibieron los bytes, si se pueden interpretar como imagen y si el documento actual puede utilizar esa respuesta son condiciones distintas. Al añadir una ruta de reenvío de imágenes, también hay que decidir hasta qué alcance debe asumirlas el servidor para cada una de estas condiciones.

Este artículo analiza dichos límites desde la perspectiva de CORP, el rebind de DNS, el streaming asincrónico y la decodificación de imágenes. Los ejemplos de código se basan en Python 3.11 o superior y HTTPX 0.28.1. La descripción de la capa de conexión se apoya en httpcore 1.0.9 y la del procesamiento de imágenes en la documentación oficial de Pillow 13.0.0. La fecha de revisión de la documentación es el 4 de noviembre de 2026.

1. HTTP 200 no incluye la autorización de uso del navegador

Supongamos que la respuesta es la siguiente.

http
HTTP/1.1 200 OK
Content-Type: image/jpeg
Cross-Origin-Resource-Policy: same-site

Si un documento de otro sitio carga esta respuesta como imagen, el navegador puede bloquear su uso. CORP es una política con la que el proveedor de recursos restringe desde qué orígenes se pueden utilizar sus respuestas. La especificación Fetch define una comprobación de CORP independiente para las respuestas a solicitudes no-cors. Que exista un modo de solicitud no-cors no significa que se omitan todas las comprobaciones de seguridad de la respuesta. Definición de CORP en la especificación Fetch

Al diagnosticar el problema, conviene separar los tres niveles siguientes.

Nivel a verificarSignificado del éxitoFallos aún posibles
Intercambio HTTPSe recibieron el código de estado y el cuerpo de la respuestaEl contenido es una página de error o no se ajusta a los permisos
Interpretación del formatoEl decodificador de imágenes identifica el formatoMarco dañado, número excesivo de píxeles, diferencias de compatibilidad entre navegadores
Uso dentro del documentoLa respuesta puede utilizarse en el contexto de seguridad de dicho documentoCORP, política de recursos del documento, etc.

También es necesario distinguir CORS de CORP. CORS es un protocolo para compartir respuestas entre orígenes distintos, mientras que CORP restringe el uso de recursos específicos de origen y sitio cruzado. Aunque la visualización de imágenes de origen cruzado no siempre requiere autorización CORS, CORP sí puede impedir dicho uso. Incluso si se añade crossorigin a la imagen para convertirla en una solicitud CORS, el servidor de origen debe proporcionar las respuestas de autorización necesarias. Protocolo CORS en la especificación Fetch

La observación de que la descarga tuvo éxito desde el servidor sirve para acotar el análisis a la red y al formato del archivo. Si se interpreta de forma exagerada como prueba de que se han superado también las políticas del navegador, se pasa por alto la causa.

2. El servidor de reenvío se convierte en emisor de una nueva respuesta

Si se adopta una estructura en la que el servidor obtiene recursos externos permitidos y los ofrece como vista previa, la ruta de la solicitud cambia de la siguiente manera.

text
브라우저
  ├─ 원본 이미지 직접 요청
  └─ 실패 시 인증된 미리보기 요청
        → 목적지 정책 검사
        → 검증된 주소로 연결
        → 제한된 크기의 본문 수신
        → 허용한 이미지 형식 검증
        → 새 응답 생성

Lo que devuelve el servidor de reenvío es una nueva respuesta HTTP emitida desde su propio origen. Por tanto, debe configurarse explícitamente la cabecera de la respuesta. Si se transmite el Set-Cookie original, se mezclan las configuraciones de estado del servidor externo con la respuesta del servicio de reenvío. El Content-Type original tampoco puede sustituir a los resultados de la verificación. El tipo MIME y la política de caché que se devuelvan deben determinarse como contrato de la ruta de reenvío.

La dirección de la solicitud sigue el mismo principio. Las cookies de sesión del usuario y los encabezados de autenticación del servicio no se envían al servidor de imágenes de origen. Autenticar una solicitud de vista previa y transmitir credenciales a un destino externo son actos independientes entre sí.

Esta estructura implica un desplazamiento de permisos. Al ejecutar en lugar del navegador las solicitudes externas, se emplea el alcance de red del servidor. Aunque el usuario esté autenticado, ello no le otorga el derecho de solicitar como URL de imagen direcciones de servicios internos o de metadatos.

3. La comprobación de DNS debe vincularse a la conexión real

Es natural implementar una resolución única del host de la URL y verificar que no contiene direcciones privadas. No obstante, si el cliente HTTP vuelve a resolver el mismo host tras la comprobación, se genera una brecha temporal.

text
t0: 검증 단계의 DNS 응답 → 허용한 공인 주소
t1: 연결 단계의 DNS 응답 → 허용하지 않은 내부 주소

Se trata del problema TOCTOU, en el que el resultado de la verificación no vincula el destino del socket real. Hay que acoplar la verificación del destino y la conexión a dicha dirección. OWASP también trata la reinterpretación de DNS y las redirecciones como elementos a revisar en la defensa contra SSRF. Guía de defensa SSRF

La condición invariante necesaria aquí es simple.

La IP elegida al establecer una nueva conexión debe pertenecer al conjunto de direcciones verificadas conforme a la política aplicable a dicha conexión.

Cuando un host devuelve varias direcciones, hay que tratar conjuntamente IPv4 e IPv6. También es una política explícita decidir si se rechaza la solicitud completa cuando algunas direcciones están denegadas, o si solo se mantienen como candidatas las direcciones verificadas. Una implementación conservadora podría rechazar por completo una respuesta mixta, pero en entornos con DNS dividido incluso las solicitudes legítimas fracasarían.

Aunque se fije la IP de conexión, debe preservarse el significado del host original.

Elemento de la conexiónValor que debe emplearse
Destino de la conexión TCPLa IP numérica verificada
Host HTTPEl host de la URL original y el puerto necesario
SNI de TLSEl nombre del host original
Objeto de verificación del certificadoEl nombre del host original

Una implementación que sustituya la propia URL por una dirección IP puede alterar simultáneamente el enrutamiento de host virtual y la verificación TLS. Se requiere una separación de capas que fije solo el destino de la conexión de red y mantenga la identificación del host en los protocolos de nivel superior.

Las redirecciones siguen el mismo principio. Desactivar el seguimiento automático facilita mantener una política de destino simple. Si se permite el seguimiento, hay que aplicar de nuevo a cada salto la política de URL y la política de IP de la nueva conexión, limitando también el número de saltos y el tiempo total. El resultado de la verificación de la primera dirección no se hereda por la siguiente.

Es importante no reutilizar tal cual en el cliente de vista previa externa las excepciones de direcciones internas necesarias para la comunicación entre servicios. Las políticas deben concederse por funcionalidad. Aunque se emplee la misma utilidad de red, el alcance permitido para la recopilación de recursos públicos y para las llamadas a servicios internos debe poder separarse.

4. Diferenciar entre el Transport de HTTPX y el Network Backend de httpcore

HTTPX ofrece una API de Transport en el límite de solicitudes y respuestas. httpcore gestiona el agrupamiento de conexiones y el comportamiento de red real, y permite conectar un Network Backend personalizado. La fijación de la IP de destino solo tiene efecto hasta el punto en que se establece la conexión real. HTTPX Transport API, httpcore Network Backend

En una implementación real hay que distinguir entre los puntos de extensión públicos y la estructura interna de la biblioteca. Por ejemplo, sustituir _pool._network_backend dentro del Transport de HTTPX 0.28.1 depende de atributos internos. Es necesario concentrar ese ámbito de dependencia en un pequeño adaptador, fijar la versión y verificar, al actualizar, que la ruta de conexión real no haya cambiado. Que exista una interfaz pública Network Backend no convierte en contrato estable todos los métodos de ensamblaje de la biblioteca superior.

Los proxy de entorno también forman parte del modelo de conexión. Si se emplean automáticamente HTTP_PROXY o HTTPS_PROXY, puede conectarse a un proxy en lugar del servidor verificado directamente. Si el diseño presupone una conexión directa, debe forzarse dicha premisa mediante trust_env=False, etc. En entornos donde se necesita un proxy, debe diseñarse por separado la resolución DNS y el control de destino que este realiza. Documentación de variables de entorno de HTTPX

Compartir el agrupamiento de conexiones aporta ventajas en la reutilización de conexiones y en el coste de TLS. Al mismo tiempo, hay que considerar el estado de autenticación por usuario, la política de destino y el tratamiento de las conexiones existentes cuando cambia la política. Un cliente por solicitud ofrece un aislamiento simple, pero reduce los beneficios de la reutilización de conexiones. Si se comparte el agrupamiento por rendimiento, primero hay que decidir qué contextos de seguridad comparten entre sí.

5. El tamaño de la respuesta, el tiempo total y el coste de decodificación son presupuestos distintos

Content-Length puede emplearse para una estimación preliminar, pero no sustituye la comprobación acumulativa de los bytes recibidos. Puede faltar o diferir del tamaño real del cuerpo procesable debido a la compresión de contenido HTTP.

En la API de streaming de HTTPX es posible limitar el tamaño mientras se leen los fragmentos. El código siguiente muestra únicamente la parte que restringe el tamaño del cuerpo y el tiempo de espera, bajo la premisa de que ya se ha recibido un cliente con la política de destino aplicada. No es el código completo de un proxy que implemente la verificación de URL o la fijación de DNS.

python
import asyncio
import httpx


class PreviewTooLarge(Exception):
    pass


async def read_bounded(
    client: httpx.AsyncClient,
    url: str,
    *,
    max_bytes: int = 10 * 1024 * 1024,
    budget: float = 15.0,
) -> bytes:
    async with asyncio.timeout(budget):
        async with client.stream("GET", url) as response:
            response.raise_for_status()
            body = bytearray()

            async for chunk in response.aiter_bytes(
                chunk_size=64 * 1024
            ):
                if len(body) + len(chunk) > max_bytes:
                    raise PreviewTooLarge()
                body.extend(chunk)

            return bytes(body)

Los valores de 10 MiB y 15 segundos son ejemplos de diseño. Deben ajustarse en función de la distribución del tamaño de los recursos y del número de solicitudes concurrentes que pueda soportar el servidor.

El read timeout de HTTPX se aplica al tiempo de espera del siguiente fragmento. Si los datos llegan continuamente en pequeñas cantidades, el tiempo total de la solicitud puede alargarse. Envolver la solicitud y la recepción del cuerpo con asyncio.timeout() permite establecer un presupuesto independiente de tiempo de espera total. Sin embargo, no es una función que fuerce la finalización en un momento fijo de operaciones sincrónicas que no colaboren con la cancelación. Documentación de timeouts de HTTPX, Documentación de timeout de Python asyncio

También debe considerarse el momento de aplicación del límite de tamaño. aiter_bytes() proporciona bytes a los que ya se ha aplicado la decodificación de contenido HTTP. El código anterior limita el tamaño del cuerpo que conserva la aplicación, pero no restringe con el mismo valor el uso temporal de memoria interno del decodificador. Un pequeño chunk_size tampoco es un límite superior de la memoria total del proceso. Documentación de respuestas de streaming de HTTPX

Si se requieren límites estrictos de recursos, hay que distinguir por separado los bytes de la capa de transporte, los bytes tras la descompresión HTTP y los píxeles decodificados de la imagen. Para que el presupuesto total de memoria se cumpla, además del límite por solicitud debe limitarse también la capacidad de procesamiento concurrente.

text
메모리 수요 ≈ 동시 요청 수 ×
    (본문 버퍼 + 디코딩된 픽셀 + 코덱 작업 공간)

Esta fórmula es un modelo aproximado de presupuesto. Dado que se suman copias de búfer, estado de conexión y comportamiento por códec, no representa un límite superior exacto.

6. La identificación, verificación y decodificación de Pillow son etapas distintas

En el procesamiento de imágenes también hay que leer con precisión el alcance del éxito de cada función.

Image.open() de Pillow emplea procesamiento diferido. Que se haya identificado el formato de archivo no significa que se hayan decodificado todos los píxeles. verify() es también una API que comprueba problemas en el archivo sin decodificar por completo los datos de la imagen. Interpretar esto como garantía de decodificación completa de todos los formatos y fotogramas conduce a sobrevalorar el alcance de la verificación. Documentación de Image de Pillow, Documentación de Image.verify

Por tanto, conviene definir primero el contrato de la ruta de vista previa. El coste y el alcance de la verificación varían según si se aceptan solo formatos raster permitidos, si se devuelve un fotograma estático único o si se conserva toda la animación.

Para una vista previa estática puede elegirse limitar el ancho, el alto y el número total de píxeles, decodificar el fotograma necesario y recodificarlo en el formato establecido. Si se mantiene la animación, el número de fotogramas y la capacidad de procesamiento acumulada también entran en el presupuesto. La recodificación supone costes de procesamiento y alteraciones de la representación, por lo que difícilmente puede considerarse un valor predeterminado aplicable incondicionalmente a todas las imágenes.

Ejecutar el decodificador en un hilo aparte puede reducir la latencia del bucle de eventos. Sin embargo, no debe asumirse que la cancelación de la espera de una coroutina termine de inmediato una tarea de hilo ya en ejecución. Si se requiere aislamiento de tiempo de ejecución con capacidad de finalización forzada, debe evaluarse también el uso de procesos separados y límites de recursos del sistema operativo.

7. La ruta de recuperación necesita una condición de finalización y una generación de solicitud

El onerror de una imagen no aporta información suficiente para clasificar la causa. El bloqueo por política, una dirección caducada, un error de formato y un fallo de red pueden manifestarse como el mismo evento.

Así, si la carga directa falla, conviene establecer una transición de estado que intente de forma limitada el reenvío por servidor, sin atribuir el fallo a una causa específica.

text
직접 요청 ─성공→ 표시
    │ 실패
    ▼
중계 요청 ─성공→ 표시
    │ 실패
    ▼
오류 안내와 사용자가 선택하는 재시도

Si se añade recuperación de sesión, debe limitarse a las direcciones de API de la propia propiedad del servicio y regular por separado el número de intentos automáticos. Si todos los fallos de imágenes externas desencadenan la renovación de sesión, averías ajenas terminan propagándose al manejo de sesiones.

Otro límite es la vida temporal de la solicitud. Mientras la imagen A espera su recuperación, la pantalla puede cambiar a la imagen B. Si la operación tardía de A vuelve a solicitar B o borra su estado de error, se mezclan los estados. Hay que impedir la aplicación de resultados obsoletos mediante el ciclo de vida de los componentes por origen, números de generación y señales de cancelación.

También debe evitarse modificar arbitrariamente las URL de origen firmadas con fines de recuperación. El identificador de reintento puede adjuntarse a la propia solicitud de reenvío, mientras que la URL original puede transmitirse como valor de parámetro separado. La preservación exacta de la cadena original y la verificación del destino son condiciones que deben cumplirse conjuntamente.

8. Las pruebas deben descender hasta la capa donde se aplica la política real

Una prueba que simule íntegramente la función de verificación de destino no puede comprobar que el resultado de la verificación coincida con el destino de la conexión de socket. Las pruebas que sustituyen la respuesta HTTP mediante MockTransport de HTTPX tampoco ejecutan la conexión de red subyacente.

Para verificar el límite de conexión, puede registrarse la dirección realmente solicitada por el backend de red inferior o avanzar el comportamiento posterior a la respuesta usando los flujos simulados de httpcore. Por ejemplo, tras recibir una redirección desde una dirección pública, confirmar que la conexión siguiente a una dirección interna se rechaza antes de crear el socket real.

Situación de verificaciónResultado a observar
Cambio de dirección entre la comprobación DNS y la conexiónLa reinterpretación ajena a la comprobación no altera la conexión
Respuesta con mezcla de direcciones públicas y no públicasSe trata conforme a la política de conjuntos de direcciones especificada
Redirección de una dirección pública a una internaSe rechaza antes de la conexión del socket interno
Fragmento que supera el límite de tamañoSe rechaza antes de añadirlo al búfer
Respuesta lenta pero ininterrumpidaFinaliza conforme al presupuesto total de tiempo de espera
HTML o imagen dañada que finge un tipo MIMENo se emite como respuesta de imagen
Resultado de recuperación anterior que llega tras cambiar la fuente de la imagenNo se aplica al estado actual de la imagen

También es necesaria la verificación en el navegador. El complete del elemento de imagen puede seguir siendo verdadero incluso tras un fallo de carga, por lo que no basta para juzgar el éxito. Si se trata de una imagen raster, deben comprobarse conjuntamente condiciones como naturalWidth > 0 y el renderizado real, así como las rutas que generan solicitudes adicionales, como la ampliación.

Por último, aunque el transporte y el renderizado sean correctos, subsiste la cuestión de si el contenido de la imagen es coherente con el tema. La fiabilidad del destino de red, la idoneidad del formato de archivo y la coherencia semántica del contenido son objetos de verificación distintos.

Los criterios de diseño del reenvío de imágenes externas no residen en el éxito de ninguna función concreta. Hay que poder explicar en cada límite si la conexión se realizó al destino verificado, si el procesamiento se ajustó al presupuesto establecido, si se emitió como nueva respuesta únicamente la representación permitida y si los resultados obsoletos de recuperación no alteran el estado actual.