ugrás a tartalomhoz

Tűzfal készítése PHP segítségével

Baranyai László · 2010. Jan. 12. (K), 10.53
Tűzfal készítése PHP segítségével
A leggondosabb tervezés mellett is maradhat sérülékeny kódrészlet a webalkalmazásokban. Ezt kihasználva rosszindulatú látogatók támadó kódokat helyezhetnek el a honlapokon, vagy egyszerűen teleszemetelik. Mindkettő komolyan csökkenti egy honlap és annak szolgáltatásainak értékét. Másrészt a sérülékenységeket kereső letöltések is terhelik a szervert. A rövid időn belül sok egymást követő próbálkozás akár elérhetetlenné is teheti a kiszemelt honlapot. Itt már nem elegendő a beérkező adatok hagyományos ellenőrzése, érdemes megfontolni az aktív védekezés lehetőségét is.

Előnyök és hátrányok

Egy tűzfal alapvető funkciója, hogy meggátolja a kialakított házirend megszegését és megtagadja a szabályok ellen vétő felhasználók kiszolgálását. Alkalmazásának előnye, hogy csökken a rosszindulatú vagy spam bejegyzések elfogadása, valamint a sikeres támadások valószínűsége. A szerver terhelése is csökken azáltal, hogy a lapok feldolgozását már az elején megszakítjuk bizonyos esetekben. A tűzfal hátránya, hogy a normál felhasználók kiszolgálása is átesik az ellenőrzésen, ami viszont növeli a szerver terhelését. Egyes IP címek kitiltása pedig vétlen felhasználókat is érinthet.

A cikkben egy egyszerű PHP alapú tűzfal osztályt mutatok be az alkalmazásának lehetőségeivel.

Beavatkozás

Az osztály minimális váza a következő listában olvasható. Természetesen elvárható, hogy a tűzfal opcionálisan egy hiba oldalra irányítsa a látogatót, vagy megtagadja a kiszolgálást.
class SWFilter {

 // az átirányítás címe
 private $redirect_url;

 // konstruktor, az alapértelmezett értékek beállítása
 function __construct() {
  $this->redirect_url = '';
 }

 // ez a belső függvény avatkozik közbe
 function BlockUser($message) {
  if (empty($this->redirect_url)) {
   header('HTTP/1.0 403 Forbidden');
   echo $message . "\n";
  } else {
   header('Location: '. $this->redirect_url);
  }
  exit;
 }

 // átirányítási cím beállítása
 public function SetRedirectURL($new_value) {
  $this->redirect_url = $new_value;
 }

}
A BlockUser() függvény közvetlenül nem használható, az osztály ellenőrző rutinjai hívják meg. Futtatása megszakítja az oldal további feldolgozását. Alapértelmezetten hibaüzenettel itt megáll a program, de a SetRedirectURL() segítségével beállítható egy gyűjtőlap címe.

URL ellenőrzés

Az alábbi CheckURL() függvény már publikus, és a paraméterként átadott tömb elemeit felhasználva ellenőrzi azok jelenlétét a címsorban. A $found==false feltételnek köszönhetően a függvény az első megtalált elemnél leáll a kereséssel. Ezzel kiküszöbölhető a további elemek felesleges feldolgozása és időt takarítunk meg.
 // címsor ellenőrzése a megadott kifejezésekre
 public function CheckURL($terms) {
  $url = isset($_SERVER['PATH_INFO']) ? substr($_SERVER['PATH_INFO'],1) : '';
  if (!isset($url) || strlen($url)<2) {
   $url = isset($_SERVER['QUERY_STRING']) ? $_SERVER['QUERY_STRING'] : '';
  }
  $found = false;
  for ($i=0; $i<count($terms) && $found==false; $i++) {
   if (stripos($url,$terms[$i])!==false) $found=true;
  }
  if ($found==true) {
   $this->BlockUser('Illegal expression found.');
  }
 }
Most, hogy már van publikus függvénye, lássuk a használatát is:
<?php
include 'swfilter_class.php';
$myFireWall = new SWFilter();
$myFireWall->CheckURL(array('tp:/','tps:/'));
?>
A fenti példában megadott array('tp:/','tps:/') paraméter segítségével a címsorban előforduló http://, https://, ftp:// és ftps:// kezdetű linkeket tiltottuk le. Ezzel a módszerrel megakadályozható, hogy távoli fájlok és kódok megnyitását erőszakolják ki a programunkból. Természetesen bármely más közismert trükk mintája is megadható, akár a saját naplófájlok elemzése alapján is alkothatunk szabályt.

