Aller au contenu principal

Politique Same-Origin et proxies web : analyse technique de la sécurité

Écrit par
Smith
Smith
Temps de lecture
17 min de lecture
Publié le
2 mars 2026
Politique Same-Origin et proxies web : analyse technique de la sécurité

En construisant ProxyOrb, nous nous sommes heurtés à un paradoxe fondamental à chaque décision de conception : un proxy web fonctionne en faisant croire au navigateur que du contenu tiers est du contenu same-origin. C'est, par définition, tromper le modèle de sécurité central du navigateur. Or les navigateurs modernes ont passé quinze ans à empiler des défenses de plus en plus sophistiquées précisément contre ce type de confusion d'origine.

Cet article examine cette tension depuis ses fondements. Si vous êtes chercheur en sécurité, pentesteur, ou ingénieur en sécurité navigateur et que vous souhaitez comprendre comment les proxies web interagissent réellement avec la politique same-origin — pas au niveau conceptuel, mais au niveau des en-têtes HTTP, du code source de Chromium et des compromis d'ingénierie en production — cet article est fait pour vous.

Nous couvrirons les bases de la SOP, la réécriture d'URL, CORS, CORB/CORP, COEP/COOP, les Service Workers, les iframes, ainsi que les implications sécuritaires pour les opérateurs de proxy et leurs utilisateurs.


1. Les fondements de la politique same-origin

La politique same-origin (SOP) est définie par un triplet : schéma + hôte + port. Deux URLs sont same-origin uniquement si ces trois composantes correspondent exactement. https://example.com:443 et http://example.com:80 sont cross-origin malgré le fait qu'elles pointent vers le même serveur.

Ce que l'on comprend souvent mal, c'est ce que la SOP empêche réellement par rapport à ce qu'elle autorise. La SOP ne bloque pas :

  • Le chargement d'images (<img src>) depuis d'autres origines
  • Le chargement de scripts (<script src>) depuis d'autres origines
  • Le chargement de feuilles de style (<link rel="stylesheet">) depuis d'autres origines
  • L'intégration d'iframes cross-origin (même si le contenu du document embarqué est isolé)
  • L'envoi de soumissions de formulaires (<form action>) vers d'autres origines

Ce que la SOP empêche, c'est de lire la réponse des requêtes cross-origin effectuées via fetch() ou XMLHttpRequest. La requête part bien — l'aller-retour réseau a lieu — mais le corps et les en-têtes de la réponse sont cachés au JavaScript qui s'exécute dans une origine différente.

Cette distinction est cruciale pour les proxies web. L'objectif d'un proxy, c'est précisément de fusionner deux origines (le serveur proxy et le serveur cible) en une seule, supprimant ainsi la frontière cross-origin. Dès lors que toutes les ressources semblent provenir de https://proxyorb.com, les restrictions de la SOP sur la lecture des réponses ne s'appliquent tout simplement plus.

La « zone légitime » qu'exploitent les proxies web, c'est la réécriture d'URL : au lieu de https://example.com/api/data, chaque requête devient https://proxyorb.com/api/data?__pot=aHR0cHM6Ly9leGFtcGxlLmNvbQ==. Du point de vue du navigateur, il s'agit d'une requête same-origin. La SOP n'a pas été contournée — elle a été rendue sans objet en changeant l'origine apparente de tout le contenu.


2. L'architecture de réécriture d'URL de ProxyOrb

Pour saisir les implications sécuritaires, il faut comprendre le mécanisme d'encodage. ProxyOrb utilise un paramètre d'URL nommé __pot (proxy origin token) qui contient l'origine cible encodée en Base64.

Le paramètre __pot

Le paramètre __pot encode toujours l'origine (schéma + hôte, sans le chemin) de la cible, et non l'URL complète. Cela signifie que https://example.com/some/deep/path?foo=bar et https://example.com/other/page produisent tous deux la même valeur __pot : aHR0cHM6Ly9leGFtcGxlLmNvbQ== (l'encodage Base64 de https://example.com). Le chemin et la chaîne de requête réels sont conservés tels quels dans le chemin et la chaîne de requête de l'URL proxy.

Lorsqu'un utilisateur navigue vers https://proxyorb.com/?__pot=aHR0cHM6Ly9leGFtcGxlLmNvbQ==, la passerelle le décode et reconstruit l'URL d'origine :

-- Gateway pseudocode: decoding the proxy request

function resolve_original_url(proxy_url):
    pot_value = parse_query_param(proxy_url, "__pot")
    if not pot_value:
        return error("Missing origin token")

    original_origin = base64_decode(pot_value)
    -- e.g. "https://example.com"

    validate_not_private_ip(original_origin)  -- SSRF protection

    -- Replace the proxy host with the target host, keep path/query intact
    original_url = replace_origin(proxy_url, original_origin)
    return original_url

