JavaScript voor beginners: Cookies & Beveiliging

In de vorige aflevering hebben we gezien hoe we in JavaScript omgaan met frames, en hoe we van de ene pagina naar de andere kunnen navigeren. In de huidige aflevering komen de onderwerpen "cookies" en beveiliging aan de orde.

Cookies

Cookies zijn geliefd bij de marketingmensen omdat ze een individuele benadering van websurfers mogelijk maken, maar gehaat bij veel Internet-gebruikers omdat ze een inbreuk kunnen vormen op hun privacy... Ze maken het mogelijk om gegevens over een sitebezoeker te verzamelen en vast te leggen, zodat deze bijvoorbeeld bij een volgend bezoek aan de site kan worden begroet als "oude bekende".

Daarnaast kunnen cookies informatie doorgeven van de ene webpagina naar de andere. Hierdoor wordt het mogelijk om, ondanks het feit dat het HTTP-protocol "stateless" is, een vorm van "gebruikerssessie" te implementeren, waardoor bijvoorbeeld de gegevens die op de startpagina zijn ingevoerd, op een latere pagina nog steeds bekend zijn.

Cookies zijn enkele jaren geleden geïntroduceerd door Netscape om de genoemde beperking van het HTTP-protocol te omzeilen; de Engelstalige specificatie (die overigens nog steeds "preliminary" oftewel voorlopig genoemd wordt) is te vinden op: http://wp.netscape.com/newsref/std/cookie_spec.html

Cookies kunnen overigens niet onbeperkt, als een soort database, gebruikt worden voor het opslaan van gegevens: de genoemde specificatie stelt namelijk een aantal beperkingen aan het aantal en de omvang van de cookies die een browser zou moeten ondersteunen.

Beperkingen

Een webbrowser die cookies ondersteunt, hoeft in totaal niet meer dan 300 cookies te bewaren (het is mogelijk dat een specifieke browser er meer bewaart, maar daar mag je als sitebouwer niet op rekenen). Ook hoeven er niet meer dan 20 cookies per server of domeinnaam te worden opgeslagen, en hoeft per cookie niet meer dan 4 kilobyte aan gegevens te worden vastgelegd.

Die 4 kilobyte heeft betrekking op de twee elementen waaruit cookies in hun eenvoudigste vorm bestaan: hun naam en hun waarde. Als je beseft dat de naam van een cookie bijvoorbeeld "rekeningnummer" is, en de waarde "1234567", dan is wel duidelijk dat het maximum van 4 kilobyte in de praktijk geen ernstig probleem vormt.

Het maximum van 20 cookies per server zal eerder tot problemen leiden, en er zijn dan ook enkele methoden verzonnen om deze limiet te omzeilen. Een mogelijkheid is, om met behulp van speciale kunstgrepen meerdere "variabelen" in een enkel cookie op te slaan; deze aanpak is echter nogal omslachtig.

Een andere mogelijkheid is, om slechts een enkel cookie te gebruiken voor het verkrijgen van "state", en de overige gegevens die bewaard moeten worden, op te laten slaan door de server en niet door de webbrowser van de eindgebruiker. (Dit is bijvoorbeeld de methode die ASP gebruikt voor het implementeren van sessie-variabelen.)

Deze aanpak is niet alleen eenvoudiger maar ook sneller dan de eerste methode, omdat minder informatie hoeft te worden uitgewisseld tussen de browser en de server, en bovendien veiliger, omdat informatie die niet wordt uitgewisseld ook niet kan worden onderschept!

We hebben het nu al een tijdje over de theoretische achtergronden van cookies; laten we nu eens kijken hoe we ze vanuit JavaScript kunnen gebruiken.

Schrijven

Cookies bestaan, zoals gezegd, uit minmaal twee elementen of "attributen": een naam en een waarde. Om een cookie aan te maken, hoeven we alleen maar die elementen toe te kennen aan de eigenschap "cookie" van het document-object:

document.cookie = "rekeningnummer=1234567"