GET és POST adatok ellenőrzése

A $_GET és $_POST globálisokon keresztül érkező adatok esete picit bonyolultabb. A mostanában divatos *script alapú támadások esetén például nem engedhető meg a támadó kód elfogadása, másrészt spam üzenetek esetében a kulcsszavak előfordulási gyakoriságát érdemes kiszámítani. Mindkét elvárásnak megfelelő kódot írhatunk, ha a határérték szabályozható. Az alapértelmezett érték beállítását ne felejtsük el hozzáadni a konstruktorhoz!
 // határérték SPAM elemzéshez, db/KByte
 private $limit_spam;

 // konstruktor, az alapértelmezett értékek beállítása
 function __construct() {
  $this->limit_spam = 2.0;
  $this->redirect_url = '';
 }

 // határérték módosítása
 public function SetLimitForSPAM($new_value) {
  if (is_numeric($new_value)) $this->limit_spam = $new_value;
 }

 // adatok ellenőrzése GET-POST sorrendben (gyakoriság számítása)
 public function CheckInputData($key, $term) {
  $val = isset($_GET[ $key ]) ? $_GET[ $key ] : '';
  $val = isset($_POST[ $key ]) ? $_POST[ $key ] : $val;
  if (empty($val)) return;
  $found  = 0;
  $offset = 0;
  do {
   $pos = stripos($val,$term,$offset);
   if ($pos!==false) {
    $found++;
    $offset += $pos+1;
   }
  } while($pos!==false && $offset<strlen($val));
  $spam_score = 1024*$found/strlen($val);
  if ($spam_score > $this->limit_spam) {
   $this->BlockUser('SPAM detected, score='. round($spam_score,2) );
  }
 }
Az elemzést a CheckInputData() publikus függvény végzi. A függvény két paramétere a változó neve és a keresett kulcsszó. A számított gyakoriság határértékét saját tapasztalat alapján 2.0-ra állítottam be. Ez nekem bevált a címeket tartalmazó spam üznetekkel szemben. A rövid és csak 1-2 címet tartalmazó spam üzenetek tipikusan 50 db/K feletti sűrűségűek. Jelenleg a függvény találat esetén kiírja üzenetben a sűrűséget, ami fejlesztés során hasznos lehet, de nem javasolt ilyen információ megosztása a spam írókkal.

Használatára példa:
<?php
include 'swfilter_class.php';
$myFireWall = new SWFilter();
$myFireWall->CheckURL(array('tp:/','tps:/'));
$myFireWall->CheckInputData('formtext','http:');
?>
A határérték nullára csökkentésével a tűzfalunk semmilyen előfordulást nem tolerál. Így ezzel a megoldással a *script alapú kártevők jellemző kódjai ellen is felléphetünk.

Büntetés: fekete lista

A büntető karantén az üzemeltető vérmérséklete szerint automatikusan is alkalmazható. Az automatizált támadások (script-kiddie) rövid időn belül nagy számú variációt próbálnak ki. Hasznos lehet egy feketelista készítése, amivel bizonyos címeket tartósan vagy átmenetileg kizárhatunk a szolgáltatásból. Ehhez egy adatbázis is szükséges, pl. SQLite segítségével:
sqlite> CREATE TABLE badguy (addr TEXT UNIQUE KEY, day TEXT KEY);
sqlite> .quit
A fenti táblában az IP címet és az esemény dátumát rögzíti a program. A UNIQUE attribútum biztosítja, hogy felesleges másodpéldányok nélkül töltsük fel a táblát. Az adatbázis műveleteket egyetlen függvény is elvégezheti:
 // adatbázis műveletek: ellenőrzés, rögzítés, karbantartás
 function ManageDB($action) {
  switch ($action) {
   case   'check':
    $sqlstr = 'SELECT * FROM badguy WHERE addr="' $_SERVER['REMOTE_ADDR'] .'";';
    break;
   case     'add':
    $sqlstr = 'INSERT INTO badguy VALUES("'. $_SERVER['REMOTE_ADDR'] .'","' . date('Ymd') .'");';
    break;
   case 'refresh':
    $sqlstr = 'DELETE FROM badguy WHERE day<'. date('Ymd', strtotime('-2 week')) .';';
    break;
  }
  if (!$this->db) {
   $this->db = new PDO('sqlite:spamdb.dat');
  }
  if ($this->db) {
   $result = $this->db->query($sqlstr);
   if ($action=='check') {
    $row = $result->fetch(PDO::FETCH_ASSOC);
    if ($row['addr']==$_SERVER['REMOTE_ADDR']) return true;
   }
  }
 }
