Het grote boze internet, deel 2: XSS en CSRF

XSS: Cross-Site ScriptingVorige week kon je lezen over SQL-injectie, een techniek die aanvallers kunnen gebruiken om de server waarop je website draait aan te vallen. Het artikel van deze week is opnieuw een technisch artikel en het laatste deel van de tweeluik over de beveiliging van je website. Het gaat over Cross-Site Scripting (XSS) en Cross-Site Request Forgery (CSRF): twee client-side aanvallen, waarbij dus jouw website misbruikt wordt voor de doelen van iemand anders.

XSS: Cross-Site Scripting

Cross Site Scripting is het injecteren van kwaadaardige JavaScript-code in pagina’s die een server terugstuurt. Een goed voorbeeld is een gastenboek: gebruikers schrijven berichtjes in een gastenboek, en de server reageert met de lijst van alle berichtjes. Als iemand een berichtje zou kunnen plaatsen met een uitvoerbare JavaScript-code, dan is er sprake van XSS. (Cross-Site Scripting wordt afgekort als XSS: de afkorting CSS was al bezet, en met een beetje fantasie lijkt een X op een kruisje, vandaar.)

Bij een XSS-aanval wordt er kortom een stukje uitvoerbare code in het midden van ‘normale’ tekst gestopt, in de hoop dat browsers het gaan uitvoeren.

R0b3rt zegt:
Leuke site! 
<script>document.location="https://r0b3rt.tk/";</script>

Cross Site Scripting komt, net als SQL-injectie, veel te vaak voor. En dat terwijl de oplossing relatief eenvoudig is.

String escaping

Wie vorige week meegelezen heeft moet hier al een zekere symmetrie zien. Ook voor het voorkomen van XSS is string escaping weer nuttig. In deel 1 was het de bedoeling om invoer van de gebruiker zo te bewerken dat de server het niet als uitvoerbare code zag. Nu doen we hetzelfde voor de client. Het idee is om tekst die de gebruiker invoert en de server teruggeeft, dus reacties in een gastenboek bijvoorbeeld, zo door de mangel te halen dat ze geen kwaad meer kunnen.

String escaping voor HTML is bijna nog makkelijker dan voor SQL. HTML heeft maar een klein aantal speciale tekens. Wie alle punthaken in de invoer door hun HTML-representaties vervangt is eigenlijk al klaar: alle openhaken (<) worden &lt; voor ‘less than’, en alle sluithaken (>) worden &gt;. Daarnaast is het netjes om alle ampersands (&) en aanhalingstekens (“) te vervangen, al kunnen die weinig kwaad. Klinkt redelijk eenvoudig, en dit is precies wat de PHP-functie htmlentities doet. Nu nog onthouden om consequent htmlentities te gebruiken en we zijn 100% beschermd tegen cross-site scripting. Toch?

UTF7 en andere problemen

Helaas. Escaping werkt alleen als jouw server en de browser van je bezoeker het eens kunnen worden over welke speciale tekens nu precies zijn. Dat is over het algemeen niet ingewikkeld: voor bijna alle tekst-encodings, van de stokoude ASCII-standaard tot het Russische KOI-8 tot het moderne UTF8, zijn de punthaken de 60e en 62e ‘letter’ van het ‘alfabet’. Een uitzondering is UTF7, een encoding die oorspronkelijk bedoeld is voor het versturen van e-mail met trema’s en accenten via servers die dat eigenlijk niet aankonden. Alle ‘lastige’ letters werden vervangen door langere combinaties van ‘gewone’ tekens. Zo zijn in UTF7 voor de linker punthaak zowel < (oftewel 60) als +ADw- toegestaan.

Tot zover geen probleem. Het werd lastig toen bleek dat Internet Explorer ervan overtuigd kon worden dat een pagina, die eigenlijk ASCII-tekst bevatte, in UTF7 opgeslagen was. Als een pagina geen informatie bevatte over welke encoding er gebruikt werd, gokte de browser de meest logische encoding. En precies dat was het probleem. Als het een aanvaller lukte om ergens +ADw-script+AD4- in een gastenboekreactie te plaatsen, zou IE de hele pagina netjes herkennen als UTF7 en vol goede moed de code van de aanvaller gaan uitvoeren – ook al was de pagina nooit als UTF7 bedoeld.

De UTF7-hack werkte alleen op websites die niet specifiek hun eigen tekst-encoding aangaven, iets dat sowieso aangeraden wordt, en dan nog alleen voor sommige browsers, waaronder Internet Explorer, dus de impact was niet erg groot. Aan de andere kant liet het probleem wel zien dat het moeilijk is om je tegen alle gevallen te beschermen. Consequent uitvoer escapen, of het nou ‘met de hand’ is of met een template-systeem als Twig, is erg belangrijk, maar we zouden liever nog een extra laag bescherming willen hebben.

Content Security Policy