Merk op dat zowel de naam als de waarde worden toegekend aan de eigenschap cookie! De naam van het cookie is dus zelf geen eigenschap van het "cookie-object" (een dergelijk object bestaat niet!); we mogen daarom niet schrijven: document.cookie.rekeningnummer = "1234567".

Cookie-waarden mogen geen komma's, puntkomma's of "whitespace" (spaties, tabs etc.) bevatten. Zonodig moeten we de escape-functie van JavaScript gebruiken om dergelijke tekens te encoderen; we moeten er dan wel aan denken om de unescape-functie te gebruiken als we het cookie weer uitlezen.

Een cookie dat we op bovengenoemde manier hebben "gebakken", is tijdelijk: het wordt niet weggeschreven op de harde schijf van de gebruiker. Als de gebruiker de browser verlaat, wordt de sessie afgesloten en gaat het cookie verloren.

Om meer permanente cookies aan te kunnen maken, gebruiken we het optionele attribuut "expires":

var nieuwjaar = new Date(2000,1,1,0,0,0,0);
document.cookie = "rekeningnummer=1234567; expires=" + nieuwjaar.toGMTString();


Dit cookie blijft bewaard tot 1 januari 2000 (middernacht), tenzij de gebruiker het eerder van zijn harde schijf wist. Voor "expires" moeten we een datum opgeven in het formaat dat door toGMTString wordt gebruikt ("Wdy, DD-Mon-YYYY HH:MM:SS GMT"); dit is dus altijd een absolute waarde.

Indien we een relatieve datum willen gebruiken (bijvoorbeeld: over drie dagen), dan moeten we eerst een berekening uitvoeren om van die relatieve datum een absolute datum te maken (bijvoorbeeld: als het vandaag 1 mei 1999 is, dan is "over drie dagen" dus 4 mei 1999), en vervolgens het resultaat gebruiken voor "expires".

Overigens is een browser niet verplicht om cookies die voorbij hun "houdbaarheidsdatum" zijn, onmiddellijk te wissen; ze kunnen dus nog enige tijd in het cookie-bestand van de browser bewaard blijven, maar ze zullen niet meer door de browser naar de server worden gestuurd.

Naast "expires" hebben cookies nog drie optionele attributen, te weten: "path" en "domain", waarmee wordt aangegeven welke pagina's toegang hebben tot de opgeslagen informatie, en "secure", waarmee kan worden afgedwongen dat de informatie alleen via een beveiligd kanaal kan worden uitgewisseld.

Zichtbaarheid

Het cookie uit ons voorbeeld is "zichtbaar" voor de pagina waarmee het is aangemaakt, de andere webpagina's in dezelfde directory, alsmede de pagina's in eventuele subdirectories. Pagina's in andere directories hebben echter geen toegang tot de gegevens van het cookie. Een voorbeeld kan dit verhelderen.

Stel dat het cookie is aangemaakt door pagina http://www.cookietest.com/test/testje/test1.htm. In dat geval is het cookie zichtbaar voor de genoemde pagina, en ook voor bijvoorbeeld http://www.cookietest.com/test/testje/test2.htm en http://www.cookietest.com/test/testje/subdir/test1.htm, maar niet voor bijvoorbeeld http://www.cookietest.com/test/subdir/test1.htm of http://www.cookietest.com/test1.htm.

De webbrowser is hiervoor verantwoordelijk: telkens wanneer er een webpagina van de server wordt opgevraagd, controleert de browser of er een cookie bestaat waar de beoogde pagina toegang toe heeft. Zo ja, dan stuurt de browser de cookie-gegevens naar de server met behulp van de zogenaamde HTTP-headers; zo nee, dan worden er geen cookie-gegevens meegestuurd.