Az adatbázist a PDO osztályon keresztül érjük el. A függvény paramétere szerint összeállított SQL utasítás A: ellenőrzi a látogató IP címét, B: rögzíti az új IP címet vagy C: törli a 2 hétnél régebbi bejegyzéseket. Ez utóbbi határidő ízlés szerint módosítható. Ha az automatizált támadásokat akarjuk csak kivédeni, akkor pár óra is elegendő. Ha azonban van pár zombi PC és onnan próbálkozik egy program, akkor érdemes több időt hagyni. Egy zombi gép felderítése és semlegesítése több napot is igénybe vehet. Végezetül a tábla karbantartás műveletét (refresh) pedig időzített módon (pl. cron) illik lefuttatni. A fenti belső függvény két helyen biztosan szerepelhet, a cím rögzítését tehetjük a BlockUser() függvénybe, míg az ellenőrzésre újat kell készíteni.
 // ez a belső függvény avatkozik közbe
 function BlockUser($message,$addtodb) {
  if ($addtodb) $this->ManageDB('add');
  if (empty($this->redirect_url)) {
   header('HTTP/1.0 403 Forbidden');
   echo $message . "\n";
  } else {
   header('Location: '. $this->redirect_url);
  }
  exit;
 }

 // feketelista ellenőrzése
 public function TestBlacklist() {
  if ($this->ManageDB('check')) {
   $this->BlockUser('Blacklisted.',false);
  }
 }
Vegyük észre, hogy immár két paramétere lett a BlockUser() függvénynek. A második eldönti, hogy rögzítsen-e IP címet. Amikor feketelistán szerepel a cím, felesleges megpróbálni az ismételt rögzítését. A többi esetben pedig egészítsük ki a függvényhívást egy true második paraméterrel. Az adatbázissal kiegészített tűzfal rendszer felhasználására egy példa:
<?php
include 'swfilter_class.php';
$myFireWall = new SWFilter();
$myFireWall->TestBlacklist();
$myFireWall->CheckURL(array('tp:/','tps:/'));
$myFireWall->CheckInputData('formtext','http');
?>
A fenti példa egy Celeron 1,4 GHz processzoros gépen (laptop) átlagosan 1,8 ms alatt lefutott. Ez alapján az elkészített tűzfal osztály várhatóan nem okoz letöltésben is érezhető késedelmet. A szerverek nagyobb sebességű és korszerűbb processzorai még gyorsabban dolgozzák fel ezt a kis méretű kódot. Végezetül a kialakított osztály kódja – megjegyzések nélkül – az alábbi lett:
<?php

class SWFilter {

 private $limit_spam;
 private $redirect_url;
 private $db;

 function __construct() {
  $this->limit_spam = 2.0;
  $this->redirect_url = '';
 }

 function BlockUser($message, $addtodb) {
  if ($addtodb) $this->ManageDB('add');
  if (empty($this->redirect_url)) {
   header('HTTP/1.0 403 Forbidden');
   echo $message . "\n";
  } else {
   header('Location: '. $this->redirect_url);
  }
  exit;
 }

 // SQLite spamdb : 'CREATE TABLE badguy (addr TEXT UNIQUE KEY, day TEXT KEY);'
 function ManageDB($action) {
  switch ($action) {
   case   'check':
	$sqlstr = 'SELECT * FROM badguy WHERE addr="'. $_SERVER['REMOTE_ADDR'] .'";';
	break;
   case     'add':
	$sqlstr = 'INSERT INTO badguy VALUES("'. $_SERVER['REMOTE_ADDR'] .'","' . date('Ymd') .'");';
	break;
   case 'refresh':
	$sqlstr = 'DELETE FROM badguy WHERE day<'. date('Ymd', strtotime('-2 week')) .';';
	break;
  }
  if (!$this->db) {
   $this->db = new PDO('sqlite:spamdb.dat');
  }
  if ($this->db) {
   $result = $this->db->query($sqlstr);
   if ($action=='check') {
    $row = $result->fetch(PDO::FETCH_ASSOC);
    if ($row['addr']==$_SERVER['REMOTE_ADDR']) return true;
   }
  }
 }