La passerelle transmet ensuite la requête au vrai serveur cible, en réécrivant l'en-tête Origin de sorte que le serveur amont voie son propre domaine plutôt que celui du proxy :

-- Set outbound headers toward the target server

request.headers["Host"]   = target_host
request.headers["Origin"] = target_origin   -- e.g. "https://example.com"
-- Remove all internal proxy headers before forwarding

Réécriture d'URL côté client via Service Worker

La passerelle gère le côté serveur. Le côté client — réécriture des URLs dans les réponses HTML, JavaScript et CSS pour que toutes les sous-requêtes continuent de passer par le proxy — est géré par un Service Worker.

La transformation centrale convertit toute URL rencontrée dans le contenu d'une page au format proxy :

-- Service Worker pseudocode: toProxyURL()

function toProxyURL(originalUrl, currentPageUrl):
    if isSameOrigin(originalUrl, currentPageUrl):
        return originalUrl   -- already proxy-origin, no rewrite needed

    -- Extract path/query/hash from the original URL
    -- Build a new URL rooted at the proxy origin
    proxyPath   = extractPathAndQuery(originalUrl)
    potValue    = base64encode(extractOrigin(originalUrl))
    proxyUrl    = proxy_origin + proxyPath + "?__pot=" + potValue

    return proxyUrl

Par exemple, https://cdn.example.com/bundle.js référencé dans une page proxifiée devient https://proxyorb.com/bundle.js?__pot=aHR0cHM6Ly9jZG4uZXhhbXBsZS5jb20=.

Cette réécriture est exhaustive : elle couvre fetch(), XMLHttpRequest, <script src>, <img src>, les connexions WebSocket et les balises <link>. Quand chaque URL de la page pointe vers l'origine du proxy, le navigateur n'effectue jamais de requête cross-origin — chaque requête est same-origin par construction.

Récupération du __pot sans navigation complète

Un cas limite subtil survient avec les requêtes qui proviennent de l'intérieur d'une page proxifiée mais qui n'ont pas le paramètre __pot — par exemple, un fetch('/api/data') relatif qui se déclenche avant que le Service Worker ait réécrit l'URL, ou une requête émise par un script injecté dynamiquement qui a contourné le réécriture d'URL.

Le Service Worker résout le token manquant via une recherche en cascade :

-- Service Worker pseudocode: recovering the origin token

function resolveMissingPot(fetchEvent):
    candidates = [
        getUrlOfControlledTab(fetchEvent.clientId),   -- most reliable
        inferUrlFromFetchEvent(fetchEvent),             -- from clientId / resultingClientId
        fetchEvent.request.referrer,                    -- referrer header
    ]

    for url in candidates:
        pot = extractQueryParam(url, "__pot")
        if pot: return pot

    return null   -- cannot recover; let the gateway handle or reject

La passerelle dispose d'un chemin de récupération parallèle : si une requête arrive sans __pot mais porte un en-tête Referer pointant vers une URL same-origin qui contient bien __pot, la passerelle extrait le token du referer et l'attache à la requête courante. Cela gère les scénarios de navigation où le token a été supprimé par le JavaScript de la page avant que la passerelle ne reçoive la requête.


3. CORS : le système explicite d'autorisation cross-origin

CORS (Cross-Origin Resource Sharing) a été conçu pour permettre aux serveurs de donner leur accord aux accès cross-origin en envoyant des en-têtes de réponse spécifiques. Du point de vue du proxy web, CORS crée deux problèmes distincts.

Problème 1 : interception des préflights CORS

Quand du JavaScript en cours d'exécution sur une page fait une requête fetch() avec des en-têtes non simples (ex. Authorization, Content-Type: application/json), le navigateur envoie d'abord une requête OPTIONS de preflight. Si la réponse preflight du serveur cible n'inclut pas des en-têtes CORS permissifs, la vraie requête est bloquée.

Dans l'architecture de ProxyOrb, le Service Worker intercepte toutes les requêtes OPTIONS et y répond de manière synthétique — avant même qu'elles n'atteignent la passerelle — en renvoyant une réponse qui débloque la vraie requête :

-- Service Worker pseudocode: synthetic CORS preflight

function handlePreflight(request):
    origin = request.headers["Origin"] or "*"

    return Response(status=204, headers={
        "Access-Control-Allow-Origin":      origin,
        "Access-Control-Allow-Methods":     "GET, POST, PUT, DELETE, OPTIONS, PATCH, HEAD",
        "Access-Control-Allow-Headers":     "*",
        "Access-Control-Allow-Credentials": "true",
        "Access-Control-Max-Age":           "86400",
        "Vary":                             "Origin",
    })

