Защита от SQL Injection - начин на работа, уязвимости, защита
23-10-2012
Защита от SQL Injection (подробно, начин на работа, уязвимости, защита)

Пак ще се повторя като от предишния урок.
SQL Инжекциите са досадно нещо. Доста начинаещи PHP програмисти просто нямат нужните познания или дори на някой напреднал програмист, може да му се случи да пропусне да си прегледа кода и да остане уязвимост.
За какво се използва? - използва се за неоторизирано изпълнение на SQL заявка към базата данни.
Тази техника е за експлоатация на уязвимите кодове.

Най-лесно ще го схванете с нагледни примери. Имаме програмист , който е нает да направи система за онлайн пазаруване. Програмиста създава база данни (MySQL) и следната таблица в нея:

CREATE TABLE `users` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(200) NOT NULL,
`password` varchar(200) NOT NULL,
PRIMARY KEY (`id`)
);
INSERT INTO `users` (`id`, `username`, `password`) VALUES (1, 'admin', 'securitypass'), (2, 'Ticketa', '123456'), (3, 'Client', '12345');


Така. Трябва да отбележа, че има проблем с горния код, а той е: НИКОГА, ама НИКОГА не оставяйте паролите в ясен вид. По-принцип се криптират най-често с md5(), за по-голяма сигурност, можете да създадете и SALT към него.

Сега да си представим, че имаме една проста Логин система(която е уязвима..)

<?php
if(isSet($_GET['submit'])) {
$username = stripslashes($_GET['username']);
$password = stripslashes($_GET['password']);
$query = mysql_query("SELECT * FROM `users` WHERE `username` = '$username' AND `password` = '$password';") or die(mysql_error());
if(mysql_num_rows($query) > 0) {
$row = mysql_fetch_assoc($query);
echo "Здрасти {$row['username']} : {$row['password']}!<br />";";
}
} else {
echo "<form method='GET' action='login.php'>
Потребител: <input type='text' name='username' /><br />
Парола: <input type='text' name='password' /><br />
<input type='submit' name='submit' value='Login' />
</form>";
}
?>

Проблеми на формата:
- Използваме $_GET , вместо $_POST - по не сигурно е. Сега след като сме видели това просто в полето за URL адрес в своя браузър записва:
http://saita.com/login.php?username=test&password=test'%20OR%201='1&submit=Login
След, подобна така атакуващия (хахора) , ще види съобщение:
Здрасти admin : securitypass!


Сещате се на къде бия нали? Т'ва се получи заради кода, който добавих: ' or 1='1

Прави SQL заявката по следния начин:
SELECT * FROM `users` WHERE `username` = 'test' AND `password` = 'test' OR 1='1'


Виждаме, че след WHERE клаузата ще бъде изпълнен за всички редове. Тук логиката на кода ограничава изхода до първия ред от таблицата, но сами се сещате, че с добавяне на малко повече код може да се извлече цялата таблица с потребителите.
За да не се превръща урока в ръководство за хахори спирам до тук.

Най-лесния начин да се предпазите от SQL инжекции е да филтрирате всички входящи данни. Например , чрез функциите:
htmlspecialchars(); addslashes(); и trim();
Какво правят функциите?

htmlspecialchars() - преобразува специални знаци в html единици
addslashes() - екранира специалните знаци на даден стринг
trim() - премахва знаци в началото и края на даден стринг
htmlentities() - преобразува всички подходящи знаци в HTML единици и е .. сходна с htmlspecialchars();


Сега ще създадем една функция и ще обхождаме всяка $_GET, $_POST заявка както и бисквитките. (кода е най-добре, да седи най-отгоре на страниците).

function security($method){
if(intval($method)){
return $method;
} else {
$method = addslashes($method);
$method = htmlspecialchars($method, ENT_NOQUOTES);
return $method;
}
}
$_GET = array_map("security", $_GET);
$_POST = array_map("security", $_POST);
$_COOKIE = array_map("security", $_COOKIE);


Сега. Взимаме горния код за вход и го променяме малко:

<?php
function security($method){
if(intval($method)){
return $method;
} else {
$method = addslashes($method);
$method = htmlspecialchars($method, ENT_NOQUOTES);
return $method;
}
}
$_GET = array_map("security", $_GET);
$_POST = array_map("security", $_POST);
$_COOKIE = array_map("security", $_COOKIE);
if(isSet($_POST['submit'])) {
$username = $_POST['username'];
$password = md5($_POST['password']);
$query = mysql_query("SELECT * FROM `users` WHERE `username` = '{$username}' AND `password` = '{$password}';") or die(mysql_error());
$row = mysql_fetch_assoc($query);
if((mysql_num_rows($query) == 1) && ($password == $row['password'])) {
echo "Здрасти {$row['username']} : {$row['password']}!<br />";";
}
} else {
echo "<form method='POST' action='login.php'>
Потребител: <input type='text' name='username' /><br />
Парола: <input type='text' name='password' /><br />
<input type='submit' name='submit' value='Login' />
</form>";
}
?>