 public function SetRedirectURL($new_value) {
  $this->redirect_url = $new_value;
 }

 public function CheckURL($terms) {
  $url = isset($_SERVER['PATH_INFO']) ? substr($_SERVER['PATH_INFO'],1) : '';
  if (!isset($url) || strlen($url)<2) {
   $url = isset($_SERVER['QUERY_STRING']) ? $_SERVER['QUERY_STRING'] : '';
  }
  $found = false;
  for ($i=0; $i<count($terms) && $found==false; $i++) {
   if (stripos($url,$terms[$i])!==false) $found=true;
  }
  if ($found==true) {
   $this->BlockUser('Illegal expression found.',true);
  }
 }

 public function SetLimitForSPAM($new_value) {
  if (is_numeric($new_value)) $this->limit_spam = $new_value;
 }

 public function CheckInputData($key, $term) {
  $val = isset($_GET[ $key ]) ? $_GET[ $key ] : '';
  $val = isset($_POST[ $key ]) ? $_POST[ $key ] : $val;
  if (empty($val)) return;
  $found  = 0;
  $offset = 0;
  do {
   $pos = stripos($val,$term,$offset);
   if ($pos!==false) {
    $found++;
    $offset += $pos+1;
   }
  } while($pos!==false && $offset<strlen($val));
  $spam_score = 1024*$found/strlen($val);
  if ($spam_score > $this->limit_spam) {
   $this->BlockUser('SPAM detected, score='. round($spam_score,2) ,true);
  }
 }

 public function TestBlacklist() {
  if ($this->ManageDB('check')) {
   $this->BlockUser('Blacklisted.',false);
  }
 }

}

?>

Összefoglalás

A bemutatott PHP osztály egyszerű tűzfal funkciókra képes, mint pl. címsor ellenőrzése adott kifejezésekre, kulcsszavak szűrése a beérkező adatokon, IP cím alapján történő kitiltás (spamdb) és türelmi idő után a tiltás automatizált feloldása. Olyan oldalakon javasolható a használata, ahol jelentős számú betörési kísérlet vagy spam szemetelés zajlik rövid időn belül, és ezek kiszűrése komolyabb erőforrásokat szabadítana fel. Szintén javasolható olyan oldalakon, ahol a tartalom minősége és megbízhatósága kiemelkedően fontos. A tűzfal szabályokat célszerű saját tapasztalatok és napló elemzések alapján összeállítani.

Fontos megemlíteni, hogy már léteznek elérhető árú tűzfal modulok a népszerű alkalmazásokhoz (pl. WordPress, Drupal). A fenti osztály kódját elsősorban gondolatébresztőnek szántam.

(A cikk ikonjához Ruzzilla New York City Firewall c. fotóját kölcsönöztük.)
 
Baranyai László arcképe
Baranyai László
A Budapesti Corvinus Egyetem docense, munkája egyben a hobbija is (szeret oktatni és programozni). Több tudományos honlap tervezője, készítője. Szeret nassolni és saját bevallása szerint jó palacsintát süt.
1

tetszett

gphilip · 2010. Jan. 12. (K), 12.12
Köszönjük szépen a cikket, valóban jó gondolatébresztő. A kódban nem mindennel értek teljesen egyet, de a téma fontos, és sok kezdőnek jelenthet kiindulási alapot.
3

Köszönöm

Baranyai László · 2010. Jan. 12. (K), 15.38
Köszönöm. A kóddal kapcsolatban igazad van, valóban csak egy csontvázat hagytam, sok helyen kiegészíthető és fejleszthető. Te melyik részére gondoltál?
6

Nincs mit

gphilip · 2010. Jan. 12. (K), 21.08
Hát nem akarok szőrszálhasogatni... Főleg ne egy egyetemi oktatóval szemben. :) nem ez a lényeg.
2