Au niveau de la passerelle, chaque réponse proxifiée reçoit des en-têtes CORS équivalents :

-- Gateway pseudocode: set CORS on outbound response

response.headers["Access-Control-Allow-Origin"]      = original_origin
response.headers["Access-Control-Allow-Headers"]     = "*"
response.headers["Access-Control-Allow-Credentials"] = "true"

Problème 2 : les requêtes avec credentials

La combinaison Access-Control-Allow-Credentials: true et un Access-Control-Allow-Origin non générique est significative. CORS exige une valeur d'origine explicite (pas *) dès lors que des credentials sont inclus. Le Service Worker émet toutes les requêtes sortantes avec credentials: 'include', donc la passerelle doit renvoyer en écho l'origine exacte de la requête plutôt qu'un wildcard.

Cela signifie que tous les cookies stockés sous proxyorb.com — accumulés depuis tous les sites cibles que l'utilisateur a visités via le proxy — sont envoyés à chaque requête proxifiée. C'est intentionnel (cela maintient l'état de session pour les sites où l'utilisateur est connecté), mais c'est aussi une considération sécuritaire importante que nous abordons en section 8.

Le pattern « strip-and-replace » des en-têtes CORS

Une réponse proxifiée peut porter des en-têtes CORS issus du serveur cible qui sont incorrects du point de vue du navigateur (ils référencent l'origine cible, pas l'origine du proxy). Le Service Worker les supprime et les remplace :

-- Service Worker pseudocode: sanitize response headers

function sanitizeResponseHeaders(response):
    headers = copy(response.headers)

    -- Remove target-site directives that would confuse the browser
    headers.delete("Content-Security-Policy")
    headers.delete("Content-Security-Policy-Report-Only")
    headers.delete("X-Frame-Options")
    headers.delete("X-Content-Type-Options")

    -- Replace with proxy-origin-correct CORS headers
    headers.set("Access-Control-Allow-Origin",      proxy_origin)
    headers.set("Access-Control-Allow-Credentials", "true")
    headers.set("Access-Control-Allow-Headers",     "*")
    headers.set("Vary", "Origin")

    return headers

4. CORB et CORP : les défenses renforcées de Chromium

CORS repose sur un accord explicite des serveurs. Chromium a ajouté deux mécanismes actifs par défaut, indépendamment de toute configuration CORS.

Cross-Origin Read Blocking (CORB)

CORB a été introduit dans Chrome 67 (2018) dans le cadre des mitigations Spectre. L'idée centrale : même si le corps d'une réponse cross-origin n'est jamais lu par JavaScript, le simple fait qu'elle ait été récupérée et décodée par le processus de rendu signifie qu'elle existe dans l'espace mémoire de ce processus. Des attaques de type Spectre peuvent alors l'en extraire par des canaux auxiliaires.

CORB empêche certaines réponses cross-origin d'entrer dans le processus de rendu. Concrètement, quand une balise <script> ou <img> récupère une réponse cross-origin dont le Content-Type est text/html, text/xml ou application/json, Chromium inspecte le corps (via un renifleur MIME défini dans la spécification CORB) et, si le type correspond, remplace la réponse par une réponse vide avant que le renderer ne la voie jamais.

Pour un proxy web, CORB serait catastrophique si les requêtes étaient réellement cross-origin. Un endpoint d'API retournant application/json chargé depuis une balise <script> renverrait silencieusement un corps vide. Cependant, puisque ProxyOrb réécrit toutes les URLs pour qu'elles soient same-origin, la condition cross-origin de CORB n'est jamais déclenchée — le navigateur ne voit jamais de réponse JSON cross-origin, car de son point de vue toutes les réponses proviennent de proxyorb.com.

Le seul cas où CORB pose problème se situe pendant la période de transition avant que le Service Worker soit complètement enregistré et qu'il intercepte les requêtes. Si une requête échappe à la réécriture d'URL pendant cette fenêtre, CORB peut la bloquer. C'est cette condition de course qui explique pourquoi ProxyOrb maintient aussi une couche d'intercepteur sur le thread principal, qui patche XMLHttpRequest, fetch et les setters d'attributs DOM de manière synchrone avant que tout JavaScript de la page ne s'exécute — offrant ainsi un filet de sécurité pendant le démarrage du Service Worker.