Indien we willen dat de gegevens van het cookie toch beschikbaar zijn voor pagina's in andere directories, dan moeten we gebruik maken van het "path"-attribuut. Geven we bij het cookie uit ons voorbeeld op: "path=/test", dan krijgt pagina http://www.cookietest.com/test/subdir/test1.htm daarmee ook toegang tot de gegevens. Voor pagina http://www.cookietest.com/test1.htm is het cookie echter nog steeds niet zichtbaar, omdat die pagina niet binnen het opgegeven pad valt. Willen we dat een cookie zichtbaar is voor alle pagina's binnen een site, dan geven we op: "path=/". De opdracht waarmee we het cookie aanmaken luidt nu:

document.cookie = "rekeningnummer=1234567; expires=" + nieuwjaar.toGMTString() + "; path=/"

Voor het attribuut "domain" geldt iets soortgelijks. Normalerwijze zijn cookies alleen toegankelijk voor pagina's van de webserver die ze heeft aangemaakt, in ons geval: "www.cookietest.com". Indien de cookies ook toegankelijk moeten zijn voor pagina's van "private.cookietest.com" (of andere servers binnen het domein "cookietest.com"), dan moeten we opgeven: "domain=cookietest.com". Het is echter niet mogelijk om een heel ander domein op te geven; "domain=microsoft.com" wordt dus bijvoorbeeld niet geaccepteerd.

Tenslotte is er nog het attribuut "secure", waar geen verdere waarde aan wordt toegekend. Wordt dit attribuut opgegeven bij het aanmaken van een cookie, dan worden de cookie-gegevens alleen uitgewisseld tussen browser en server indien sprake is van een beveiligd kanaal (d.w.z.: indien het https-protocol wordt gebruikt in plaats van http).

De volledige opdracht waarmee we een cookie kunnen aanmaken luidt derhalve:

document.cookie = "rekeningnummer=1234567; expires=" + nieuwjaar.toGMTString() + "; path=/; domain=cookietest.com; secure"