Gratulálok

inf · 2010. Jan. 12. (K), 12.18
Szia, gratulálok a cikkhez!


Tetszik ez a fajta megközelítés, azt hiszem még kiegészíteném web2-es dolgokkal. Spam-eknél feldolgoznám a spam-et, és fekete listára tenném a benne szereplő url-eket is. Az átcsúszott spameket meg fel lehetne deríttetni a felhasználókkal, pl 2 spam-nek minősítés után ellenőrizné a levelet a rendszer, hogy van e benne url, aztán spam-nek minősítené vagy tovább küldené az adminoknak.

IP címnél azt mondják, hogy nem szerencsés a ban, bár ezt döntse el mindenki saját maga, biztos, hogy van olyan eset, amikor szükséges.
4

SPAM

Baranyai László · 2010. Jan. 12. (K), 15.46
Hát, bevallom, ez csak inkább amolyan kedvcsináló kód. Hatékony SPAM szűréshez a Defensio vagy az Akismet API-kat javasolnám. De nem látom akadályát, hogy azokat egy saját tűzfalba integráljuk.
5

Kiváló

janoszen · 2010. Jan. 12. (K), 16.02
Kiváló cikk. Sajnos már nekem is kellett írni hasonlót. Gyorsan hozzátenném, hogy azért ez kizárólag a PHP alkalmazásbeli gyengeségek kiküszöbölésére jó, a rendszergazdai munkát nem helyettesíti.
7

hasznos, de nem mindig életszerű

virág · 2010. Jan. 13. (Sze), 16.19
Szia,

ezek a megoldások szerintem nem életszerűek, mert túl nagy terhelést róhatnak a szerverre, a helytelen inputokat máshol és más formában kell szerintem szűrni (validátorokkal, nem okvetlenül PHP szinten stb.), én legalábbis nagy terhelésű portáloknál nem alaklmaznék ehhez hasonló elgondolásra épülő megoldást.

Tudom, hogy ezek a kódok csak egy szemléltető példa gyanánt szolgálnak, de nekem maga a megközelítés sem tetszik túlzottan. A szűrő metodikák (POST, GET filterezése, URL cleaning stb.) természetesen nagyszerűek, de egy rendszerben nagyon jól meg kell választani azokat a pontokat ahol a szerver felé érkező input adatokat szűrjük stb.

Amúgy jó cikk, kiindulási alapnak megfelelő lehet és már az is pozitívum, hogy ha egy fejlesztő ügyel ezekre... :)
8

részben

Baranyai László · 2010. Jan. 13. (Sze), 16.42
Szia! Nos igen, ez szerintem részben igaz. Természetesen egy jól megtervezett programnál erre külön nincs szükség. A gyakorlatban mégis előfordult már velem, hogy a használt open source motort megtörték és a frissítés kiadásáig is tenni kellett valamit. A kézzel gyomlálás napi pár százas nagyságrendig még talán mehet, de felette biztosan nem.
Szerintem egy ilyen elő szűrőnek nem is kell input adatokat validálni, mert nem ez a feladata. Arra gondoltam, hogy a nyilvánvalóan kártékony kísérleteknek nem is kellene eljutni az űrlap adatok ellenőrzéséig.
9

Nyilván

inf · 2010. Jan. 13. (Sze), 21.43
Arra gondoltam, hogy a nyilvánvalóan kártékony kísérleteknek nem is kellene eljutni az űrlap adatok ellenőrzéséig.
Nyilvánvalóan az a logikus, ha nem végeztetünk felesleges munkát a szerverrel. A post/get validálási helyét meg sok esetben nem is olyan egyszerű meghatározni. Pl én már hallottam olyat, hogy script tageknél a kiírásnál raknak rá egy filtert, de csak akkor, ha a kimenet html stbstb...
10

kimenet szűrése

Baranyai László · 2010. Jan. 14. (Cs), 09.21
Ilyenkor a strip_tags() függvény is megteszi:

<?php
 echo strip_tags('<script type="text/javascript">alert("Nem sikerült. :(")</script>');
?>
11

Yepp