Il convient de noter que CORB a été partiellement supplanté par Opaque Response Blocking (ORB), que Chromium est en train d'adopter. ORB affine les règles CORB pour réduire les faux positifs tout en maintenant les protections Spectre. Les implications pour la compatibilité des proxies sont similaires.

Cross-Origin Resource Policy (CORP)

CORP, défini dans la spécification Fetch, permet aux serveurs de déclarer que leurs ressources ne doivent être chargées que par des contextes same-origin ou same-site :

Cross-Origin-Resource-Policy: same-origin

Quand Chromium rencontre cet en-tête sur une requête de sous-ressource cross-origin, il bloque entièrement la réponse. C'est plus agressif que CORB — cela s'applique quel que soit le type de contenu et ne peut être contourné par le reniflage MIME.

Là encore, puisque la réécriture d'URL de ProxyOrb garantit que toutes les requêtes sont same-origin du point de vue du navigateur, les en-têtes CORP des serveurs cibles ne déclenchent pas de blocage. La passerelle supprime également ces en-têtes des réponses de manière défensive, gérant les cas limites où une requête passe sans réécriture d'URL.

Le problème plus profond avec CORP est en réalité un cas où l'architecture du proxy améliore la compatibilité : si https://api.example.com et https://www.example.com sont tous deux proxifiés, ils correspondent tous deux à la même origine proxy, donc une requête de l'un vers l'autre est désormais véritablement same-origin — les restrictions CORP ne s'appliquent plus entre eux.


5. COEP et COOP : l'isolation cross-origin

À partir de Chrome 92 (2021), l'accès à SharedArrayBuffer — et par extension aux timers haute résolution utilisés pour certaines API de performance — a été restreint aux pages ayant opté pour l'isolation cross-origin. Cet opt-in nécessite deux en-têtes de réponse fonctionnant de concert.

Cross-Origin-Embedder-Policy (COEP)

Cross-Origin-Embedder-Policy: require-corp

Quand une page envoie cet en-tête, le navigateur impose que chaque sous-ressource chargée par cette page soit, soit :

  1. Same-origin, OU
  2. Elle envoie explicitement Cross-Origin-Resource-Policy: cross-origin, OU
  3. Elle dispose d'une réponse CORS permissive pour le fetch avec credentials

Le problème pour les proxies web : si un site cible envoie COEP: require-corp sur son document principal, et que ce document est servi via le proxy avec l'en-tête préservé, le navigateur exige désormais que chaque sous-ressource chargée par cette page fasse de même. Toute ressource qui ne le fait pas — et la majorité ne le fera pas — provoque une erreur réseau.

Cross-Origin-Opener-Policy (COOP)

Cross-Origin-Opener-Policy: same-origin

COOP contrôle si une page peut partager un groupe de contexte de navigation avec des pages cross-origin. Quand il est défini à same-origin, une navigation cross-origin ouvre un nouveau groupe de contexte de navigation, cassant les références window.opener et les canaux postMessage entre la page et tout opener cross-origin.