Het liefst zou je het uitvoeren van scripts op plaatsen waar dat niet hoort helemaal verbieden. Stel dat je weet dat alle script-tags van jouw website of applicatie in het head-gedeelte staan. Dan is elke script-tag die per ongeluk door de escaping heen is gekomen dus een aanval – en zou je die willen blokkeren. Je zou eigenlijk het liefst een beschrijving mee willen geven met wat de pagina hoort te doen, zodat browsers kunnen ingrijpen als er iets gebeurt dat niet binnen die beperkingen past.

Dat is precies wat CSP, of Content Security Policy, inhoudt. Met CSP geef je via een speciale HTTP-header precies aan van welke andere sites er scripts ingeladen mogen worden, of er plaatjes van andere websites gehaald mogen worden.

Content-Security-Policy: script-src 'self' https://platform.twitter.com

Naast deze mogelijkheden blokkeert CSP standaard alle ‘inline’ JavaScript. Alle scripts in de body-tag van je pagina, plus code in attributen als onclick, worden genegeerd en nooit uitgevoerd.

Content Security Policy is nog volop in ontwikkeling, en wordt nog niet door alle browsers volledig ondersteund. Bovendien kan het best veel werk zijn om een correcte policy op te stellen voor een bestaande site, helemaal als je veel diensten van anderen gebruikt (buttons van Facebook, statistieken van Google, lettertypen van Typekit…).

CSRF: Cross-Site Request Forgery

Maar stel dat alle maatregelen die je hebt getroffen om XSS te voorkomen toch omzeild worden. Wat heeft een aanvaller er aan als hij of zij code in je pagina kan stoppen? Eén van de gemenere ‘trucjes’ is Cross-Site Request Forgery of CSRF. CSRF is het beste te demonstreren met een voorbeeld.

Stel dat een bank dit formulier in zijn internetbankieren-applicatie heeft:

<form action="https://mijn.meerdijkbank.nl/overmaken">
  <input name="rekening">
  <input name="bedrag">
  <input type="submit" value="Overmaken">
</form>

Een heel gewoon formulier dus, waarmee rekeninghouders van de bank van Meerdijk geld naar elkaar kunnen overmaken. Tot dusver niks nieuws onder de zon. Het probleem is dat een kwaadwillende derde op zijn site (of op jouw site, met XSS) een afbeelding kan opnemen:

<img src="https://mijn.meerdijkbank.nl/overmaken?rekening=1234567&bedrag=100">

Een browser zal in dit geval proberen een afbeelding op te halen van de Meerdijkbank – maar de Meerdijkbank zal de aanvraag interpreteren als een verzoek om geld over te maken. De bank zal netjes 100 euro overmaken, in dit geval van de rekening van de persoon die toevallig ingelogd is naar de rekening van de kwaadwillende derde uit de vorige alinea. Als de overboeking voltooid is merkt de browser dat er helemaal geen plaatje teruggegeven is, maar dan is het al te laat.

Preventie met de Referer-header

Om CSRF te voorkomen moet je een manier hebben om ‘echte’ aanvragen en ‘vervalste’ (forged) aanvragen van elkaar te onderscheiden. Gelukkig sturen browsers vaak aan hun aanvragen mee wat de vorige pagina is die een bezoeker bezocht heeft, de zogenaamde referer. (De Engelse term is eigenlijk ‘referrer’, maar de spelfout is blijven hangen.) Op basis van die header kun je de beslissing maken of een aanvraag van jouw applicatie komt of van iemand anders.

$referer = $_SERVER['HTTP_REFERER'];

if (strpos($referer, 'https://mijn.meerdijkbank.nl/') == 0) {
  // Bezoeker komt van een pagina op onze site.
} else {
  // Onbekend.
}

Helaas zijn browsers niet verplicht om de Referer-header mee te sturen. De meeste doen dat ook niet altijd, bijvoorbeeld als de gebruiker van een HTTPS- naar een HTTP-site gaat. Het is dus onverstandig om volledig op de Referer-header te vertrouwen. Bovendien helpt de controle niet als de ‘bron’ van de aanval op je eigen site staat, wat zomaar kan gebeuren met XSS.

Preventie met tokens

Het liefst zou je goede en slechte aanvragen van elkaar scheiden op basis van hun inhoud, en niet op basis van een HTTP-header. Bij het voorbeeld van de Meerdijkse bank hierboven is dat bijna onmogelijk. Zowel de onvervalste als de neppe aanvraag hebben een rekeningnummer en een bedrag.

Een veelgebruikte aanpak is om alle formulieren in je applicatie die een effect hebben, dus informatie wijzigen of verwijderen, te beschermen met een token. Zo’n token is een geheime sleutel die alleen de server kan weten, en die elke keer als een formulier wordt meegestuurd. De token wordt aan het formulier meegegeven en meegestuurd met de aanvraag. Als er een aanvraag binnenkomt kan de server dan kijken of de token bekend en correct is.

<form action="https://mijn.meerdijkbank.nl/overmaken">
  <input name="rekening">
  <input name="bedrag">
  <input type="hidden" name="token" value="d41d8cd98f00b20">
  <input type="submit" value="Overmaken">