inf · 2010. Jan. 14. (Cs), 11.17
Yepp, főleg hogy meg lehet adni külön listán, hogy mik engedélyezettek...
Mondjuk én úgy gondolom, hogy a bejövő templatet (bbcode, etc..) a kimenő szűrők alakítsák át az oldal lekérésénél, mert így könnyen lehet módosítani a kimenő szűrőket, tehát a kinézetet. Pl ha a félkövér szövegnek piros színt akarok, akkor csak egy szűrőt kell átírnom, nem pedig az összes addigi commentet. Szóval sokkal dinamikusabb az oldal, ha bizonyos dolgokat a kimeneten szűrünk. (Cachelni meg nyilván kell ilyenkor.)
12

CSS

prom3theus · 2010. Jan. 15. (P), 13.25
Ha a BBCode a tároláskor átalakítja a tagjeit HTML tagekké, nem egyszerűbb az azokhoz a tagekhez tartozó CSS-t átírni? Mondjuk
#commentBlock strong {color: #F00;}
?
13

Hát

inf · 2010. Jan. 15. (P), 16.09
Hát alap dolgoknál egyszerűbb, én speciel html/json szoktam küldeni adatot, és azon belül is vannak animációk stb.. bbcode-ot csak példaként hoztam fel, mert arról mindenki tudja, hogy micsoda.
14

mod_security

Heilig Szabolcs · 2010. Jan. 18. (H), 14.08
Ilyen feladatokat (részben) az Apache mod_security modulja is el tud látni. Gyakorlatilag egy szabály-alapú webszerver tűzfalnak tekinthető, és rengeteg spambotot képes megfogni már csak egy olyan szabállyal, hogy UserAgent nélkül szóba sem áll a túloldallal. Előfordul, hogy kicsit reszelni kell a beállításokon (mostanság jellemzően AJAX kérések esetén fordul elő), de rengeteg mindent képes elcsípni: directory traversal, SQLi. Lekapcsolni nagyon kevés esetben kell, myadmin esetén pl ugye by design SQL injection-öket hajtunk végre folyamatosan, tervezetten. :)
15

hoppá

Baranyai László · 2010. Jan. 24. (V), 22.14
most, hogy utána olvastam a dokumentációban, pont ilyesmire gondoltam. Az AJAX szűrésére is van javaslat:
http://www.modsecurity.org/documentation/Ajax_Fingerprinting_and_Filtering_with_ModSecurity_2.0.pdf

Ha van tapasztalatod vele, nem írnál róla esetleg?
16

Háááát

janoszen · 2010. Feb. 7. (V), 11.28
Hát azért óvatosan. Saját szerveren lehet ilyesmiben gondolkodni, de hostolt környezetben valszeg több a szívás vele szolgáltatói oldalról, mint amennyi előnye van. Egyébként a User-Agent mezőt semmi nem írja elő, egy céges proxy is szűrheti, ezzel leginkább saját magadat lövöd lábon, de hasznod nincs belőle mert aki meg akar zúzni, az fog küldeni ilyen headert is.
18

???

inf · 2010. Már. 5. (P), 12.42
"myadmin esetén pl ugye by design SQL injection-öket hajtunk végre folyamatosan, tervezetten"
El nem tudom képzelni, hogy ilyet mi célból kellhet???
17

A cikkben sokat markoltál,

vbence · 2010. Feb. 8. (H), 13.15
A cikkben sokat markoltál, szerintem egy kisebb (rész)téma alposabb kidolgozása szerencsésebb lett volna. URL-ek kitiltása a querystringből, vagy spam azonosítása az url/szöveg aránnyal olyan módszerek amit csak legvégső esetben, a rendszer összeomlása ellen használnék.

Ha egy 3rd party portálmotort használsz, akkor sem kell megvárni a következő verziót. Azokra a résekre, amikre botokat írnak, már van pár soros, könnyen alkalmazható fix (patch) is. Vagy egyszerűen a regisztrációs formba beszárhatsz egy plusz mezőt "ide írd be hogy cseber", és máris lepattannak a botok az oldaladról.

A query string ellenőrzésre itt egy ötlet: a bementi filterrel nem tiltólistára teszed a próbálkozót, hanem csak összekavarod az URL-t. Pl: http:// -ből INGYOMBINGYOMhttp:// -t csinálsz (ezzel meg tudod védeni a remote include-tól a leglyukasabb motort is), majd egy output buffering függvényben visszaforgatod az URL-eket.