Een stored HTML-injectiebug in het RamenHire vacaturebord stelde iedereen in staat om een openbaar formulier in te vullen en de notificatie-e-mails van het team te veranderen in een phishingplatform.

Hoe de bug binnensliep

RamenHire draait op Next.js en Supabase en stelt startups in staat om vacatures te plaatsen zonder in te loggen. Elke inzending triggert een e-mail naar het interne recruitmentteam. De e-mailbody wordt opgebouwd door ruwe formuliervelden — bedrijfsnaam, functiebeschrijving, contactnaam — rechtstreeks in een HTML-string te plakken. Er vindt geen escaping of sanitization plaats voordat de string de e-mailservice bereikt.

Omdat de template gebruikersinvoer behandelt als veilige HTML, kan een kwaadwillende sollicitant een valse "klik hier om te verifiëren"-link injecteren. De payload wordt op de server opgeslagen als onderdeel van de vacature, waardoor elke volgende notificatie-e-mail de kwaadaardige code bevat. Een aanvaller kan die link verbergen naast de echte "View Dashboard"-knop, waardoor een teamlid wordt misleid om inloggegevens prijs te geven of een phishingwebsite te bezoeken.

Waarom het ertoe deed

De e-mails gaan naar het kernteam van de recruitmentafdeling, mensen die constant op links klikken om kandidaten door de pipeline te loodsen.

Het technische gat

Het escapen van HTML is niet één enkele stap. Twee contexten hebben bescherming nodig:

  • Tekstnodes – inhoud tussen tags. Het converteren van < naar &lt; en > naar &gt; stopt tags, maar voorkomt niet dat een aanvaller uit een attribuutwaarde breekt.
  • Attribuutwaarden – strings binnen tags zoals href="…". Door een aanhalingsteken (" of ') te injecteren, kan een aanvaller het attribuut voortijdig sluiten en eigen markup invoegen.

De oorspronkelijke code verwerkte alleen platte tekst, waardoor attribuutinjectie volledig openstond.

Het probleem oplossen

De ontwikkelaar voegde een kleine escapeHtml-helper toe en paste deze toe op elke door de gebruiker verstrekte waarde vóór interpolatie, ongeacht of de waarde in een tekstnode of een attribuut terechtkomt.

Verificatiestappen

  • Local testing – Een reeks injectiestrings werd aan de templatefuncties gevoerd. Elke test toonde alleen geëscapte karakters, geen uitvoerbare tags.
  • Production testing – Na het implementeren van de patch werd een payload ingediend via de live site, waarbij de Cloudflare Turnstile-botcheck werd doorstaan. De resulterende e-mail kwam aan in de inbox van de ontwikkelaar; de kwaadaardige link en eventuele scripttags verschenen als onleesbare tekst, niet als klikbare elementen.

Een opmerking over Gmail: de dienst kan een URL nog steeds blauw kleuren en klikbaar maken, maar dat is een client-side gemak en betekent niet dat de HTML-injectie is geslaagd. De fix voorkomt dat de injectie echte knoppen aanmaakt of scripts uitvoert.

Waar ontwikkelaars op moeten letten

  • Vertrouw nooit data van openbare formulieren – Zelfs onschuldig ogende formulieren kunnen opgeslagen output produceren die elders wordt gebruikt.
  • Escape op het moment van gebruik – Pas context-bewuste escaping toe (tekst vs. attribuut) vlak voor het renderen, niet eerder in de pipeline.
  • Test zowel client-side als server-side – Unit tests vangen duidelijke fouten op; een handmatige end-to-end test via de live UI bevestigt dat de verdediging standhoudt onder echt verkeer.
  • Wees je bewust van de eigenaardigheden van e-mailclients – Sommige clients maken automatisch links van URL's, wat een vals gevoel van veiligheid geeft. Controleer of de onderliggende HTML geen actieve elementen bevat.

De kern

Een enkele onoplettendheid bij het verwerken van gebruikersinvoer veranderde routineuze notificatie-e-mails in een phishing-vector. Het introduceren van een uitgebreide HTML-escaping routine en het valideren van de fix lokaal en in productie herstelde de integriteit van de inbox. Dit incident onderstreept een tijdloze les voor elke webapp die HTML genereert uit onbetrouwbare data: goede sanitization is niet onderhandelbaar, en het negeren ervan kan een directe lijn naar de inbox van je gebruikers openen.