image

PHP sessions; hoe het wel moet

maandag 9 augustus 2010, 22:36 door Certified Secure, 13 reacties

Helaas, door een cross site scripting probleem op je favoriete website heeft een hacker je session cookie bemachtigd, ga niet langs start. De hacker heeft ondertussen toegang tot jouw sessie op de website, en daar gaat al je e-mail, je vriendjes of je matches. Ok, cross site scripting hoort niet voor te komen, maar een meerlaags security model had dit kunnen opvangen. Jammer dat PHP (en andere web programmeertalen) de sessies niet veilig implementeren. Hoogste tijd om het zelf te doen.

Omdat een webpagina eigenlijk stateless is, wordt tegenwoordig altijd een cookie gebruikt met hierin een sessionid. Dit id is uniek voor de sessie die jij met de website hebt, en geeft de server de mogelijkheid om informatie van jouw sessie bij te houden. Na het inloggen met gebruikersnaam Piet weet de server bijvoorbeeld dat de sessie met jouw sessionid is ingelogd als gebruiker Piet.

Hackers kunnen jouw sessionid op twee manieren bemachtigen. De eerste heet session-fixation, waarbij de hacker vooraf een sessionid bepaalt, en jou deze laat gebruiken. De tweede heet session-hijacking, waarbij de hacker je sessionid kaapt met behulp van cross site scripting. Mocht je meer willen weten over deze twee aanvallen, zoek dan op de termen 'session fixation', 'cross site scripting', 'session hijacking', dit artikel gaat juist over de beveiliging van de sessies.

Voor veilige sessies in PHP zul je zelf wat moeten programmeren. Als het goed is heb je al in een initialisatiescript een aanroep van session_start() staan. Er bestaan in PHP een paar functies om wat parameters van het session cookie aan te passen:

void session_set_cookie_params ( int lifetime 
[, string path [,string domain [, bool secure
[, bool httponly]]]] )
string session_name ( [string name] )

Met het lifetime argument kunnen we aangeven hoelang het cookie maximaal mag bestaan, dit kunnen we het beste op 0 zetten, zodat de cookies niet worden opgeslagen in de browser. De timeout van een sessie kunnen we beter zelf bepalen. Omdat er meerder PHP sites op dezelfde webserver kunnen draaien is het belangrijk het path en domein goed in te stellen. Vanwege dit is het belangrijk de session_name functie te gebruiken om een session-id cookie alleen voor jouw applicatie te laten genereren. Het secure argument geeft aan of het cookie alleen via SSL mag worden verzonden, wat natuurlijk essentieel is voor enige veiligheid. De optie httponly is nieuw in 5.2.0, en vertelt de browser dat Javascript geen toegang heeft tot dit cookie. Niet elke browser ondersteunt dit nog, maar het is in ieder geval een mooie extra maatregel. Voor mijn testapplicatie komt dit neer op:

session_name($cookiename);
session_set_cookie_params(0,
"/~test/cs/test.php", "127.0.0.1",
true, true);

De secure (cookie mag alleen via SSL worden verzonden) optie is leuk om te gebruiken, maar het cookie wordt bij een onbeveiligde verbinding nu wel vrolijk onversleuteld naar de gebruiker verzonden. Om dit tegen te gaan moeten we helemaal aan het begin controleren of er wel gebruik wordt gemaakt van een SSL verbinding, en anders de gebruiker redirecten.

if (!$_SERVER['SSL_PROTOCOL']) {
header("Location: https://127.0.0.1/~test/cs/test.php");
exit;
}

Om beveiliging tegen session fixation te bieden, kunnen we in het sessieobject een waarde initialized toevoegen. Als deze waarde nog niet bestaat, is het een nieuwe sessie, en genereren we een nieuw random sessionid:
if (!$_SESSION || $_SESSION['initialized'] !== true) {
session_regenerate_id(true);
$_SESSION['initialized'] = true;
}

Tot slot is het mooi wanneer we het ip-adres van elke sessie bijhouden. Mocht een session-id dan worden gekaapt en gebruikt worden vanaf een ander systeem, dan zien we dit omdat het ip-adres anders is. Ik las dat er wordt geadviseerd om uit te gaan van de versie van de browser bij deze detectie. Het mag duidelijk zijn dat dit geen extra veiligheid biedt. Tegenwoordig is het spoofen van een HTTP header veld (waar de browser versie in wordt meegestuurd) veel eenvoudiger dan het spoofen van een ip-adres.