</form>

Anderen kunnen, als het goed is, nooit een correcte token meesturen, en kunnen dus geen aanvragen meer vervalsen. De site owasp.org heeft een voorbeeld van hoe je een tokenfunctie zou kunnen implementeren in PHP.

Natuurlijk is het voorbeeld wat overdreven. Echte sites voor internetbankieren gebruiken geen GET-formulieren voor het overmaken van geld, en zijn vaak zo gemaakt dat je zo snel mogelijk uitgelogd wordt. Daarnaast gebruiken ze extra technieken om zich ervan te verzekeren dat transacties correct zijn, zoals het versturen van SMS’jes en het gebruik van speciale kastjes. Voor de meeste systemen is dat wat overdreven en is het gebruik van een token voldoende om ‘normale’ CSRF-aanvallen tegen te gaan.

Clickjacking

Een bijzonder nare variant van CSRF is clickjacking. Bij clickjacking wordt een actie van een bezoeker, dus een klik op een knop bijvoorbeeld, ‘gekaapt’ om een andere actie uit te voeren dan dat de bezoeker oorspronkelijk bedoeld had.

Als je Facebook gebruikt heb je vast al eens gezien dat iemand een pagina ‘per ongeluk’ geliked heeft. Vaak zijn het dan Facebook-pagina’s met sensationele titels (“Man springt met parachute van flatgebouw!” of “Win iPad gratis nu!”) en dito afbeeldingen. De persoon in kwestie is dan slachtoffer geworden van clickjacking.

Clickjacking is technisch niet ingewikkeld. Je hoeft als aanvaller alleen maar voor te zorgen dat de bezoeker ergens op klikt – en dan een andere knop, een like-button bijvoorbeeld, er voor te zetten. Die like-button is vervolgens 100% transparant en dus onzichtbaar te maken. Een transparante knop is niet te zien, maar nog wel te klikken.

Clickjacking met een like-button: links 100% transparant, rechts 50% transparant

Clickjacking met een like-button: links 100% transparant, rechts 50% transparant

Facebook kan onmogelijk weten of je de like-knop daadwerkelijk hebt gezien, en registreert dus netjes dat je van parachutes of gratis iPads houdt. Je Facebook-vrienden klikken op de link die je net, zonder jouw medeweten, gedeeld hebt – en raken zelf verzeild in een web van neppagina’s, virussen en andere narigheid. (Als iemand er praktijken als clickjacking voor nodig heeft om jou op zijn website te krijgen, kan het bijna geen zuivere koffie zijn.) Tegen de tijd dat Facebook doorheeft dat het om een zwendel gaat, is de pagina natuurlijk al lang verdwenen.

Clickjacking met like-buttons komt relatief veel voor, maar het is lang niet de enige toepassing. Er is minstens één geval bekend van een clickjacking-aanval tegen de Flash-plug-in, toen nog van Macromedia. Spelletjes en andere applicaties die gemaakt zijn in Flash kunnen, als ze dat nodig hebben, toegang krijgen tot je webcam en microfoon. De bekende site Chatroulette, bijvoorbeeld, werkt met Flash. Voordat een applicatie de webcam mag gebruiken moet je eerst als gebruiker toestemming geven door op een akkoord-knop te klikken – maar weer kan de Flash-plug-in niet zien of je ook echt toestemming wilde geven of dat je er ingeluisd bent.

Voor clickjacking is er nog geen echte oplossing: CSRF-preventietechnieken halen hier niks uit, omdat het daadwerkelijk het oorspronkelijke formulier of een echte like-button is die gebruikt wordt. Wat je als gebruiker kunt doen is voorzichtig zijn met waar je op klikt, en vooral welke sites je bezoekt. Als webbouwer is het jouw verantwoordelijkheid om je bezoekers te beschermen, bijvoorbeeld door maatregelen te nemen tegen XSS.

Daarnaast kun je overwegen iets als NoScript te installeren, een plugin die vrij aggressief scripts blokkeert en ook specifieke functies heeft die clickjacking proberen te voorkomen. NoScript heeft als nadeel dat het ook scripts die wél nuttig zijn blokkeert.

Tot slot de conclusie

Zo komen we eigenlijk terecht bij de titel. Het internet is ongelofelijk groot, en niet elke internetgebruiker heeft even zuivere bedoelingen. Wat je als webbouwer kunt doen is jezelf verdiepen in wat de Duistere Zijde voor narigheid bedacht heeft, en wat voor technieken er bedacht zijn om narigheid te voorkomen. Met goede voorbereiding en oplettendheid is het niet moeilijk om de digitale tak van de gilde met de lange vingers nét een stapje voor te blijven. Anders gezegd: weet wat er kan gebeuren, en weet wat je er tegen kunt doen. Succes!

Deel Deel Deel Deel

Geef een reactie

Het e-mailadres wordt niet gepubliceerd. Vereiste velden zijn gemarkeerd met *