Säkerhet i PHP

Denna guide tar upp de viktigaste metoderna för att förbättra säkerheten i PHP skript.

Det finns en hel del fallgropar att hamna i när man arbetar med PHP. PHP är ett språk där det är väldigt lätt att bygga applikationer, och det finns inga påtvingade strukturer som man måste programmera efter, så som i t.ex. java. Detta gör att språket är ganska enkelt att lära sig, och när man inte kan språket så bra, är det lätt att göra fel. Jag ska därför gå igenom de vanligaste säkerhetsmissarna man kan göra, och vissa av dem är generella för alla scriptspråk för webben.

Indatakontroll

Numero uno, missen med stort M, är en stor säkerhetsmiss som man ser överallt. Jag uppdaterade en webbplats för ett tag sen där denna missen var av värsta sort, och företaget som hade skapat webbplatsen tog ett 5-siffrigt belopp för det. Så känner du igen dig i missen, så kanske det kan trösta lite.

Det är när man inte kontrollerar indatan i från besökare. Ett exempel på horribel kod är:

index.php?delete_id=3
---------------------
<?php
mysql_query('DELETE FROM tabell WHERE id = '. $_GET['delete_id']);
?>

Det är ingen fara, så länge du har en snäll besökare. Men man ska alltid förutsätta att ens besökare är den värsta sort av hackers det finns. Den värsta sortens hacker hade direkt testat att ändra index.php?delete_id=3 till index.php?delete_id=id. Och då hade vi skickat in detta till databasen:

<?php
mysql_query('DELETE FROM tabell WHERE id = id');
?>

Och vipps, så är all data i tabellen borta. Det är så otroligt lätt att skydda sig mot detta, och ett exempel är:

<?php
//Kontrollerar så att $_GET['delete_id'] enbart innehåller siffror
if(ctype_digit($_GET['delete_id']))
{
	mysql_query('DELETE FROM tabell WHERE id = '. $_GET['delete_id']);
}
else
{
	//Någon har ändrat frågesträngen, 
	//logga hackerförsök och skriv ut felmeddelande
}
?>

Att ta bort all data ur en tabell är kanske inte det värsta som kan hända. Det värsta som kan hända är att någon ställer frågor själv direkt till databasen via adressen. T.ex:

index.php?delete_id=3;drop database db;
index.php?delete_id=3;insert into tabell(text) values('Denna sidan är dålig!');

Där försvann helt plötsligt hela databasen "db", eller att "Denna sidan är dålig!" skrivs ut någonstans på sidan. Lyckligtvis tillåter inte MySQL att man ställer två sqlfrågor separerade med ; i en mysql_query(), men det gör SQL Server. Webbplatsen jag skrev om innan använder just SQL Server, och gjorde ingen indatakontroll. Så en dag hade det företaget som äger sidan kunnat sitta med all produktinformation raderad p.g.a. dåligt skriven kod. Ajabaja.

Men det finns fler dimensioner av indatakontroll. Ta t.ex. ett inloggningsformulär, som kan se ut så här:

<html>
<body>
<form action="?do=login" method="post">
<input type="text" name="username">
<input type="password" name="password">
<input type="submit" value="Logga in">
</form>
</body>
</html>

Och med en inloggningskod som denna:

<?php
mysql_query("SELECT * FROM users WHERE username = '{$_POST['username']}' 
				AND password = '{$_POST['password']}'");
?>

Kan tyckas vara ganska harmlöst, men tänk ifall någon skulle skriva ' OR username <> ' som användare, och ' OR password <> '. Då får vi en SQL-fråga som ser ut så här:

"SELECT * FROM users WHERE username = '' OR username <> '' 	
		AND password = '' OR password <> ''"

Dvs, man loggar in på första bästa konto. Det absolut bästa sättet att skydda sig mot detta, är att använda den inbyggda funktionen mysql_real_escape_string() (http://se2.php.net/mysql_real_escape_string). Den lägger automatiskt till ett \ före alla skadliga tecken, och fungerar ungefär så här:

<?php
$password = mysql_real_escape_string($_POST['password']);
$username = mysql_real_escape_string($_POST['username']);

mysql_query("SELECT * FROM users WHERE username = '$username'
				 AND password = '$password'");
?>

mysql_real_escape_string() förutsätter dock att du använder mysql, och att du har en öppen koppling till en databas. Dvs, att du öppnar databasen med mysql_connect() innan du använder mysql_real_escape_string().

Indatakontrollbehovet tar dock inte slut där. Har du någonsin haft en gästbok, ett forum, eller liknande, så vet du vad detta handlar om. Man måste se till att en användare inte kan skriva ut skadlig HTML på ditt forum/din gästbok, etc. Man kanske frågar sig vad som kan vara farligt med att skriva ut HTML, och det ska jag med glädje besvara. Kan man skriva ut HTML, så kan man skriva ut Javascript. Men Javascript kan du skapa oändliga loopar som krashar besökarens webbläsare. Ett annat skadligt sätt är att skriva ut en bild med gigantisk bredd och höjd. Då kommer vissa webbläsare att begära så mycket minne av operativsystemet att hela operativsystemet krashar. Nåja, nu till hur man löser det:

<?php
$input = htmlentities($_POST['input']);

mysql_query("INSERT INTO tabell(input) VALUES('$input')");
?>

Den förvandlar t.ex. ' " ' till ' &quot; ' och ' < ' till ' &lt; ' vilket gör det omöjligt för en användare att lägga in egna taggar.

Trodde du att det tog slut med indatakontroll där? Icke! Många använder include() för att inkludera sidor beroende på frågesträngar. Det kan se ut så här:

index.php
---------
<?php
if(isset($_GET['page']))
{
	include($_GET['page'].'.php');
}
?>

Finns inte sidan $_GET['page'].'.php', så kommer det skrivas ut ett fult felmeddelande som användare aldrig ska se. Men det värsta är ifall man då skriver:

index.php?page=index
Då blir det oändlig rekursion, eftersom index inkluderas i index, som inkluderas i index, som inkluderas i index, etc. etc. Det äter snabbt ut serverns minne, vilket inte är speciellt trevligt. Det allra bästa här är att köra en switchbox, som kan se ut så här:
<?php
if(isset($_GET['page']))
{
	switch($_GET['page'])
	{
		case 'info':
			include('info.php');
			break;
		case 'forum':
			include('forum.php');
			break;
		default:
			include('start.php');
			break;
	}
}
?>

På så sätt går det aldrig in någon indata i include(), och det kan aldrig bli skadligt. Har man väldigt, väldigt många filer, så kan man även använda file_exists() för att se att filen existerar, och sen inkludera. Men då finns alltid risken med att en fil inkluderas som egentligen inte ska inkluderas.

Skapa hashsträngar till dina frågesträngar

Ett bra sätt att säkerställa att ens frågesträngar inte blir manipulerade är att använda hashsträngar, som man skickar med som frågesträngar. Ett exempel, som förklaras sen:

index.php
---------
<?php
$page = 'forum.php';
$hash = md5($page . 'min hemliga nyckel');
echo "<a href=\"index.php?page=$page&hash=$hash\">länk</a>";
?>

index.php?page=forum.php&hash=4b889d7682ed63981dc10d31de4fea97
--------------------------------------------------------------
<?php
$page = $_GET['page'];
$hash = $_GET['hash'];
if(md5($page . 'min hemliga nyckel') == $hash)
{
	//frågesträngen är inte manipulerad
}
else
{
	//hackattack, frågesträngen är manipulerad
}
?>

Ifall besökaren ändrar i adressen, till något annat än "?page=forum.php", så kommer hashet i &hash= inte att stämma, eftersom det är skapat speciellt med strängen "forum.php". Anledningen till att man lägger in 'min hemliga nyckel' är för att besökaren själv inte ska kunna förstå att det är ett md5-hash, och själv lägga till rätt hash.

Register_globals

Register_globals är en sak som gradvis håller på att försvinna, sån tur är. När register_globals är påslaget så är:
$_GET['var'], $_POST['var'], $_SESSION['var'], $_SERVER['var'] och $_FILES['var'] allihopa samma sak som $var. Detta innebär förstås en stor säkerhetsrisk. Ett exempel:

index.php
---------
<?php
mysql_query("SELECT userid FROM users WHERE userid = $uid")
//I det här fallet tänker man sig att $uid ska vara $_SESSION['uid']
?>

Skriver man då: index.php?uid=1, så kommer SQL-frågan bli:

"SELECT userid FROM users WHERE userid = 1"

oavsett ifall någon session är skapad eller inte, eftersom $_GET['uid'] är samma sak som $uid. Du kan kontrollera ifall ditt webbhotell har register_globals avslaget eller påslaget med koden:

<?php
if(ini_get('register_globals'))
{
	//påslaget
}
else
{
	//avslaget
}
?>

Ligger du på en Apache-server, och har möjlighet att skriva en .htaccess fil, så lägg in detta för att stänga av register_globals:

php_flag register_globals 0

Det kommer dock inte fungera när servern uppgraderas till Apache 2. I Apache 2 måste man ange detta i VirtualHost i httpd.conf. Men eftersom du lär ha en egen virtual host på servern, så kan du snällt fråga ditt webbhotell ifall de kan lägga in raden till dig i httpd.conf.

Använd exit;

Säg att du har en kontroll för att se om en användare har rätt att se ett innehåll eller inte som ser ut så här:

<?php
if(!isset($_SESSION['user']) || !$_SESSION['user'] == 'admin')
{
	header('location: index.php?err=not_auth');
}
<html>
<body>
Hemligt innehåll!
</body>
</html>

Vad händer här ifall en hacker av värsta sort ser till att hans webbläsare ignorerar ifall din PHP-kod modifierar headers? Jo, då fortsätter den läsa resten av sidan, och visar upp "Hemligt innehåll!" även ifall denne inte har rätt att se det. Därför, sätt alltid exit; efter att du skickat en location-header för att vara säker på att ingen mer kod körs efteråt.

Lägg filer under www-roten

Många som programmerar ASP använder Microsoft Access som databas till mindre projekt. Lägger man då en Access-databas ovanför www-roten, så kan man enkelt ladda ner den, och få all data i den. Det kan se ut så här:

/domän.com/
/domän.com/public_html/
/domän.com/public_html/index.asp
/domän.com/public_html/db/
/domän.com/public_html/db/db.mdb
/domän.com/log/

Då är det lätt att skriva /db/db.mdb i adressfältet, och helt sonika ladda ner databasen. Lyckligtvis är det väldigt få PHP-programmerare som använder Access, men det finns ändå risker med att lägga saker ovanför www-roten. De allra flesta använder sig av include-filer. Antingen för gemensamma variabler, eller för något helt annat. Det kan verka smidigt att döpa en sådan fil till includefile.inc. Men alla servrar är inte konfigurerade för att låta PHP läsa .inc-filer, utan serverar dem som rena textfiler. Ligger då din fil så här:

/domän.com/public_html/includefile.inc

så kan man skriva /includefil.inc, och då serveras filen, med PHP-kod och allt, som helt vanlig text. Därför bör du alltid döpa dina include-filer till inc.includefile.php, för att vara säker på att de läses som php-filer. Du bör även givetvis lägga dem under www-roten, eftersom det inte är meningen att de ska kunna läsas av en besökare. T.ex:

/domän.com/public_html/
/domän.com/inc/includefile.inc

Felmeddelanden

Du bör aldrig visa felmeddelanden för besökaren. Du bör skriva dina program på ett sådant sätt att du fångar upp felmeddelanden, och skriver ut egenkomponerade felmeddelanden. Använder du redan PHP5, så ta en titt på try-catch-metoden. Det första du bör göra är att skapa en config-fil som inkluderas i alla sidor, och då menar jag alla sidor. I den sätter du:

<?php
error_reporting(0);
$debug_mode = false;
?>

error_reporting(0); gör att inga felmeddelanden visas för användaren. $debug_mode använder du i resten av applikationen för att se vilken sorts felmeddelande som ska skrivas ut. När det är dags att felsöka, så gå in och sätt $debud_mode till true, och sätt error_reporting till error_reporting(2048), eftersom det skriver ut alla felmeddelanden. Är $debug_mode satt till true, så skriver du ut riktiga felmeddelanden, t.ex. mysql_error(), medan om det är satt till false, så skriver du kanske ut "Något gick fel, var god kontakta webmastern ifall felet upprepar sig".

Summering

Summan av kardemumman är, använd ditt sunda förnuft, och var orimligt pessimistisk och paraniod. Förutsätt att alla världens hackers kommer att ge sig på dig. Förutsätt att allt som kan gå fel, kommer att gå fel, och jobba på att lösa det. Se även till att du alltid har de senaste uppdateringarna. Använder du PhpBB, så är det extra känsligt. Väldigt många letar efter säkerhetsluckor i det, så gå in och uppdatera så fort det kommer nya versioner. Hör dig även för i olika forum innan du väljer webbhotell. Ofta finns det kunder, eller före detta kunder, som kan berätta hur bra/dåligt det är.

Skriven av:
Anders Ekdahl, Ekdahl IT
Webbplats: Webbdesign


Se fler PHP guider



kommenteraKommentarer    Antal 0    Medelpoäng 0/10

Bli medlem för att kunna skriva kommentarer!
Logga in om du redan är medlem.


Copyright © 2005 webbdesign.info    Cookies    Gratis statistik till hemsida    sitemap Webbdesign sitemap icon
Använd gärna våra RSS feeds:

Artikel Feed
[XML]
Forum Feed
[XML]

Svenska webhosts:
Svenska Webhotell

Använd gärna denna länk för att länka till oss:

Ny översättning:
Swedish affiliate programs

Är du medlem och vill synas här?
Hör av dig i vårt forum