De complete implementatie in een mooi direct bruikbare functie, inclusief ip-adres is als volgt:


function securesession
($path, $domain, $cookiename, $secureurl) {
/* Force the usage of SSL */
if (!$_SERVER['SSL_PROTOCOL']) {
header("Location: $secureurl");
exit;
}
/* Set correct sessioncookie parameters */
session_name($cookiename);
session_set_cookie_params(0, $path, $domain, true, true);
session_start();
/* Test for a proxy */
$forward = "";
if (defined($_SERVER['REMOTE_ADDR'])) {
$forward = $_SERVER['REMOTE_ADDR'];
}
/* Force the creation of a sessionid */
if (!$_SESSION || $_SESSION['initialized'] !== true) {
session_regenerate_id(true);
$_SESSION['initialized'] = true;
$_SESSION['REMOTE_ADDR'] = $_SERVER['REMOTE_ADDR'];
$_SESSION['X_FORWARDED_FOR'] = $forward;
}
/* Test the user's ip and possible proxy */
if (($_SERVER['REMOTE_ADDR'] != $_SESSION['REMOTE_ADDR']) ||
($_SESSION['X_FORWARDED_FOR'] != $forward)) {;
$_SESSION = array();
if (isset($_COOKIE[session_name()])) {
setcookie(session_name(), '',
time()-42000, $path,
$domain, false, true);
}
session_destroy();
exit();
}
securesession("/~test/cs",
"127.0.0.1", "TESTID",
"https://127.0.0.1/~test/cs/test.php");
print "Welcome";
?>

De principes in dit artikel kunnen uiteraard ook worden toegepast voor ASP, JSP en andere web-programmeertalen. Dit laten we liever over als oefening voor de lezers.
Reacties (13)
10-08-2010, 07:55 door T3K_
Goed uitgelegd, CS Bedankt!
10-08-2010, 08:56 door Anoniem
Volgens mij wordt hier een oplossing geboden van wat het oorspronkelijke probleem niet is.

Probleem: de site lijdt aan Cross Site Scripting (XSS)
Hierdoor kunnen er O.A. sessie cookies worden gekaapt, maar er zijn ook backdoors (bijv http://xss-proxy.sourceforge.net/) die je helpen dit lek uit te buiten, waarbij je op 'kosten' van de gebruiker verder kunt gaan. Je zou bijv bij een XSS van een bank hier gebruik van maken en content aanpassen op runtime.

De oplossing is dus NIET om de cookies http-only te maken, maar om te zorgen dat je geen xss fouten meer kunt maken. Dit kan door over striphtml te gebruiken, maar wij als mensen vergeten dat altijd op een keer, dus een slechte methode. Beter is om een goede api te gebruiken hiervoor. php-template bijv. Je geeft hierbij het format op waarin de pagina moet verschijnen, waarna je aangeeft welke waardens erin moeten verschijnen. De api zorgt er dan voor dat de waardens correct geescaped worden. Als programmeur hoef je je dan geen zorgen meer te maken om per ongeluk fouten te maken.


Helaas, door een verkeerde analyse van het probleem lijdt je site nog steeds aan cross site scripting. De hacker heeft je bankgegevens aangepast, ga niet langs start. Jammer dat PHP (of de programmeur?) het schrijven van html niet veilig implementeren. Hoogste tijd om het TE LATEN doen.
10-08-2010, 10:09 door Anoniem
"!$_SESSION"
Dit werkt misschien in JavaScript, maar in PHP gebruiken we hier isset() voor! Anders krijg je namelijk een E_NOTICE. En nee uitschakelen is geen optie!!

De http://www.phpfreakz.nl/downloadz/webprogrammers_hacking_huide.pdf staat vol handige tips (waaronder sessies beveiligen).
10-08-2010, 11:22 door Anoniem
Gevaarlijk artikel. Een andere orde van xsshacks die hij uitlegt, is javascript injection in attributen (bijv in src tag script setting).

Code die hij gebruikt:
# check if the user wants to execute javascript
if (substr($inputTmp, 0, 11) == 'javascript:') {
trigger_error("Someone tried to exploit our UBB system! Original input: '$input'", E_USER_WARNING);
return false;
}

Dit soort 'eigen' functies zijn altijd erg gevaarljik, probeer nooit zelf zoiets te maken. Browsers zitten namelijk vol met quirks. Wist je dat bijv op IE java\0script:alert(3); ook werkt? Je kunt dan heel wijs op 'javascript:' checken, maar zulk soort checks zijn altijd te omzeilen.
10-08-2010, 11:35 door Anoniem
@reactie 2

Het artikel gaat over correcte sessie implementatie. Op basis van mijn kennis heb ik geen fouten kunnen herleiden in dit artikel, enig min puntje is dat men ook als extra veiligheid: session_regenerate_id() kan gebruiken. Daarnaast geef ik je wel gelijk dat het geen oplossing is voor XSS, ook niet voor variaties van CSRF. HTTPOnly is een additionele module toegevoegd door browser ontwikkelaars om JavaScript geen toegang te geven tot de Cookie. Het is geen oplossing -ook nooit zo bedacht als oplossing- maar een extra stap in het moeilijker maken van het stelen van cookies. Maar ik denk dat jij ook het verkeerde voorbeeld geeft om hoe correct om te gaan met XSS, zie mijn reactie s.v.p. als opbouwende kritiek.

"De oplossing is dus NIET om de cookies http-only te maken, maar om te zorgen dat je geen xss fouten meer kunt maken. Dit kan door over striphtml te gebruiken, maar wij als mensen vergeten dat altijd op een keer, dus een slechte methode. Beter is om een goede api te gebruiken hiervoor. php-template bijv. Je geeft hierbij het format op waarin de pagina moet verschijnen, waarna je aangeeft welke waardens erin moeten verschijnen. De api zorgt er dan voor dat de waardens correct geescaped worden. Als programmeur hoef je je dan geen zorgen meer te maken om per ongeluk fouten te maken."

1. striphtml ?

Hopelijk bedoel je:

$value = htmlspecialchars($value, ENT_QUOTES,'UTF-8');
en niet:

$value = strip_tags($value);
Want de laatste is in gevallen te omzeilen. Er is een vrij goede consensus bij web-programmeurs waarbij de richtlijn is om alle html karakters te coderen naar hun entiteiten, waarbij htmlspecialchars() een grote rol speelt.

2. Beter is om een goede api te gebruiken hiervoor. php-template bijv?

Nee, dit is geen oplossing an sich. Omdat je niet weet wat die API doet met de data en voorts staat templating hier totaal los van. Ik heb vaak genoeg fouten ontdekt waarbij de template engine zwakke regular expressie regels bevatte.

3. "waardens correct geescaped worden" (sic)

'Escapen' is alleen noodzakelijk is voor data waarmee een database query wordt aangemaakt, en dus niet voor de uitgave van data in een document.


Vriendelijke groet,

Sasha van den Heetkamp.
10-08-2010, 11:51 door Anoniem
'Escapen' is alleen noodzakelijk is voor data waarmee een database query wordt aangemaakt, en dus niet voor de uitgave van data in een document.
Dit is juist wat ik probeer te beargumenteren dat dat juist WEL noodzakelijk is. Als je data naar de browser schrijft, moet je altijd zorgen voor een juiste escaping. Door htmlspecialchars te gebruiken, ga je dit op een gegeven moment vergeten, dus leun niet op discipline, maar zorg voor een eenvoudige api. php-template is EEN voorbeeld om escaping uit te besteden.

Dus zoals stored procedures ervoor zorgen dat je GEEN fouten meer kunt maken met sql-injection, zo moet je ook iets dergelijks hebben voor html. Iedere moderne taal heeft dat inmiddels, php heeft idd alleen van die lekke api's. (Dat het in php zeer lastig is, zie je in populaire pakketten als joomla, met veel sql injections en xss: de programmeur is de bottleneck, laten we dat veranderen en dit overlaten aan de api.

Daarnaast geeft de schijnveiligheid van http-only sessies nog steeds de mogelijkheid om je bankgegevens online aan te passen middels een xss hack.
10-08-2010, 12:25 door Anoniem
@Sasha van den Heetkamp: htmlspecialchars escaped natuurlijk tekens met een speciale betekenis in HTML.

Puntjes bij dit artikel

Vervelend hoor, als je DHCP-lease expired en je plots opnieuw moet inloggen. Of als je ineens op de andere loadbalancer zit.

Ik kan me niet voorstellen dat de "proxy" test werkt; je kijkt namelijk naar REMOTE_ADDR.

Ik snap niet hoe de "initialized" vlag sessie fixatie moet voorkomen. Een kwaadwillend gebruiker hoeft namelijk niet perse een nieuwe sessie te gebruiken, maar kan een sessiefixatie proberen met een door haar ge-initialiseerde sessie.

Het is belangrijker het sessie ID opnieuw te genereren als de authorisatielevel van een gebruiker veranderd. (bv, anonieme bezoeker -> inlog -> geregistreerde gebruiker).

Dit voorkomt het benutten van een bestaande sessie met verhoogde rechten.
10-08-2010, 12:57 door Anoniem
Het nadeel van het controleren van het remote IP adres is dat als er gebruik wordt gemaakt van een proxy-farm ipv 1 enkele proxy, het remote IP adres bij elke hit anders kan zijn: de requests van de gebruiker worden verdeeld over meerdere proxies, dus elke request kan van een ander IP adres komen.

Overigens gaat dit ook weer niet zo heel vaak fout, omdat dat soort proxy farms vaak een eindgebruiker constant met dezelfde proxy server verbinden, juist om dit soort IP controles niet te verstoren. Maar er is geen garantie...

Sommige sites hebben bij het inloggen een checkbox 'use extra security', als je die aanzet komt je IP adres ook in de sessie terecht en wordt het gecontroleerd, als je die uitlaat, wordt je IP adres niet gebruikt. Dan kan degene die inlogt zelf kiezen.
13-08-2010, 09:26 door Anoniem
if (defined($_SERVER['REMOTE_ADDR'])) {
Tsk-tsk, controleren of een constante is met de waarde van dat array element?

Goed voorbeeld van hoe het niet moet. ;-)

Een isset() bedoelde je hier waarschijnlijk....

Heb je de code wel getest? :-)
13-08-2010, 13:33 door Anoniem
eindelijk eens wat technische diepgang hier! IPV zweverig gelul!

Een hele dikke vette pluim voor Certified Secure!!!!

Heb even geen tijd om er inhoudelijk iets aan toe te voegen maar volgende keer doe ik mee.

Greetingz,
Jacco
13-08-2010, 14:41 door Anoniem
Een hele dikke vette pluim voor Certified Secure!!!![/quote]Technische diepgang, ja; maar die pluim kan je beter bewaren voor iemand die het *echt* verdient...

Dit demonstreert namelijk heel mooi hoe XSS en SQL injectie mogelijk blijven op websites: het verkeerde gebruik van de programmeertaal. "Oeps foutje, ik heb het aangepast," straks waarschijnlijk? Ja dat zeggen die websites, nadat ze gegevens hebben zitten lekken, ook altijd. :-D
13-08-2010, 15:09 door Anoniem
Ok, cross site scripting hoort niet voor te komen, maar een meerlaags security model had dit kunnen opvangen

Ik snap niet hoe "secure" sessions het cross-site scripting probleem op kunnen vangen. Het voorgestelde script gaat er vanuit dat de aanvaller via de gestolen cookie zelf gaat inloggen terwijl dit niet nodig is om een cross-site scripting bug te exploiten. Op het moment dat de aanvaller javascript kan uitvoeren in de sessie van het slachtoffer kan hij alles automatiseren. Mogelijke CSRF-tokens kunnen uitgelezen worden met XMLHttpRequest. Afhankelijk van de web applicatie kan er informatie uitgelezen worden, wachtwoorden veranderd worden, etc. Dit gebeurt dan allemaal in de session en via de browser van het slachtoffer (dit is dan ook het hele idee achter cross-site scripting) De voorgestelde mitigations (SSL, HttpOnly, IP adres loggen?, session lifetime, etc.) voegen vrijwel niks toe. Het biedt geen bescherming tegen cross-site scripting bugs en het zorgt er ook niet voor dat cross-scripting bugs moeilijker te exploiten zijn.

Ik sluit me aan bij de reactie van Anoniem (Dinsdag,08:56) dat dit een totaal verkeerde analyse van het probleem is. De focus moet liggen op het oplossen van het cross-site scripting probleem.
12-12-2010, 18:04 door Anoniem
Nog een tip:

Altijd op entree van de login pagina, direct een session_regenerate_id(true);
gevolgd na CORRECT inloggen door nog eens een session_regenerate_id(true);

Verder een polymorf-encrypted ip, user-agent, referrer én een hash van het totaal van voorgaande waarden in cookie opslaan. Username, password en token vanuit database met de waarden (hashes) van de cookie vergelijken en deze altijd op iedere pagina laten checken op juistheid.

Maakt hjckn ook al een *beetje* lastiger. :)
Reageren

Deze posting is gelocked. Reageren is niet meer mogelijk.