Auteur: Thomas - 27 augustus 2004 - 14:27 - Gekeurd door: Dennisvb - Hits: 61435 - Aantal punten: 4.23 (44 stemmen)
----------------------------------------------------------------------- WAARSCHUWING - DIT SCRIPT IS VEROUDERD / NIET 100% VEILIG (MEER)
-----------------------------------------------------------------------
2014-10-29
Dit script (ondertussen meer dan 10 jaar oud!) valt eigenlijk in een aantal onderdelen uiteen, die eigenlijk afzonderlijk behandeld zouden moeten worden:
- communicatie met de database (database-laag)
- gebruikersbeheer (gebruikersbeheer/-backend)
- gebruik (en verwerking) van formulier(-data) (formuliersysteem)
- authenticatie van een gebruiker (authenticatie/beveiliging)
- rechtenbeheer van gebruikers (rechtenbeheer/-backend)
- het combineren van bestanden die samen een "pagina" vormen (templatesysteem of equivalent)
In dit script zijn er veel aannames gedaan over hoe je webpagina's in elkaar zitten, dit maakt het script waarschijnlijk minder geschikt om (naadloos) te integreren in je eigen code. Dit neemt niet weg dat je de principes kunt gebruiken om delen van je website af te schermen.
De crux van dit geheel is het gebruik van sessies. Sessies stellen je, net als cookies maar dan op een "veilige(re)" manier, in staat om informatie te onthouden tussen verschillende pagina's van een website. Een sessie kan dus bijvoorbeeld onthouden dat je op een zeker moment een juiste gebruikersnaam/wachtwoord-combinatie hebt ingevoerd die jou (hopelijk) identificeert als de bijbehorende gebruiker.
Een tweetal dingen uit het onderstaande script zou ik (structureel gezien) nou waarschijnlijk anders aanpakken:
1. de opslag van rechten/privileges in de sessie
Het script werkt zo dat de rechten die iemand heeft (hier $_SESSION['slevel']) opgeslagen worden in de sessie zelf.
Dit heeft zowel voor- als nadelen. Een voordeel is dat je niet elke page-access de rechten hoeft op te halen uit de database (dit scheelt dus een of meer queries). Een nadeel is dat deze rechten van kracht blijven zolang de sessie van
de gebruiker bestaat, ook al pas je ondertussen de rechten aan in de database. De enige manier om iemand zijn "sessie rechten" in te trekken is deze gebruiker geforceerd (via code of anderszins) uit te laten loggen. Een mogelijk beter
alternatief is dat je in je sessie geen rechten opslaat (maar enkel een gebruikers-id) maar dat je deze rechten elke page-access opnieuw berekent en bijvoorbeeld opslaat in een user-object. De user-klasse zou je vervolgens kunnen voorzien van methoden waarmee je bepaalt of een gebruiker een bepaalde handeling mag verrichten. Hiermee bewerkstellig je waarschijnlijk een betere "scheiding van verantwoordelijkheden": je sessie onthoudt (enkel) wie je bent, met het (elke page-access opnieuw geconstrueerde) user object bepaald je verdere authenticatie-vraagstukken (if ($user->hasRights($array_met_te_controleren_verplichte_rechten)) { ... } o.i.d.).
2. het "doorloggen"
Indien in het onderstaande script de sessie is verlopen (door timeout of wat dan ook) en je hebt aangegeven dat je je login wilt onthouden, dan zal worden getracht om de sessie on-the-fly door te starten. Hierbij wordt de pagina wel ververst. Als je op dat moment dus bijvoorbeeld een lange post aan het schrijven was dan gaat deze informatie verloren. Het zou dus beter zijn om dus niet tussendoor te redirecten. Als je er voor zorgt dat de heropbouw van de sessie plaatsvindt voordat je bijvoorbeeld controleert of iemand toegang heeft tot een pagina (dus ook -als we voortborduren op het gebruik van een user class-
voordat we het user object opnieuw bouwen (wat op zijn beurt weer zijn informatie uit de sessie onttrekt)), dan zou deze doorstart geruisloos kunnen plaatsvinden.
Je kunt het onderstaande script wel gebruiken om enigszins vertrouwd te raken met de materie, maar echt veilig is deze niet meer. Wel zou dit script eenvoudig veilig(er) gemaakt kunnen worden, maar het is beter om een iets andere opzet te hanteren wanneer je dit soort functionaliteit wilt. Je zou daarvoor dit script in combinatie met bovenstaande notities als leidraad kunnen gebruiken om zelf iets te bouwen.
EINDE WAARSCHUWING / EXTRA NOTITIES
-----------------------------------------------------------------------
Inleiding:
Met dit script kun je members met een gebruikersnaam en wachtwoord (afhankelijk
van het 'gebruikersniveau' of user level) toegang geven tot bepaalde delen van
je website, en anderen de toegang ontzeggen. Met behulp van een sessie (sessie-
variabelen) bepalen we tot welke delen van de website gebruikers toegang
hebben.
HOE je gebruikers deze rechten geeft en wat deze rechten inhouden is een ander
verhaal. Dit script beschrijft alleen het beveiligingsmechanisme.
Vereisten / aannames:
* De webmaster heeft de beschikking over een MySQL database waarin onder andere
gebruikersnaam, (versleuteld) wachtwoord en gebruikersniveau staan.
LET EROP dat het veld 'pass' tenminste 32 karakters lang dient te zijn, in
verband met gehashde passwords.
* sessies moeten ondersteund worden door de webserver
* Enige kennis van sessies, php functies (trim, md5, strcmp), binair tellen
(bitwise comparator)
Dit script is door mij al enige tijd in gebruik, en tot dusver heb ik nog geen
problemen ondervonden.
Mocht je echter problemen ondervinden (of gaten in de beveiliging ontdekken
die ik over het hoofd heb gezien ;)) mail of reply gerust.
Probeer (als iets niet werkt) eerst zelf uit te vogelen waarom iets niet werkt,
daar leer je echt het meeste van.
Lees ook eerst alles goed door; in de scripts zelf staat voldoende info om
vooruit te komen.
Bestanden:
- connect.php
In dit bestand wordt een connectie gemaakt naar je MySQL-databaseserver,
en selecteer je je eigen database.
- een login-script (login.php)
Hier type je je gebruikersnaam en wachtwoord in. In de database (of een file)
wordt gecontroleerd of je een bekende gebruiker bent. Als dit het geval is en
je wachtwoord klopt, worden enkele sessie-variabelen geïnitialiseerd en
ben je ingelogd. Vervolgens heb je toegang tot bepaalde member-only delen van
de site.
Hier kun je tevens aangeven of je login onthouden moet worden (op de machine
waar je mee inlogt).
Als je dit wilt, zal er een cookie worden aangemaakt en wordt in de database
bijgehouden onder welk IP-nummer je bent ingelogd. Het onthouden van je login
zal dus NIET werken op machines die een dynamisch (internet-)IP-nummer hebben.
Mijn script is zo gemaakt, dat wanneer je een bepaalde tijd niets
uitvoert, je op een gegeven moment weer uitgelogd bent (timeout) - deze "idle
time" kun je naar eigen smaak aanpassen.
- een logout-script (logout.php)
Hier wordt je sessie gedestroyed en wordt het sessie array leeggemaakt;
oftewel alle sessie gegevens worden verwijderd.
Wanneer er een cookie was geset die je login onthield, word deze hier
gecleared.
- een sessie-script (session.php)
Dit script dien je overal te includen waar je je pagina wilt beveiligen
(dus include("session.php"); of require("session.php"); - zie ook
voorbeeld.php) In deze include wordt gekeken of je idle-time is overschreden
en naar aanleiding daarvan worden acties uitgevoerd.
Als je nog niet bent ingelogd, maar bij een eerder bezoek hebt aangegeven dat
je login onthouden moest worden (en wanneer je nog steeds hetzelfde IP-nummer
als toen had) zal je sessie automatisch opnieuw opgestart te worden - je
wordt dan automatisch ingelogd.
- een voorbeeld-pagina (voorbeeld.php)
Hier worden (afhankelijk van het gebruikersniveau van een ingelogd persoon)
bepaalde delen van een pagina al dan niet zichtbaar gemaakt.
Uitbreiding:
Vanaf versie 1.2 is het mogelijk om je login te 'onthouden' door middel van het
setten van een cookie. Hierin staan de username en het encrypted wachtwoord van
een gebruiker opgeslagen. Om ervoor te zorgen dat mensen niet met een gestolen
cookie in kunnen loggen met de gegevens uit het cookie, wordt in de database
het IP bijgehouden van de machine van de persoon die het laatst succesvol is
ingelogd.
In de verschillende pagina's van het script zal aangegeven worden welke dingen
je moet toevoegen voor de gebruikmaking van deze extra functionaliteit.
<?php
require("connect.php"); // verbinding met de database maken
?>
<html>
<head>
<title>sessies · aanmelden</title>
</head>
<body>
<?php
// als het formulier nog niet is ingevuld
if(!isset($_POST['submit'])) {
?>
<form action="aanmeld.php" method="post">
naam <input type="text" name="naam" size="40" maxlength="20" /><br />
wachtwoord <input type="password" name="wacht1" size="40" maxlength="30" /><br />
wachtwoord opnieuw <input type="password" name="wacht2" size="40" maxlength="30" /><br />
<!--
en wat je verder over deze gebruiker bij wilt houden
bijvoorbeeld leeftijd, woonplaats, email, ...
voor het opvragen van het wachtwoord etc. etc.
hier moet je dan ook velden voor in je tabel 'members' aanmaken.
Op deze extra velden moeten hieronder ook controles uitgevoerd worden of ze ingevuld zijn!
-->
<input type="submit" name="submit" value="submit" />
</form>
<?php
// formulier gepost, kijk of alle velden ook daadwerkelijk zijn ingevuld
} elseif(trim($_POST['naam']) <> "" && trim($_POST['wacht1']) <> "") {
// formulier ingevuld - kijk eerst of de gebruiker al bestaat
$naam = $_POST['naam'];
$res = mysql_query("SELECT * FROM users WHERE name='".$naam."'") or die(mysql_error());
if(mysql_num_rows($res) == 0) {
// geen resultaten - dit is wat we willen
// kijk of de opgegeven wachtwoorden overeenkomen
if(!strcmp($_POST['wacht1'], $_POST['wacht2'])) {
// wachtwoorden komen overeen - sla alle gegevens op in de database
// naam is al opgehaald uit het formulier
$wacht = md5($_POST['wacht1']); // versleuteld wachtwoord
$level = 1; // standaard gebruikersniveau
mysql_query("INSERT INTO users (name, pass, level) VALUES ('".$naam."','".$wacht."',".$level.")") or die(mysql_error());
// geef melding weer
?>
Je gegevens zijn opgeslagen.<br />
Je kunt <a href="login.php">hier</a> naartoe om in te loggen.<br />
<?php
} else {
// wachtwoorden komen niet overeen
?>
De twee opgegeven wachtwoorden zijn niet hetzelfde.<br />
Druk op de "back" knop van je browser en voer twee identieke wachtwoorden in.<br />
<?php
}
} else {
// er bestaat al een gebruiker met deze naam
?>
Er bestaat al een gebruiker met deze naam.<br />
Druk op de "back" knop van je browser en geef een andere naam op.<br />
<?php
}
} else {
// sommige velden zijn niet ingevuld
?>
Alle velden dienen ingevuld te worden.<br />
Druk op de "back" toets en vul in alle velden wat in.<br />
<?php
}
?>
</body>
</html>
<?php
require("connect.php");// verbinding met de database maken
<?php
require("connect.php"); // connectie met database maken en database selecteren
session_start(); // start een sessie of zet een sessie voort
// controleer hier of iemand inlogt - dit moet _voor_ de <HTML>-tag gebeuren gebeuren
if(isset($_POST['login'])) {
if(trim($_POST['naam']) <> "" && trim($_POST['wacht']) <> "") {
// naam en wachtwoord zijn ingevuld. Haal het (versleutelde) wachtwoord en
// het gebruikersniveau dat bij deze gebruikersnaam hoort uit de database op
// en vergelijk dit wachtwoord met het in het formulier opgegeven wachtwoord.
$naam = $_POST['naam'];
$wacht = md5($_POST['wacht']);
$res = mysql_query("SELECT id, pass, level FROM users where name='".$naam."'") or die(mysql_error());
// Als er een resultaat is, oftewel er bestaat een gebruiker met de naam $naam
if(mysql_num_rows($res) > 0) {
$row = mysql_fetch_assoc($res);
// aanname: het wachtwoord in de database is eenmalig gehashed mbv de functie md5()
// vergelijk het opgehaalde wachtwoord met een versleutelde versie van het in het
// formulier opgegeven wachtwoord
if(!strcmp($wacht, $row['pass'])) {
// alle gegevens kloppen
// v1.2 extra functionaliteit, onthouden login
// wil de gebruiker zijn gegevens onthouden ?
if(isset($_POST['memory'])) {
// set cookie (voor 2 maanden) en onthoud het IP
// gebruik hierbij het id van de gebruiker
setcookie("login_cookie", $row['id'].";".$row['pass'], time()+3600*24*31*2, "/");
$ip = $_SERVER['REMOTE_ADDR'];
mysql_query("UPDATE users SET last_ip='".$ip."' WHERE id=".$row['id']) or die(mysql_error());
}
// vul sessievariabelen
$_SESSION['suser'] = $naam; // gebruikersnaam van ingelogd persoon
$_SESSION['slevel'] = $row['level']; // bijbehorende gebruikersniveau
$_SESSION['stime'] = time(); // de huidige tijd
$_SESSION['smaxidle'] = 60 * 60; // het aantal seconden inactiviteit
} else {
// wachtwoorden komen niet overeen, breek de sessie weer af
$_SESSION = array();
session_destroy();
}
// geef de resultaten van deze query weer vrij
unset($row);
mysql_free_result($res);
}
// ververs, of de gebruikersnaam/wachtwoord combinatie nou klopt of niet, de pagina
header("Location: login.php");
}
}
?>
<html>
<head>
<title>sessies · login</title>
</head>
<body>
<?php
// als de (een willekeurige) sessievariabele suser in het sessie-array nog geen waarde heeft
// (dus als iemand nog niet is ingelogd)
if(!isset($_SESSION['suser'])) {
?>
<form action="login.php" method="post">
naam <input type="text" name="naam" size="15"><br />
wachtwoord <input type="password" name="wacht" size="15"><br />
<!-- v1.2 extra functionaliteit, login onthouden -->
<input type="checkbox" name="memory" value="1"> onthoud mijn login (gebruikt cookie)<br />
<input type="submit" name="login" value="log in"><br />
</form>
<?php
} else {
// de gebruiker is ingelogd - geef een welkomstboodschap oid
// hier kun je tevens kijken of alle sessievariabelen de goede
// waarden hebben (debugging)
?>
Hi <b><?= $_SESSION['suser'] ?></b>,<br />
<a href="voorbeeld.php">voorbeeld pagina</a><br />
<a href="logout.php">uitloggen</a><br />
<?php
}
?>
</body>
</html>
<?php
require("connect.php");// connectie met database maken en database selecteren
session_start();// start een sessie of zet een sessie voort
// controleer hier of iemand inlogt - dit moet _voor_ de <HTML>-tag gebeuren gebeuren
<?php
session_start(); // start een sessie of zet een sessie voort
// als de gebruiker is ingelogd
if(isset($_SESSION['suser'])) {
// het volgende timeout deel is optioneel - dit mag worden weggelaten
// * timeout gedeelte *
$now = time();
// als er meer tijd is verstreken dan smaxidle
// sinds het aanmaken van de sessie
if($now - $_SESSION['stime'] > $_SESSION['smaxidle']) {
// breek de sessie af, de gebruiker dient opnieuw in te loggen
$_SESSION = array();
session_destroy();
} else {
// ververs anders de sessietijd. Dit zorgt er voor
// dat de gebruiker ingelogd blijft zolang deze actief is.
$_SESSION['stime'] = $now;
}
// * einde timeout gedeelte *
// v1.2 extra functionaliteit, onthouden login
} elseif(isset($_COOKIE['login_cookie'])) {
// bekijk de waarden van de cookie en als deze kloppen met de database - start alsnog een sessie
// aanname - er is een verbinding met de database
list($id, $wacht) = split(";", $_COOKIE['login_cookie']);
$res = mysql_query("SELECT id, pass, level, last_ip FROM users WHERE id='".$id."'") or die(mysql_error());
if(mysql_num_rows($res) > 0) {
$row = mysql_fetch_assoc($res);
if(!strcmp($wacht, $row['pass']) && $_SERVER['REMOTE_ADDR'] == $row['last_ip']) {
// init session
$_SESSION['suser'] = $naam;
$_SESSION['slevel'] = $row['level'];
$_SESSION['stime'] = time();
$_SESSION['smaxidle'] = 60 * 60;
// update cookie
// gebruik hierbij wederom het id en het versleutelde wachtwoord
setcookie("login_cookie", $id.";".$wacht, time()+3600*24*31*2, "/");
} else {
// password of ip komt niet overeen - unset het cookie en beeindig de sessie
setcookie("login_cookie", "", time(), "/");
$_SESSION = array();
session_destroy();
}
// geef resultaten vrij
unset($row);
mysql_free_result($res);
} else {
// gebruiker onbekend, cookie vervalst ?
$_SESSION = array();
session_destroy();
}
// ververs de pagina
header("Location: ".$_SERVER['REQUEST_URI']);
}
?>
<?php
session_start();// start een sessie of zet een sessie voort
<?php
// include/require hier evt nog andere zaken
require ("connect.php");
// we willen op deze pagina gebruik maken van beveiliging mbv sessies,
// dus includen (requiren) we session.php
require("session.php");
?>
<html>
<head>
<title>sessies · voorbeeld</title>
</head>
<body>
<?php
/*
we gaan hier kijken of de gebruiker is ingelogd, en welk
user level de gebruiker heeft. Op grond daarvan laten we
bepaalde delen al dan niet zien.
Een gebruikerslevel is een getal wat aangeeft hoeveel
"macht" je hebt. Vaak is het zo: hoe hoger het getal, hoe
meer je mag.
bijvoorbeeld:
Voor een bepaalde bewerking heb je gebruikers-
niveau 1 nodig, maar voor een andere bewerking heb je niveau
2 nodig. Een gebruiker die beide bewerkingen mag uitvoeren
heeft gebruikersniveau 1+2 = 3. MAAR: Een gebruiker die om
een of andere reden alleen de tweede bewerking mag uitvoeren
heeft gebruikersniveau 0+2 = 2. Dus je telt de nummers die
bij bepaalde rechten horen bij elkaar op.
Het nummer dat het recht geeft op een bepaalde bewerking is
altijd een macht van 2.
bijvoorbeeld:
recht #1 (bv inloggen) heeft gebruikers niveau 2^0 = 1
recht #2 (bv je eigen info veranderen) heeft gebr. niveau 2^1 = 2
recht #3 (bv nieuws toevoegen) heeft gebruikers niveau 2^2 = 4
recht #4 (bv members toevoegen) heeft gebruikersniveau 2^3 = 8
enz.
Iemand die al deze bewerkingen mag uitvoeren heeft dus
gebruikersniveau 1+2+4+8 = 15 (of 2^4 - 1)
Iemand die alleen recht #1 en recht #3 heeft, heeft
dus gebruikersniveau 1+4 = 5
*/
// controle op ingelogd zijn:
if(isset($_SESSION['suser'])) {
?>
user <b><?= $_SESSION['suser'] ?></b> is logged in.<br />
<?php
/*
vervolgens kijken we naar het userlevel, we vergelijken
bitsgewijs het gebruikerslevel - dit doen we met behulp van
een enkele '&' (de bitwise comparator)
*** LET OP ***
Enkel controleren met & is niet genoeg !
Stel dat je level 9 moet hebben voor een bepaalde bewerking, en je hebt
maar level 1. 1 & 9 is gelijk aan 1, en dan zou if(1 & 9) { ... } true opleveren
Je moet dus expliciet controleren of je level hoog genoeg is.
*/
if(($_SESSION['slevel'] & 1) == 1) {
// voer code uit behorend bij recht #1
?>
Je hebt recht #1.<br />
<?php
} else {
// geef een melding dat je de acties
// behorend bij recht #1 niet mag uitvoeren
?>
Je hebt recht #1 NIET.<br />
<?php
}
if(($_SESSION['slevel'] & 2) == 2) {
// voer code uit behorend bij recht #2
?>
Je hebt recht #2.<br />
<?php
} else {
?>
Je hebt recht #2 NIET.<br />
<?php
}
if(($_SESSION['slevel'] & 4) == 4) {
// voer code uit behorend bij recht #3
?>
Je hebt recht #3.<br />
<?php
} else {
?>
Je hebt recht #3 NIET.<br />
<?php
}
if(($_SESSION['slevel'] & 8) == 8) {
// voer code uit behorend bij recht #4
?>
Je hebt recht #4.<br />
<?php
} else {
?>
Je hebt recht #4 NIET.<br />
<?php
}
// et cetera
?>
<a href="logout.php">uitloggen</a><br />
<?php
} else {
?>
Je bent op dit moment niet ingelogd.<br />
<a href="login.php">inloggen</a><br />
<?php
}
?>
</body>
</html>
<?php
// include/require hier evt nog andere zaken
require("connect.php");
// we willen op deze pagina gebruik maken van beveiliging mbv sessies,
// dus includen (requiren) we session.php
require("session.php");
?>
<html>
<head>
<title>sessies · voorbeeld</title>
</head>
<body>
<?php
/*
we gaan hier kijken of de gebruiker is ingelogd, en welk
user level de gebruiker heeft. Op grond daarvan laten we
bepaalde delen al dan niet zien.
Een gebruikerslevel is een getal wat aangeeft hoeveel
"macht" je hebt. Vaak is het zo: hoe hoger het getal, hoe
meer je mag.
bijvoorbeeld:
Voor een bepaalde bewerking heb je gebruikers-
niveau 1 nodig, maar voor een andere bewerking heb je niveau
2 nodig. Een gebruiker die beide bewerkingen mag uitvoeren
heeft gebruikersniveau 1+2 = 3. MAAR: Een gebruiker die om
een of andere reden alleen de tweede bewerking mag uitvoeren
heeft gebruikersniveau 0+2 = 2. Dus je telt de nummers die
bij bepaalde rechten horen bij elkaar op.
Het nummer dat het recht geeft op een bepaalde bewerking is
altijd een macht van 2.
bijvoorbeeld:
recht #1 (bv inloggen) heeft gebruikers niveau 2^0 = 1
recht #2 (bv je eigen info veranderen) heeft gebr. niveau 2^1 = 2
recht #3 (bv nieuws toevoegen) heeft gebruikers niveau 2^2 = 4
recht #4 (bv members toevoegen) heeft gebruikersniveau 2^3 = 8
enz.
Iemand die al deze bewerkingen mag uitvoeren heeft dus
gebruikersniveau 1+2+4+8 = 15 (of 2^4 - 1)
Iemand die alleen recht #1 en recht #3 heeft, heeft