Сега, за да не ни пусне в чужд акаунт горния код, просто трябва да направим ограничение за уникалност на стойностите в колона - username.
Променили сме и метода на формата от GET на POST, дипче.. не е няк'ва ГОЛЯМА защита. Колкото да не се вижда какво въвеждаме в браузъра. И другото, което сме променили е начина за проверка към базата данни.
Именно вече използваме md5() за да "кодираме"/"хешираме" паролата.

Пак казвам. Това е прост пример, може да се направят и други защити, но.. може да направи атаката само малко по сложна. В такъв случай, въведете и една проверка, за "грешни пароли". Ако някой реши, може да напише един brute-force за сайта ви и да счупи случайно някой от паролите. А, ако имате скрипт, който проверява за грешни пароли тогава е друго. Какво искам да кажа?

При 3 грешни опита за вход с грешна парола в базата данни, заключваме акаунта за 15 минути. Ловя бас, защото повечето сте виждали подобно нещо.

Причини за бъгове в системата(уязвимости към SQLi). SQL Инжекцията, може да възникне в тези случай:
- Без филтриране на входящите данни
- Уязвимост в сървъра на базата данни

Методи за защита от SQL инжекция
- * Филтриране на данните (по-горе съм дал пример)
- Изключване на докладите за грешки.
- Създаване на потребител, с по-малко привилегии.
- Максимална стойност

1. Изключване на докладване за грешки
<?php
error_reporting(0);
?>
В резултат, хахора няма да може да разпознае дали има грешка и дали е уязвим сайта. Е.. Не всички, но пак е част от защитата.
По-този начин само ще усложним възможността на хахора да ни счупи сайта, може да успее да се добере до базата само с по-сложна атака.

2. Създавайте MYSQL потребителите с най-слабите привилегии
Да предположим, че имаме сайт за хм.. Онлайн магазин? Ок. Какво всъщност ни трябва? Потребителя вижда информацията (SELECT) и си избира, разни продукти.
Накрая прави проръчката (INSERT) , също така да речем, че имаме и колко прегледа има един продукт(UPDATE).

Имаме и админ панел - къде без него :)) Сега, правим си два акаунта(mysql). Единия има привилегии само за: SELECT, INSERT и UPDATE - това са привилегиите в магазина.
Правим си ОЩЕ един акаунт, който ще ни е за админ панела. Например SELECT, INSERT, UPDATE, DELETE. - Той може да трие, това което не го кефи, да сменя информация и т.н.

Всичко, друго е просто. 2та потребителя, ще използват една дб, но ще имаме два вида връзка към дб-то. Една за потребителя и една за админа. mysql_connect('localhost', 'adm', 'admpass'); и mysql_connect('localhost', 'user', 'userpass');

Повечето програмисти не го одобряват това, нооо не са и патили :))

3. Максимална стойност
Имаме форма за поръчка на даден продукт. В нея трябва да напишем нашето име и ще го ограничим до.. хм. 15 знака. HTML кода или по-точно полето за писане, можем да го ограничим с maxlength="15".
<input name="name" type="text" maxlength="15" />

Това няма , много, много да ни предпази, защото ако хахора си свали HTML формата или просто редактира формата, чрез някой браузър плъгин може да промени това полe. Която ще обработи информацията, през една и съща връзка. След това трябва да се дублира с това ограничение на сървъра:
Ще използваме функцията substr(); за да ограничим символите в полето.
<?php
$name = substr($_POST['name'], 0, 10);
?>


Щях, да забравя. Атрибутът ACTION на тагът FORM. Никога не задавайте пълния път, а относителния. По този начин формата винаги ще се събмитва към файл от вашия сървър.

<form method="POST" action="dir/test.php"> - правилно
<form method="POST" action="http://sata.com/dir/test.php"> - неправилно

Ако искате формата да се събмитва към текущия файл (т.е. този който я съдържа), използвайте това:

<form action="<?php echo $_SERVER['PHP_SELF']; ?>" method="POST">


но ще трябва и него да го защитите , защото е уязвимо към XSS атаки, по следния начин;

<form action="<?php htmlentities($_SERVER['PHP_SELF'], ENT_QUOTES); ?>" method="POST">


Лек ден, ако имате предложения драскайте ЛС. :)