(De verschillende attributen worden dus van elkaar gescheiden door middel van puntkomma's.)

Lezen

Om in JavaScript een cookie te lezen, vragen we eenvoudigweg de waarde van document.cookie op. Twee punten verdienen hierbij echter extra aandacht.

Allereerst moeten we weten dat alleen de naam en de waarde van cookies door de browser verstuurd worden, en dus niet de vier optionele attributen "expires", "path", "domain" en "secure". Die attributen worden door de browser alleen gebruikt om te bepalen of cookies überhaupt mogen worden verstuurd bij het opvragen van een bepaalde pagina. (Is het cookie nog niet verlopen? Ligt de opgevraagde pagina binnen een pad en een domein dat toegang heeft tot de gegevens van het cookie? Is een beveiligd kanaal vereist, en zo ja, is dat thans voorhanden?)

Daarnaast moeten we ons ervan bewust zijn dat meer dan een cookie naar een bepaalde pagina kan worden gestuurd (de specificatie laat minimaal 20 stuks toe). In zo'n geval worden de naam/waarde-paren van elkaar gescheiden door middel van puntkomma's:

"rekeningnummer=1234567;achternaam=Janssen; voornaam=Jan"

Het ligt voor de hand om een functie te schrijven die (met behulp van de functie indexOf) onderzoekt of een cookie met een bepaalde naam aanwezig is, en zo ja, wat daar de waarde van is.

Wijzigen en wissen

Om een cookie te wijzigen, kennen we er simpelweg een nieuwe waarde aan toe, bijvoorbeeld:

document.cookie = "rekeningnummer=7654321"

en om het cookie te wissen, geven we een vervaldatum op die in het verleden ligt. We gebruiken daarbij de bestaande naam van het cookie, maar het is natuurlijk niet van belang welke waarde we er aan toekennen, omdat het cookie toch zal worden gewist:

var vorigjaar = new Date(1998,1,1,0,0,0,0);
document.cookie = "rekeningnummer=xyz; expires=" + vorigjaar.toGMTString()

Beveiliging

Veel mensen schakelen uit privacy-overwegingen het gebruik van cookies in hun browser uit, of willen op zijn minst gewaarschuwd worden voordat er cookies worden verstuurd. Echter: ook zonder cookies zou een kwaadwillende site-eigenaar met JavaScript dingen kunnen doen die in strijd zijn met de gewenste privacy en beveiliging. De ontwerpers van JavaScript hebben daarom een aantal maatregelen genomen om de kans op misbruik zo klein mogelijk te maken.

Allereerst heeft men besloten om de taal geen faciliteiten te geven voor het benaderen van bestanden op de computer van de eindgebruiker: er is geen File-object of iets dergelijks, en het is niet mogelijk om bestanden op de harde schijf te wissen of te wijzigen.

Daarnaast heeft men bepaalde informatie, zoals het emailadres van de gebruiker en de URLs in de browser-history, ontoegankelijk gemaakt voor JavaScript, zodat een boosaardig script bijvoorbeeld niet kan ontdekken welke websites de gebruiker zoal heeft bezocht.

Netscape Navigator maakt echter vanaf versie 4 gebruik van een "privilege-model". Dit houdt bijvoorbeeld in dat scripts toch toegang kunnen krijgen tot het History-array, mits ze het "UniversalBrowserRead"-privilege hebben. Dit privilege moet eerst door het script worden aangevraagd met behulp van de enablePrivilege-methode, en pas als de gebruiker daarvoor toestemming heeft gegeven, kan de potentieel "onveilige" functie worden uitgevoerd. (Het privilege-model van Netscape is overigens gebaseerd op het beveiligingsmodel van Java, en privileges worden aangevraagd en beheerd met behulp van een Java-class.)

Hoe weet een gebruiker nu of hij wel of niet toestemming moet geven voor het verlenen van een privilege? Dat bepaalt hij aan de hand van een "digitale handtekening" die aangeeft wie de auteur is van het script (heeft een script zo'n handtekening, dan is sprake van een "signed script"). Hierbij wordt gebruik gemaakt van cryptografische technieken om te voorkomen dat de eigenlijke auteur zich kan voordoen als een ander.

Ook Internet Explorer gebruikt weliswaar digitale handtekeningen, maar alleen voor ActiveX-elementen en niet voor JavaScript; er is bij deze browser dan ook geen sprake van een privilege-model, en "gevoelige" informatie als de inhoud van het History-array kan nooit vanuit JavaScript worden opgevraagd.

Zelfde oorsprong

Een laatste belangrijke beperking die JavaScript in verband met de beveiliging aan scripts stelt is het zogenaamde "same origin"-beleid. Dit houdt in dat een script alleen toegang heeft tot de eigenschappen van documenten met dezelfde oorsprong als het script, d.w.z.: documenten die afkomstig zijn van dezelfde host als het script. (In Netscape Navigator 4+ kunnen scripts deze beperking weer op basis van door de gebruiker toegekende privileges omzeilen.)

Indien een scherm bijvoorbeeld twee frames bevat, en de inhoud van de frames is afkomstig van twee verschillende servers, dan heeft een script in het ene scherm geen toegang tot de eigenschappen van het document in het andere frame. Dit geldt althans voor de "ingebouwde" eigenschappen van documenten; indien de auteur van een document zelf eigenschappen in JavaScript heeft gedefinieerd, dan zijn die wel toegankelijk voor scripts van een andere server.

Een voorbeeld: de document-eigenschap "links" is ingebouwd en daardoor ontoegankelijk; als we echter zelf een eigenschap "exposedlinks" definiëren, en daar de waarde van "links" aan toekennen (document.exposedlinks = document.links), dan is "exposedlinks" wel toegankelijk voor scripts van andere servers. We kunnen dit met opzet doen als we willen dat "externe" scripts toegang hebben tot de eigenschappen van ons document, maar willen we dat niet, dan moeten we op dit potentiële beveiligingsprobleem bedacht zijn!

Conclusie

In de voorlaatste aflevering hebben we gezien hoe JavaScript omgaat met cookies en beveiliging. In de laatste aflevering komen tenslotte nog enkele meer geavanceerde onderwerpen aan de orde.