Pour un proxy, COOP est moins immédiatement destructeur que COEP, mais il casse tout de même les sites qui reposent sur des flux postMessage cross-origin (les flux OAuth en popup étant l'exemple le plus courant).

Le dilemme de l'opérateur de proxy

C'est le compromis le plus difficile auquel nous sommes confrontés chez ProxyOrb :

Option A : supprimer COEP et COOP des réponses.

  • Pour : le chargement des sous-ressources fonctionne normalement ; la page proxifiée n'applique pas ces restrictions.
  • Contre : nous supprimons des en-têtes de sécurité que le site cible a intentionnellement positionnés. Si le site utilisait l'isolation cross-origin pour protéger des données sensibles contre des attaques de type Spectre, supprimer ces en-têtes réexpose ce risque.

Option B : préserver COEP et COOP.

  • Pour : l'intention sécuritaire du site cible est respectée.
  • Contre : toute sous-ressource qui ne définit pas explicitement CORP: cross-origin échouera au chargement, cassant la plupart des applications web complexes.

La stratégie actuelle de ProxyOrb est l'Option A : la passerelle supprime Cross-Origin-Embedder-Policy et Cross-Origin-Opener-Policy des réponses. Cela maximise la compatibilité des sites. Le compromis sécuritaire est assumé consciemment : les utilisateurs d'un proxy web acceptent qu'ils exécutent du contenu dans un environnement différent de son contexte de déploiement prévu.

Chaque nouveau mécanisme de sécurité des navigateurs suit un schéma identique : il est introduit comme un opt-in (les sites peuvent envoyer l'en-tête s'ils veulent la protection), puis progressivement rendu obligatoire pour les nouvelles API, et finalement envisagé pour une application obligatoire. COEP a suivi ce chemin : optionnel dans Chrome 83, requis pour SharedArrayBuffer dans Chrome 91. Les sites qui intégraient du contenu tiers sans coordination ont vu leur contenu cassé. Les proxies web amplifient ce problème car ils servent par définition d'intermédiaires pour du contenu tiers.

La communauté de sécurité des navigateurs est consciente de ce problème. La proposition émergente Document-Isolation-Policy vise à découpler l'isolation de processus des exigences d'isolation cross-origin, rendant potentiellement possible l'obtention des mitigations Spectre sans exiger que toutes les sous-ressources optent pour ce mécanisme. Quand cette proposition sera déployée, la compatibilité des proxies avec les en-têtes d'isolation devrait s'améliorer.


6. Le Service Worker comme couche de confiance côté navigateur

Le Service Worker n'est pas qu'une simple optimisation — il est architecturalement essentiel au modèle de sécurité de ProxyOrb. Voici pourquoi.

La portée d'enregistrement est liée à l'origine

Un Service Worker est enregistré avec un scope qui se trouve toujours dans l'origine de la page qui l'enregistre. Quand ProxyOrb enregistre son Service Worker, la portée est https://proxyorb.com/, ce qui signifie qu'il intercepte chaque fetch de toute page sous cette origine.

C'est la raison fondamentale pour laquelle la réécriture d'URL vers same-origin fonctionne : une fois que le SW contrôle un client, chaque requête réseau de ce client — quelle que soit l'URL que le JavaScript de la page tente de récupérer — passe d'abord par le Service Worker.

// Service Worker entry point
self.addEventListener('fetch', (event) => {
  event.respondWith(handleProxyRequest(event))
})

Le pipeline d'interception

Quand le Service Worker intercepte une requête, il passe par plusieurs étapes de décision :

-- Service Worker pseudocode: request interception pipeline

function handleProxyRequest(fetchEvent):
    request = fetchEvent.request

    -- Stage 1: Synthetic CORS preflight (never hits the network)
    if request.method == "OPTIONS":
        return syntheticCORSResponse(request)

    -- Stage 2: Pass through requests that shouldn't be proxied
    if shouldBypass(request.url):
        return originalFetch(request)

    -- Stage 3: Ensure __pot is present; recover from context if missing
    enrichedRequest = attachOriginToken(request, fetchEvent)

    -- Stage 4: Forward to gateway
    response = originalFetch(enrichedRequest)

    -- Stage 5: Strip and replace security headers in the response
    return sanitizeAndReturn(response)

Transmission transparente des en-têtes

Un défi subtil : le navigateur empêche JavaScript de définir certains en-têtes de requête « interdits » (Host, Origin, Referer, etc.) via l'API Fetch. Le Service Worker contourne cela en encodant les en-têtes restreints dans un seul en-tête de passthrough personnalisé ; la passerelle les décode ensuite et les applique avant de les transmettre au serveur cible :

-- Pseudocode: encode browser-restricted headers for the gateway

function addPassthroughHeaders(request, outboundHeaders):
    restricted = {}
    for name, value in request.headers:
        if name not in COMMON_ALLOWED_HEADERS:
            restricted[name] = value

    if restricted is not empty:
        outboundHeaders["X-Proxy-Passthrough"] = json_encode(restricted)

-- Gateway side: decode and apply before forwarding upstream
function applyPassthroughHeaders(incomingHeaders):
    encoded = incomingHeaders["X-Proxy-Passthrough"]
    if encoded:
        for name, value in json_decode(encoded):
            request.headers[name] = value
        request.headers.delete("X-Proxy-Passthrough")

Implications sécuritaires du SW comme intermédiaire de confiance

D'un point de vue sécurité, le Service Worker occupe une position privilégiée : il peut inspecter, modifier et forger n'importe quelle requête faite depuis n'importe quelle page dans sa portée. Pour un usage proxy légitime, c'est précisément le but recherché. Pour un proxy malveillant, ce serait une surface d'attaque extrêmement puissante.

C'est pourquoi la fiabilité du script Service Worker lui-même est primordiale. ProxyOrb sert le script d'intercepteur depuis sa propre origine via HTTPS avec des contrôles de cache stricts. Si un attaquant parvenait à injecter un Service Worker modifié, il pourrait intercepter tout le trafic des utilisateurs affectés — pas seulement le trafic proxy, mais toute requête depuis les pages sous cette origine.


7. Les iframes : le problème des frame-ancestors

Les iframes représentent un défi distinct des sous-ressources ordinaires car elles impliquent un contexte de navigation imbriqué avec sa propre navigation, sa propre frontière SOP et ses propres restrictions d'intégration.

X-Frame-Options et frame-ancestors

Deux mécanismes empêchent les pages d'être intégrées dans des iframes :

Historique : X-Frame-Options: DENY ou X-Frame-Options: SAMEORIGIN

Moderne : Content-Security-Policy: frame-ancestors 'none' ou frame-ancestors 'self'

Quand la passerelle proxifie une page cible qui envoie l'un ou l'autre de ces en-têtes, la page ne peut pas être intégrée dans une iframe sous l'origine du proxy. Puisque X-Frame-Options: SAMEORIGIN référence l'origine cible (example.com), et que le proxy sert la page depuis proxyorb.com, la vérification same-origin du navigateur échoue.

Le proxy doit supprimer ces en-têtes des réponses proxifiées et remplacer la CSP par une version permissive :

-- Gateway pseudocode: override embedding-restrictive headers

response.headers.delete("X-Frame-Options")
response.headers.delete("Content-Security-Policy")
response.headers.delete("Content-Security-Policy-Report-Only")

-- Set a permissive replacement that allows the proxy to embed content
response.headers["Content-Security-Policy"] =
    "upgrade-insecure-requests; frame-ancestors 'self'; " +
    "default-src * data: blob: about: ws: wss: 'unsafe-inline' 'unsafe-eval'"

Interception des iframes et injection de scripts

Les iframes intégrées posent un second problème : leur contenu est chargé depuis le proxy, mais le contexte de navigation de l'iframe n'a pas automatiquement le Service Worker ni l'intercepteur du thread principal actifs. Si la page proxifiée crée un <iframe src="https://widget.example.com/...">, cette URL d'iframe doit également être réécrite au format proxy, et les scripts d'intercepteur du proxy doivent être injectés dans le document de l'iframe.

L'intercepteur d'iframe opère à deux niveaux :

Niveau 1 — Patch du prototype (capture la création programmatique d'iframes) :

-- Pseudocode: intercept iframe src assignment

override HTMLIFrameElement.prototype.src setter:
    if value is not already a proxy URL:
        value = toProxyURL(value)   -- rewrite to proxy format
    call original setter(value)
    scheduleProxyInjection(this)    -- inject interceptor into iframe document

Niveau 2 — MutationObserver de fallback (capture les iframes insérées dans le DOM) :

-- Pseudocode: observe DOM for dynamically added iframes

observer = new MutationObserver(mutations):
    for mutation in mutations:
        for node in mutation.addedNodes:
            if node is an <iframe>:
                rewriteSrcIfNeeded(node)
                injectProxyScript(node)

observer.observe(document, { childList: true, subtree: true })

Le problème de l'attribut sandbox

L'attribut HTML sandbox restreint ce qu'une iframe peut faire : sandbox="allow-scripts allow-same-origin" contrôle l'exécution des scripts, la soumission de formulaires, l'accès cross-origin, etc. Pour que l'injection du proxy fonctionne, l'iframe doit pouvoir exécuter des scripts et accéder au contexte proxy du parent.

Le token allow-same-origin est particulièrement subtil : quand il est présent avec allow-scripts, il annule l'isolation d'origine du sandbox — l'iframe s'exécute avec l'origine de la page d'intégration. Quand il est absent, l'iframe obtient une origine opaque unique, ce qui casserait complètement l'injection du script proxy.

L'approche actuelle de ProxyOrb pour les attributs sandbox est de supprimer entièrement l'attribut sandbox avant d'injecter le script proxy :

-- Pseudocode: handle sandbox attribute before proxy injection

function handleSandboxedIframe(iframe):
    if iframe.hasAttribute("sandbox"):
        -- Remove so the proxy can inject scripts and access contentWindow
        iframe.removeAttribute("sandbox")
    injectProxyScript(iframe)

C'est un compromis sécuritaire significatif : le sandboxing avait été intentionnellement mis en place par le site d'origine pour restreindre les capacités du contenu intégré. Le supprimer élargit ce que ce contenu peut faire. C'est le genre de décision invisible pour les utilisateurs finaux mais qui affecte matériellement la posture sécuritaire de la session proxy.


8. Implications sécuritaires pour les opérateurs de proxy

Le paysage de la surface d'attaque

Quand des utilisateurs naviguent via ProxyOrb, plusieurs catégories de données sensibles transitent par l'origine du proxy :

Cookies de session : puisque tous les sites cibles sont proxifiés via proxyorb.com, les cookies sont stockés sous cette origine. Les sessions authentifiées d'un utilisateur avec sa banque, son email et ses réseaux sociaux sont toutes des cookies sous un seul domaine. Le credentials: 'include' du Service Worker signifie que ces cookies accompagnent chaque requête proxifiée.

En-têtes Referer : l'en-tête Referer envoyé aux serveurs amont révèle quelle page l'utilisateur consultait. Le proxy supprime soigneusement son propre domaine des valeurs de referer avant de les transmettre au serveur cible. Si une requête provient de https://proxyorb.com/some/path?__pot=..., l'en-tête Referer sortant est reconstruit comme l'URL cible d'origine (https://example.com/some/path) — le domaine du proxy n'est jamais divulgué aux serveurs amont.

-- Pseudocode: normalize referer before forwarding upstream

function sanitizeReferer(referer):
    if referer is a proxy URL:
        return reconstruct_original_url(referer)   -- e.g. "https://example.com/path"
    if referer's origin equals proxy_origin:
        return ""   -- suppress: don't leak proxy domain to upstream
    return referer

Contexte d'exécution JavaScript : chaque fichier JavaScript servi via le proxy est modifié (URLs réécrites) et exécuté dans l'origine du proxy. Un script malveillant de evil.example.com proxifié via ProxyOrb s'exécute dans l'origine proxyorb.com et dispose d'un accès complet au local storage, aux cookies et à l'IndexedDB de cette origine — ce qui inclut les données de tous les autres sites cibles que l'utilisateur a consultés via le proxy.

Le modèle de menace du proxy malveillant

À titre pédagogique, il vaut la peine d'être explicite sur ce qu'un proxy malveillant pourrait faire, que ProxyOrb ne fait délibérément pas :

  1. Collecte de credentials : un proxy malveillant pourrait enregistrer chaque corps de requête (contenant mots de passe et données de formulaire) à mesure qu'il transite par la passerelle.

  2. Détournement de session : puisque tous les cookies des sites cibles sont stockés sous le domaine du proxy, un opérateur de proxy malveillant pourrait accéder à ces cookies côté serveur via la passerelle.

  3. Injection de contenu : le proxy modifie nécessairement le contenu des pages (pour injecter le Service Worker et réécrire les URLs). Un proxy malveillant pourrait injecter du JavaScript arbitraire — keyloggers, mineurs de crypto, scripts de fraude publicitaire — invisible pour l'utilisateur.

  4. SSL Stripping : si le proxy dégrade les connexions HTTPS en HTTP pour la partie passerelle-utilisateur, tout le trafic est en clair.

ProxyOrb fonctionne en HTTPS de bout en bout et n'enregistre pas les corps de requêtes. L'opérateur de tout proxy web dispose de ces capacités, ce qui explique pourquoi choisir un opérateur de proxy de confiance est aussi important que choisir un fournisseur VPN de confiance.

Contenu mixte

Les navigateurs modernes appliquent le blocage du contenu mixte : une page HTTPS ne peut pas charger des sous-ressources HTTP. ProxyOrb gère cela en imposant HTTPS sur toutes les connexions sortantes depuis la passerelle, même quand l'URL cible utilise HTTP. Le remplacement de la CSP inclut upgrade-insecure-requests pour demander au navigateur de mettre à niveau les URLs de sous-ressources HTTP vers HTTPS avant le chargement.

C'est généralement souhaitable, mais cela peut casser des sites qui servent délibérément des ressources en HTTP (CDN legacy, ressources localhost, etc.).


9. La course aux armements permanente : conclusion

Quand nous avons commencé à construire ProxyOrb, les principaux défis de compatibilité étaient CORS et X-Frame-Options. Depuis, Chromium a déployé CORB, CORP, COEP, COOP, et développe Document-Isolation-Policy. La direction est claire : les navigateurs deviennent de plus en plus agressifs dans l'application des frontières cross-origin, et chaque nouveau mécanisme contraint l'infrastructure proxy à s'adapter.

Le défi le plus significatif à venir est probablement le déploiement généralisé de COEP dans les grandes applications web. Les sites qui utilisent SharedArrayBuffer pour la performance (éditeurs vidéo, IDE, outils de collaboration) définissent de plus en plus COEP: require-corp. Comme ProxyOrb supprime cet en-tête pour maintenir la compatibilité, les utilisateurs qui ont besoin des fonctionnalités haute performance que COEP active les verront dégradées dans un contexte proxy.

Pour les chercheurs en sécurité, l'architecture proxy révèle quelque chose d'important : la politique same-origin n'est pas un pare-feu. C'est un modèle de confiance basé sur la structure des URLs. Les proxies web exploitent le fait que la SOP ne fait aucune distinction intrinsèque entre « ces deux ressources sont légitimement same-origin » et « ces deux ressources ont été réécrites en URL pour paraître same-origin ». L'ensemble de la pile de sécurité navigateur au-dessus de la SOP — CORS, CORB, CORP, COEP, COOP — peut être vu comme une tentative d'ajouter des défenses plus robustes que l'identité d'origine basée sur les URLs.

Comprendre cela est essentiel pour quiconque audite des services proxy, conçoit des politiques de sécurité navigateur, ou évalue la posture sécuritaire réelle d'applications susceptibles d'être accédées via des proxies.


Foire aux questions

Un proxy web contourne-t-il la politique same-origin ?

Pas exactement. Un proxy web fonctionne par réécriture d'URL : il transforme toutes les URLs cross-origin en URLs same-origin, rendant les restrictions cross-origin de la SOP sans objet plutôt que de les contourner. Du point de vue du navigateur, tout le contenu semble provenir de la propre origine du proxy, donc aucune frontière cross-origin n'est franchie. C'est architecturalement différent d'un contournement — la SOP applique toujours ses règles ; le proxy conçoit simplement le contenu de sorte que ces règles ne se déclenchent jamais.

Comment CORS affecte-t-il les serveurs proxy web ?

CORS affecte les proxies à deux niveaux. Premièrement, le proxy doit intercepter les requêtes de preflight OPTIONS et répondre avec des en-têtes CORS permissifs, car la réponse de preflight du vrai serveur cible (référençant l'origine cible) ne satisferait pas la vérification CORS du navigateur pour l'origine du proxy. Deuxièmement, le proxy doit supprimer les en-têtes CORS des réponses du serveur cible et les remplacer par des en-têtes référençant l'origine du proxy. Le paramètre Access-Control-Allow-Credentials: true, combiné avec credentials: 'include' dans le Service Worker, signifie que tous les cookies de l'origine du proxy accompagnent chaque requête proxifiée.

Qu'est-ce que CORB et quel est son impact sur les services proxy ?

Cross-Origin Read Blocking (CORB) est une défense Chromium qui empêche certaines réponses cross-origin sensibles (HTML, JSON, XML) d'entrer dans le processus de rendu quand elles sont chargées comme sous-ressources cross-origin. Pour les proxies web correctement configurés, CORB n'est généralement pas déclenché car la réécriture d'URL rend toutes les requêtes same-origin. Cependant, CORB peut causer des problèmes pendant la période précédant l'enregistrement complet du Service Worker, quand certaines requêtes peuvent échapper à la réécriture d'URL et être envoyées comme de vraies requêtes cross-origin. CORB a été introduit comme mitigation Spectre et est défini dans la spécification WHATWG Fetch.

Les proxies web peuvent-ils accéder aux cookies HTTP-only ?

Non. Les cookies HttpOnly sont définis par le serveur cible et ne peuvent pas être lus par JavaScript — y compris le JavaScript s'exécutant dans un Service Worker. Cependant, la passerelle reçoit ces cookies lors de la transmission des requêtes pour le compte de l'utilisateur, puisque le navigateur les envoie dans les en-têtes de requête. Le flag HttpOnly empêche le vol JavaScript côté client mais n'empêche pas le serveur proxy lui-même de voir les valeurs des cookies en transit. C'est analogue à la façon dont HttpOnly protège contre le XSS mais pas contre l'interception côté serveur.

Est-il sûr d'utiliser un proxy web du point de vue de la sécurité navigateur ?

Cela dépend du modèle de menace. Du point de vue du navigateur, tout le contenu servi via un proxy web s'exécute dans l'origine du proxy, ce qui signifie qu'une vulnérabilité dans le JavaScript d'un site proxifié pourrait potentiellement accéder aux données de la session d'un autre site proxifié (car ils partagent le même jar de cookies et le même stockage d'origine). L'opérateur du proxy est également une partie de confiance avec accès à tout le trafic en transit. Pour accéder à du contenu non sensible dans des environnements restreints, le risque est généralement acceptable. Pour les sessions authentifiées avec des services sensibles (banque, santé, administration), les utilisateurs doivent comprendre que l'opérateur du proxy a la capacité technique d'observer ce trafic, et ne devraient utiliser que des services proxy avec des politiques de non-journalisation auditables et des pratiques de sécurité opérationnelle solides.


Cet article reflète l'architecture de ProxyOrb telle qu'elle était début 2026. Les mécanismes de sécurité des navigateurs évoluent rapidement ; les lecteurs sont encouragés à consulter le Standard WHATWG Fetch, la spécification W3C Content Security Policy, et les documents de conception sécuritaire de Chromium pour les informations les plus récentes.