П.С. Сега се, сещам че водихме една дискусия във форума и драснах за mod_security модула, може да видите мнението тук: http://web-tourist.net/forum/viewtopic.php?p=635752#635752

Добър модул е. Доволен съм от него, въпреки че може да се намери някоя друга вратичка.


П.П. АКО използвате $_GET заявка, която ще съдържа само цифри използвайте (int)$_GET['id']; например.




/ Трябва да сте регистриран за да напишете коментар /
От: proba
23:28 23-10-2012
Браво, харесва ми че някой прави такива уроци тъй като мен ме мързи :D
Но така пък и потребителите ще се наплашат , че само с едно писане на ' OR 1=1' им изкарва паролата, което не е точно така де.
И рядко се среща логин системи с GET. В днешно време почти 100% от sql инжекциите се правят или от страници с ?id ?page ?cat и т.н. затова и те трябва да бъдат добре защитавани.

И друго с което съм ИЗКЛЮЧИТЕЛНО НЕ СЪГЛАСЕН е това да задава action на формата която всъщност е равна на PHP_SELF.
Дори мога да ти кажа защо, и най-добре ще разбереш ако погледнеш старият урок на @deam0n като още на първият пример обяснява точно за това кото ти каза.. http://web-tourist.net/login/login/view.php?st=2599
От: proba
23:28 23-10-2012
Всъщност не бях видял че си написал в следващия цитат точно за тази защита, за което се извинявам.
Но все пак ако е в същият файл то може и без да се задава action пак да стане ;)
От: Ticketa
0:10 24-10-2012
@proba, естествено, че с ' OR 1=1' няма да му изкара паролата - къде съм казал, че му изкарва паролата?

Ако беше си направил труда да видиш изходния код когато потребителя се е "вписал", няма да го кажеш де. :)

А именно: echo "Здрасти {$row['username']} : {$row['password']}!";

Другото, което е. Да - Сял инжекциите се правят в страници като:
view.php?ID=1
index.php?PAGE=1
index.php?CAT=1

НО както казах по-горе, решението е просто пише го най-долу в "урока". Всъщност, по-горе съм написал, че не искам да давам по-големи примери за SQLi тъй като не искам да превръщам статията в хахорски наръчник!

Ти и аз, може да не използваме PHP_SELF , но има уеб програмисти, които го използват в уеб приложенията си. :)

Със здраве!
От: proba
0:17 24-10-2012
[quote]
echo "Здрасти {$row['username']} : {$row['password']}!";";
[/quote]

[quote]
След, подобна така атакуващия (хахора) , ще види съобщение:
Здрасти admin : securitypass!
[/quote]

Аз за тук говорех, главно че такава ситуация вече не се среща..

И, със здраве разбира се!
От: Ticketa
0:21 24-10-2012
Пак не ме разбра! :D

Виж , че нарочно вадя паролата. Нарочно не е и криптирана както по-горе съм казал - трябва да се криптират.

Можеше просто да напиша и: Добре дошъл, {$row['username']}!

Hello {$row['username']}!

Загрели? :) То няма , как да се визуализира паролата само като се е вписал и не е инжектиран кода. Надявам се да си ме разбрал ;-)

П.П. Можеше да бъде и така:
Здрасти {$row['username']} твоя баланс по сметката е {$row['amount']}.

Просто не исках да драскам по-дълъг код. Хах.
От: proba
0:41 24-10-2012
Да е аз те разбирам и ми е напълно ясно че никой не си изкарва паролата в сайта :D
От: kikity_94
15:18 24-10-2012
Урока кат цяло е много добър браво за това, но едно не разбрах. Защо подяволите в actiona на форма някой ще ползва PHP_SELF ??? о.О Ако оставиш полето празно е абсолютно същото и няма никакво главоболие
От: Ticketa
15:21 24-10-2012
$PHP_SELF се използва когато документа праща данни до себе си. Вместо да се пише адреса, може да се използва тази глобална променлива.. Всеки с капризите си @kiki :))

http://php.net/manual/en/reserved.variables.server.php
От: uphero
15:39 27-10-2012
и като цяло само ми загуби времето ,нищо съществено
От: Ticketa
15:50 27-10-2012
@uphero, като е нищо съществено били ми обяснил тогава всичко съществено. Стана ми интересно, може би знаеш доста неща :)
От: TryMe
16:49 08-11-2012
@ticket хората се мъчат да ти кажат че action="" е идентично с PHP_SELF. И тогава няма нужда да се минава през тея глупости.
И тази функция нещо не ме кефи :) security ()
От: Zion
11:29 12-01-2015
Искам да вмъкна, че за целта FORM-ата да праща към текущия файл, не е нужно да се преминава през функцията htmlentities() (както е тук
1