* Повишаване на сигурността при cookies за автентикация
Публикувано на 20 ноември 2008 в раздел ОСУП.
Нека вземем кода от предишната статия и се опитаме да го направим по-сигурен. На първата стъпка ще се възползваме от няколко допълнителни променливи, които можем да дадем като входни параметри при създаване на cookie, а именно:
- директория на сървъра, за която cookie е валидно
- домейн за който cookie е валидно
- задължително използване на SSL
- httponly (предотвратява изпращането му с javascript)
Промяната в кода на съществуващата програма е в два реда:
<?php function security_start_session($ssl, $timeout, $maxtime, $ip){ // ... security checks session_start(); session_regenerate_id(true); return 1; } if (security_start_session(0, 6*60, 20*60, 1) != 1){ echo "<br>session destroyed!"; return; } function showheaders(){ echo '<html><head><title>WorldBank.dom login page</title></head><body>'; } function showloginform(){ echo '<form action="cookie.php" method="POST">'; echo 'user:'; echo '<input type="text" name="user">'; echo '<br>pass:'; echo '<input type="password" name="pass">'; echo '<br>Set cookie: '; echo '<input type="checkbox" name="remember" value="true">'; echo '<br><input type="submit" name="submit"'; } function checklogin($user, $pass){ if(!is_string($user) || !is_string($pass)) return 0; if ($user == "test" && $pass == "123456") return 1; else return 0; } if(isset($_COOKIE['user']) && isset($_COOKIE['pass'])){ if (checklogin($_COOKIE['user'], $_COOKIE['pass']) == 1){ $_SESSION['authenticated'] = 1; } else{ setcookie("user","", mktime(12,0,0,1, 1, 1970)); setcookie("pass","", mktime(12,0,0,1, 1, 1970)); } } if($_SESSION['authenticated'] == 1){ showheaders(); echo "Welcome back. I remember your session..."; } else{ if( isset($_POST['submit'])){ if (checklogin($_POST['user'], $_POST['pass']) == 1){ $_SESSION['authenticated'] = 1; if($_POST["remember"]=="true"){ setcookie("user",$_POST['user'], time()+3600, "/secure/", "users.cphpvb.net", true, true); setcookie("pass",$_POST['pass'], time()+3600, "/secure/", "users.cphpvb.net", true, true); } echo "You are logged in successfully!"; } else{ showheaders(); echo "Invalid username and password"; } } else{ showheaders(); showloginform(); } } ?> </body></html>
В удебелените два реда сме добавили именно тези допълнителни параметри. Така създаденото cookie е валидно само за файлове намиращи се в директорията на адрес users.cphpvb.net/secure/. Освен това с последния параметър "true" сме указали, че cookie няма да бъде валидно ако канала не е криптиран с SSL (тоест връзката трябва задължително да мине през https).
При версии на PHP по-големи от 5.2.0 можете да добавите още един последен параметър със стойност true, който ще отговаря на полето "httponly". Когато той е true то cookie ще бъде фиксирано да бъде предавано само и единствено през http протокол. Това означава, че скриптове като JavaScript и VBScript няма да имат достъп до това cookie, което е значителен напредък в борбата срещу XSS атаки.
Ако отворите създаденото cookie в текстов редактор ще видите, че в него сме записали следната информация:
user test users.cphpvb.net/secure/ ... * pass 123456 users.cphpvb.net/secure/ ... *
Веднага ще забележите един сериозен проблем - всеки, който открадне това cookie ще види паролата като обикновен текст. Дали не можем да го криптираме?
Подобно нещо се реализира елементарно чрез сигурни алгоритми като например AES256 - в PHP за целта може да използвате хункциите openssl_encrypt и съответната ѝ openssl_decrypt. Идеята е да съхраните ключ за криптиране в кода на приложението, с който да криптирате името и паролата преди да ги изпратите към браузъра на клиента със setcookie, например:
DEFINE('CKEY', 'JKreWE32pO0032A3'); DEFINE('CIV', 'kgREop430IiireRE'); ... setcookie("user", openssl_encrypt($_POST['user'], "aes-256-cbc", CKEY, false, CIV), time()+3600, "/secure/", "users.cphpvb.net", true); setcookie("pass", openssl_encrypt($_POST['['pass'], "aes-256-cbc", CKEY, false, CIV), time()+3600, "/secure/", "users.cphpvb.net", true);
В случая константите CKEY и CIV са ключовете, с които криптираме съответното cookie (IV е "инициализиращ вектор", който трябва да се сменя при криптиране на по-големи количества информация, когато тя се разделя на блокове). Когато имаме нужда да прочетем и съответно декриптираме въпросните cookies, това се прави с "обратната" функция:
$user = openssl_decrypt($_COOKIE['user'], "aes-256-cbc", CKEY, false, CIV); $pass = openssl_decrypt($_COOKIE['pass'], "aes-256-cbc", CKEY, false, CIV);
Дотук обаче не постигнахме нищо кой знае колко съществено от гледна точка на "кражба на cookie". Реално ако някой открадне това файлче, той ще може да влезне в нашия акаунт с него. Именно тук адекватно могат да се намесят техниките за фиксиране чрез характеристики на браузъра на потребителя, които засегнахме в предишна статия тук: https://www.cphpvb.net/network-security/391-locking-login-cookies/
Ако спазвате схемата index.php -> login.php -> securearea, то вие може:
- Да прочетете характеристиките на браузъра на потребителя с javascript и да ги отпечатите в input поле в login формата;
- Вашият login.php скрипт ще може да прочете safe_browser_fingerprint променливата и да я използва като част от ключа за криптиране;
- При повторен login, който този път е направен с cookie, index.php страницата може да прочете променливите отново и да пренасочи браузъра чрез javascript с GET параметър към login.php.
В крайна сметка ако приложите тази техника, вашият CKEY може да изглежда по следния начин:
$safe_browser_fingerprint = $_REQUEST['safe_browser_fingerprint']; $safe_browser_fingerprint .= $_SERVER['HTTP_ACCEPT']; $safe_browser_fingerprint .= $_SERVER['HTTP_ACCEPT_LANGUAGE']; $safe_browser_fingerprint .= $_SERVER['HTTP_ACCEPT_ENCODING']; $safe_browser_fingerprint .= $_SERVER['HTTP_CONNECTION']); DEFINE("CKEY", substr(hash("sha256", $safe_browser_fingerprint."JKreWE32pO0032A3"),0,16));
В PHP $_REQUEST е еквивалент на "$_GET или $_POST".
Разбира се можем да добавим и IP адреса на потребителя към фиксирането. Както вече беше дискутирано обаче, това не е винаги подходящо за интернет приложения, защото има много потребители, които не са със статичен IP адрес. Евентуално за тези случаи бихме могли да създадем login форма подобна на следната:
Въпросният checkbox "lock it by IP" ще е тривиален:
<input type="checkbox" name="lockcookie" />
При записването на login cookie ще трябва допълнително да добавим още една променлива, с която да укажем, че то е заключено по IP:
if(isset($_POST['lockcookie'])&&$_POST['lockcookie']=="on"){ setcookie("locked_cookie", "1", time()+3600, "/secure/", "users.cphpvb.net", true); }
Така вече можем да предефинираме CKEY по следния начин:
DEFINE("CKEY", substr(hash("sha256", SAFE_BROWSER_FINGERPRINT."tr89frdrwePoweIr".((isset($_POST['lockcookie'])&&$_POST['lockcookie']=="on")?$_SERVER['REMOTE_ADDR']:(isset($_COOKIE['locked_cookie']) && $_COOKIE['locked_cookie']=="1")?$_SERVER['REMOTE_ADDR']:'')),0,16));
Накрая ще се спрем на още един изключително важен момент, а именно - подобни правила важат и за сесийните cookies!
- Ще е трудно да реализирате фиксирането по параметри с javascript, но спокойно може да фиксирате session cookie чрез някои от характеристиките в хедърите;
- Както browser cookie, така и session_cookie поддържа HTTPOnly флаг, както и Secure flag:
// Забранява изпращането с javascript ini_set('session.cookie_httponly', 1); // Забранява PHPSESSID в URL ini_set('session.use_only_cookies', 1); // Задължава използването на HTTPS ini_set('session.cookie_secure', 1);
Извиквайте тези команди преди session_start().
Допълнителна задача 1: Направете така, че IV да е различен за всеки потребител. Използвайте функцията openssl_random_pseudo_bytes за генериране на IV за всеки различен потребител и записвайте това IV в допълнително cookie.
Допълнителна задача 2: По начина, по който генерирахме CKEY - взимаме подниз от резултата на хеш функцията разгледана като шестнадесетично число - сме понижили значително ентропията на ключа. Защо? Предложете начин да се поправи.
Искам да попитам тези проверки за куки и сесии дали не трябв а да са преди html тага или просто трябва да се направят преди да се извежда информация в кода. Другото което е: например аз искам да направя бисквитките задължителни в сайта си, а не потребителя да избира. В такъв случай проверката на всяка страница пак трябва да е за куки или за сесия дали има или греша? Има ли значение също дали първо ще проверя за куки или за сесия? Според моята логика мисля, че първо трябва да проверя дали има сесия, ако няма тогава проверявам за куки- ако информацията в кукито съвпада с тази от БД то тогава потребителя има достъп, в противен случай разрушавам кукито и го препращам да се логне. Ако пък няма и куки следва препращане към логина. Правилно ли съм разбрала смисъла на всичко това?
Благодаря предварително!
Със сигурност трябва записването на cookie да е преди отварянето на какъвто и да е html таг и изпращането на какъвто и да е друг код. Кодът по-горе е категорично неработещ и го знам отдавна... но отдавна не съм го поправил :)
За другото - правилно си разбрала смисъла.
П.С. Редактирах кода. Надявам се сега да е добре.
Благодаря за бързия отговор и обяснението ти. Сега ще опитам да си направя и моя код и дано стане!
А в случай, че използвам задължително бисквитки в сайта, мога ли при проверката дали имам сесия и ако имам да извлека потребителското име от COOKIE['user'] например или тази променлива мога да я извлека само в случая с isset(COOKIE['user'])?
Другият ми въпрос е как да извлека от всеки потребител ключа му за SSL, за да мога да го използвам като проверка?
Функцията "isset" проверява дали променливата е дефинирана.
Колкото до втория въпрос - предполагам, че OpenSSL функциите ще помогнат.
Съжалявам за грешно зададения въпрос. Иимах предвид относно първото ми питане следното: Не да използвам isset(COOKIE['user']) за да извлека username-a от бисквитката а да използвам само COOKIE['user']. Но въпроса ми беше относно следния код:
if($_SESSION['log'] == "true")
{
echo "welcome COOKIE['user']";// мога ли така да извлека името или трябва да е със сесия задължително? В сайта използването на кукита е задължително.
}
elseif(isset($_COOKIE['user']))
{
правя си разни проверки и т.н.
}
else
{
трябва да не логне
}
Не, така не може:
1. Преди да се използва променлива трябва да си убеден, че е дефинирана (а тук не си и няма как да си);
2. Никога (!!!) не се отпечатват променливи подавани от потребител, преди да бъдат филтрирани.
Относно първия ти отговор: защо да не съм сигурна че е дефинирана, нали при логин задължително създавам куки с юзърнейма примерно. А и използването на сесии да изведа юзърнейма не е ли несигурно? ИСка ми се да е с бисквитка въпросното извеждане.
По втория отговор: Тук не съм написала кода изцяло, а само идеята си, но честно казано бях забравила да филтрирам стойностите от бисквитките. Това необходимо ли е дори ако не извеждам стойност от бисквитката а само я използвам за проверка?
Cookie се пази на клиентската машина, а не са сървъра - това означава, че приложението ти няма никакъв контрол над това дали и как клиента го съхранява. Следователно няма как да гарантираш, че той не е направил нещо "нередно" с него. Например ако има firewall той може да го изтрие. Или хакер може да го подмени или "прецака". Проверка трябва да се прави винаги.
По втория въпрос - ако нещо се отпечатва на екрана, то трябва да се отпечата филтрирано. Когато се използва в други ситуации - според ситуацията. Проверка на типовете данни, проверка за валидност - винаги са препоръчителни.
А има ли разлика дали ще използвам if($_SESSION['authenticated'] == 1) или if(isset($_SESSION['username']) && isset($_SESSION['userid'])) { проверка дали има такива стойности и т.н.} ?
Не е ли по- малко сигурно да се използва първия вариант тъй като стойността е константна?
bunchevi - тук съм дал просто най-прост пример, в който не се проверява кой е потребителя, а просто дали има такъв или не.