download.bg
 Вход Списание  Новини  Програми  Статии  Форум  Чат   Абонамент  Топ95   Архив 

PHP проста защита на кода и логинг страницата

Автор
Съобщение
phrozencrew
Нед, 19.08.07, 22:13
( PHP simple security codding )
Здравейте,
В този урок ще се опитам да дам леки насоки за размисъл относно PHP сигурността. В този аспект могат да се кажат много неща, но аз ще бъда лаконичен и максимално четлив. Най-важните неща, които съм се старал да спазвам са:
1. лесна четимост на кода
2. простота
3. лесна употреба
4. максимална защита с минимални усилия
В кода ще използвам някои функции на php, които няма нужда да познавате, защото съм ги представил в натурален вид, готов за употреба, без да обръщам внимание на подробностите. Целта ми е с един поглед да става ясно какво върши всеки ред. Например: strcmp(), md5(), setcookie(), $var=<<<...
Този код съм го писал за да защитава админ панела на мой реален сайт. Не съм имал проблеми досега с него.
Нека започнем кодирането с създаването на добра парола. В случая аз съм използвал md5 сумирането за да защитя паролата и логина си. Конкретно за този урок юзера е admin, а паролата demo. Прекалено лесни за кракване, нали! Но не това е страшното. Страшното е когато не внимавате и напише код който дава достъп до php кода ви, дори повикването на една картинка може да покаже сорса на даден файл. И така с лек скрипт за криптиране аз извличам криптираните стойности на логина и паролата:
<?php
echo md5("admin");
echo md5("demo");
?>

Разбира се този файл не бива да присъства по никакъв начин на сървара ви. Достатъчно е да го стартирате веднъж под конзола или CommnadPromt за да извлечете стойностите. От там нататък ще трябва да помните паролата и юзернейма си за логването. (този файл може да се намери в приложения архив на края на урока под името config.php)
След като вече сме извлекли криптирани параметри за логване можем да ги вкараме директно в php файла за логване.
Нека видим сорса на целия файл:
<?php
$user="21232f297a57a5a743894a0e4a801fc3"; // криптиран юзернаме
$pass="fe01ce2a7fbac8fafaed7c982a04e229"; // криптираната парола
if ($_COOKIE["Cookie"]){
echo "Имате забрана за логване в сайта за 2 мин. Моля бъдете търпеливи и опитайте отново!";
exit;
}
 
$errMsg = '';
if (isset($_POST['num'])){
	$counterror = $_POST['num'] + 1;
	if ($counterror>=4){
		setcookie("Cookie", "Забрана за 2 мин.", time()+120);
		echo "Имате забрана за логване в сайта за 2 мин.";
		$counterror=0;
		exit;
	}
}
 
if (isset($_POST['txtUserid'])) {
    if (!strcmp(md5($_POST['txtUserid']),$user) && !strcmp(md5($_POST['txtUserpw']),$pass)) {
        setcookie("Loging", "True", time()+3600);
		header('Location: index.php');
		exit;
    } else {
		$errMsg = "Грешен <b>Потребител</b> или <b>Парола</b>. До сега сте пробвали <b>" . $counterror . "</b> пъти да влезете в системата. При въвеждане на 4 грешен пореден път ще бъдете принудени да изчакате 2 мин. преди да опитате отново.";
	}		
} 
 
 
 
 
$html1 = <<< ___HTML___
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
<title>Login</title>
<meta http-equiv="Content-Type" content="text/html; charset=windows-1251">
<link rel="stylesheet" href="style.css" type="text/css"></style>      
</head>
<body>
    <div class="outer">
		<div class="header">
		</div>
		
		<div class="body">
			<div class="log">
				<div class="loginer">
				Login
				</div><!-- затваряме тага class="loginer" -->
				<div class="textlog">
				Вие все още не сте се идентифицирали в системата. <br />Моля въведете своето потребителско име и парола за достъп:
				<div class="form">
					<form form action="loging.php" method="post" name="frmCampaign" id="frmCampaign">
					Потребител:<br /><input type="text" id="txtUserid" name="txtUserid" size='14'><br />
					Парола:<br /><input type="text" name="txtUserpw" id="txtUserpw" size='14'>
					<input type="hidden" name="num" value=$counterror />
					<button type="Submit" name="Submit" value="Submit" style="font-weight: normal; margin-left: 10px; padding: 0px 10px 0px 10px;" id="Submit">Вход в системата!</button>
					</form>
				</div><!-- затваряме тага class="form" -->	
					
				</div><!-- затваряме тага class="textlog" -->
 
			</div><!-- затваряме тага class="log" -->
		</div>
 
		<div class="footer">
		</div>	
		<div class="mess">$errMsg $bbb</div>
    </div>
</body>
</html>
___HTML___;
echo $html1;
?>

Мисля, че няма нужда от много коментари, но все пак нека да видим какво се случва.
if ($_COOKIE["Cookie"]) - С това проверяваме дали има сложена бисквитка за предозиране на опитите за логване. С това неутрализираме brutalforce атаките, разбира се ако атакуващия не трие бисквитките след всеки опит. Аз поне така бих направил, но мога да ви кажа, че доста кракери не го правят. Нека видим и редовете, които вкарват тази бисквитка:
if ($counterror>=4){
	setcookie("Cookie", "Забрана за 2 мин.", time()+120);

Ясно е предполагам. Ако има повече от 3 опита за влизане с грешни данни ще бъде вкарана бисквитка, която ще контролира забавянето.
По същия начин действа и реда по-долу:
setcookie("Loging", "True", time()+3600);

С тази бисквитка казваме на браузера да пази логина 1 час(3600 секунди), след което ще се наложи ново логване.
Как сравняваме истинската парола с подадената от потребителя? С функцията strcmp(). Тази функция връща "0" ако двата стринга съвпадат. Ето и примера:
strcmp(md5($_POST['txtUserid']),$user)

Тук виждаме още една функция - md5(). Това е функцията която ще кодира подадените данни и ако те съвпадат с кодираните данни в началото на файла тогава ще имаме достъп до следващата страница, а това ще ни го осигури бисквитката "Loging".
Какво ли следва. След като веднъж сме се логнали с правилните данни:
Потребител: admin
Парола: demo
Следва да видим как страницата на която биваме прехвърлени от реда
header('Location: index.php');
  след коректен логинг ще провери дали наистина сме се логнали. Файла index.php ни проверява като гледа за:
if (!$_COOKIE["Loging"]=="True"){
header('Location: loging.php');
exit;
}

Т.е. ако имаме различна бисквитка от Loging="True", то тогава ще ни върне в страницата за логин.
Разбира се, това задаване на бисквитки е доста опростено и едва ли може да се нарече сигурно. Можете да използвате и задаване на домейн и директория, но за сега и това ще ни свърши работа.
Относно графичния дизайн и JScript-a надявам се да нямате забележки. Постарал съм се цялото кодиране да е по стандартите, без таблици и както си трябва - ясно и просто. Постарал съм се и да отделя PHP кодирането от XHTML кодирането, затова и използвах перфектността на Perl от която PHP толкова се възползва:
$html1 = <<< ___HTML___

Надявам се урока да ви е полезен! Отворен съм за критика и ще се радвам да коментираме каквото и да било по кода.

Демо може да се види тук:
http://www.nediko.info/phpsecurity/loging.php
Сорса на файловете може да се изтегли от тук:
http://www.nediko.info/phpsecurity/PHP-simple_security_by_PhrozenCrew.zip

Урока е написан лично от мен и материалите, които съм използвал са основно от референции за PHP.
Недялко Войняговски [PhrozenCrew] 19.08.2007

Та сега идва въпроса как да прехвърля проверките вместо да са с бисквитки да са със сесии: $_SESSION. Ако някой разбира от PHP нека пише в тази тема. Ще ми е интересна всяка критика или коментар по темата.
Благодаря!

insecteater
Пон, 20.08.07, 10:00
Със сесии не се печели кой знае колко. Идентификатора и името на сесията обикновено се съхранява в бисквитка. Другият начин за предаване е чрез добавяне на двойката идентификатор=име към utl адреса. И в двата случая потребителят винаги може да си извика нова сесия (било то и автоматизирано).

За да работи едно такова игнориране, би трябвало данните за ignor-а да се пазят на сървъра, свързани с потребителското име - а там вече модерният начин е с база данни. На много места даже и сесиите вместо във временни файлове също се съхраняват в база данни но това вече са по-сериозни истории. За нашият случай е достатъчно да прикачим по някакъв начин времето за ignore към ника - например чрез запис на данните във файла в който са данните за login. Може и в отделен ткестов файл-таблица, в който да се поглежда дали даден портебител е блокиран и за колко време.

Иначе използването на сесии е лесно. В началото на скрипта се извиква session_start() и след това си се работи с "масива" $_SESSION. (макар да има там някои дребни уловки).

Една забележка имам относно паролите. Не са криптирани, а хеширани - т.е. видът им не зависи от някакъв допълнителен ключ. Ако някой се добере до тях, може да им приложи една dictionary или brute force атака , а защо ли пък не и rainbow таблички. За случая обаче става, иначе се отдалечаваме от простотата.

Ако ми остане време и аз ще приложа един примерен прост log-in (който ще се основава на твоят ), но пък за това е форума . Изкушението да се слагат нови работи и да се усъвършенстват старите е голямо.

редактиран от insecteater на 20.08.07 10:01
anonymous
Пон, 20.08.07, 10:07
Ще се опитам да дам отговор на поставения въпрос със сесиите.
Както всички, които програмират на PHP знаят, използването на сесии в един сайт започва със запазения израз
session_start();
който предизвиква създаването на файл на сървъра, който файл има служебно име. Такъв файл се създава за всяка една конекция към сървъра, осъществена през даден сокет. Всъщност може да се каже, че това е cookie, но в по-обобщен вид, създадено на сървъра в директория, която е указана в настройките на сървъра. Например, ако ползвате Apache, в php.ini съществува клаузата session.save_path, която служи за задаване на пътя за запис на сесийните файлове.
Във всеки сесиен файл информацията се пази във вид, подобен на този от cookie, а именно: <име_на_поле>:<стойност>, като е възможно и много често използвано стойността да бъде обект или масив от данни.
Да се върнем на примера. Ако в сесийният масив $_SESSION въведем броя на опитите за вход в дадения сайт, то те ще бъдат на разположение за проверка по всяко време преди изтичането на валидността на сесийния файл.
Например:
if(isset($_SESSION['counter'])
{
 $_SESSION['counter']=$counter+1;
}
else
{
 $_SESSION['counter']=1;
}
if($_SESSION['counter']>=4)
{
 header('Location: loginerror.php');
}
else
{
 header('Login: login.php');
}
Това е прост пример за използване на сесиен файл за защита от логване. Аз бих предпочел да използвам генерирането на картинка в реално време, което затруднява или по-скоро прави невъзможно логването чрез програма.
редактиран от anonymous на 20.08.07 10:10
phrozencrew
Пон, 20.08.07, 12:00
Благодаря за чудесните идеи!
stas67, урока ти е доста интересен. Аз опитах да стартирам файла code.php, но скрипта не можа да намери шрифт. Предполагам, че трябва да конфигурирам php.ini къде да търси шрифтовете на компютъра ми.
Намерих някакъв скрипт, който добавя enviroment променлива, но иска чоплене.
<?php
// Set the enviroment variable for GD
putenv('GDFONTPATH=' . realpath('.'));
// Name the font to be used (note the lack of the .ttf extension)
$font = 'SomeFont';
?>

Ще се опитам да го редактирам и да копирам един TTF шрифт в директорията с файловете.
Относно сесиите вече съм малко по-наясно благодарение на вас.

anonymous
Пон, 20.08.07, 12:10

RE: PHP проста защита на кода и логинг страницата

” Благодаря за чудесните идеи!
stas67, урока ти е доста интересен. Аз опитах да стартирам файла code.php, но скрипта не можа да намери шрифт. Предполагам, че трябва да конфигурирам php.ini къде да търси шрифтовете на компютъра ми.
Намерих някакъв скрипт, който добавя enviroment променлива, но иска чоплене.
<?php
// Set the enviroment variable for GD
putenv('GDFONTPATH=' . realpath('.'));
// Name the font to be used (note the lack of the .ttf extension)
$font = 'SomeFont';
?>

Ще се опитам да го редактирам и да копирам един TTF шрифт в директорията с файловете.
Относно сесиите вече съм малко по-наясно благодарение на вас. „

Извини ме за неточността! Наистина във функцията е посочен шрифт на точно определено място и това е така поради факта, че в Linux често няма шрифтове, които идват от Windows системите. Просто остави там име на шрифт, например arial.ttf. Смятам, че ще тръгне.
редактиран от anonymous на 20.08.07 12:13
редактиран от anonymous на 20.08.07 12:19
insecteater
Чет, 23.08.07, 12:26
По-долу прилагам примерен сорс код за система за login (базиран на сесии). Възможностите са следните:
    - Бан за определено време след определен брой неуспешни опити за влизане
    - Запис на последното влизане на всеки потребител - време и IP адрес

Големият файл е необходим само там, където е формата за login.
На всяка друга страница, която е необходимо да бъде защитена, е нужно в началото да се сложи следният код (или по-добрият вариант - да се include-не файл който го съдържа):
<?php
session_start();
if (!$_SESSION["User"]) {
    header("Location: login.php");
    //Ако браузърът/сървърът не приеме хедъра за пренасочване:
    echo 'Click <a href="login.php">here</a> to redirect.';
    die();
}
?>

Данните за потребителите се съхраняват в текстов файл (с разширение .php), изглеждащ по този начин:
<?php die(); ?>
admin	202cb962ac59075b964b07152d234b70			11:50:21 23.08.2007	192.168.0.1
InsectEater	7215ee9c7d9dc229d2921a40e899ec5f	4	1187859805	11:32:59 23.08.2007	192.168.0.2
user1	24c9e15e52afc47c225b757e7bee1f9d	1		11:42:58 23.08.2007	192.168.0.2

На всеки ред има следната информация:
Потребителско име, md5 хеш на паролата, брой неуспешни влизания, време (timestamp) на започване на бана. последно време на влизане, последен IP адрес от който е влизано.
Полетата са разделени със знака за табулация.

Разбира се една сериозна login система не би трябвало да използва такъв метод на съхранение на данните, а да съхранява информацията за потребителите в база данни, където поддръжката на голям брой записи е оптимизирано. Настоящата схема е подходяща за малък брой потребители.

Гледал съм да коментирам смислово кода, за да стане по-ясно къде какво става. Имайте предвид че може и да не е по-силите на някой съвсем начинаещ, незапознат с повечето функции. Така че този код може да "даде урок" на някой начинаещ и да му бъде полезен.

А ето го най накрая и самият сорс:
<?php
session_start();
 
//Проверка за изход от сесията (ако потребителят е натиснал Log out)
if ($_SERVER["QUERY_STRING"] == "logout") {
    //Унищожаваме сесията и създаваме нова
    session_destroy();
    unset($_SESSION);
    $ErrMsg = "You have successfuly logged out";
    session_start();
}
 
$BadLoginsAllowed = 3;              //Брой позволени грешки преди задействане на бана
$BanTime = 2;                       //Време на бана в минути;
$PasswordsFile = "userdata.php";    //Пътя до файла с паролите;
 
//Начало на проверката за влизане
if (!$_SESSION['User']) {
    //Ако потребителят не е влязъл
    //проверяваме дали вече е изпратил потребителско име и парола
    if ($_POST['usrName']) {
        $ErrMsg = "User not found";
        
        //Извличане на данните за потребителите
        $UserData = file($PasswordsFile);
        foreach($UserData as $Line) {
            //Търсене данните за искания потребител
            if (preg_match('/^'.$_POST['usrName']."t.*/i", $Line)) {
                $UserData = explode("t", $Line);
                //Премахване на знака за нов ред от последният елемент
                $UserData[count($UserData) - 1] = trim($UserData[count($UserData) - 1]);
                $ErrMsg = false;
                break;
            }
        }
        
		//Ако е зададен съществуващ потребител
		if (!$ErrMsg) {
		
        //Проверка за премахване на бана
        if (!$ErrMsg && ((time() - $UserData[3]) >= $BanTime*60) && ($UserData[2] > $BadLoginsAllowed)) {
            $UserData[2] = null; $UserData[3] = null;
        }
 
        //Проверяваме дали има временно ограничение за влизането му (3 min)
        if (!$ErrMsg && ((time() - $UserData[3]) < $BanTime*60))
            $ErrMsg = "You are temporary banned";
            
        //Ако няма ограничение, правим проверка на паролата
        if (!$ErrMsg && ($UserData[1] == md5($_POST['usrPass']))) {
            $UserData[2] = null; $UserData[3] = null; //Валидна парола - изчистваме неуспешните опити
            $_SESSION['User'] = $_POST['usrName']; //Записваме необходимите проенливи в сесията
        } else {
            //При грешна парола, увеличаваме неуспешните опити с 1, като
            //същевремнно се защитаваме от "препълване на брояча" :}
            $UserData[2] = ($UserData[2] > 2048)?2048:$UserData[2] + 1;
            //Задаваме и данните за последното грешно влизане - дата и IP адрес
            
            //Проверяваме дали да задействаме временният бан
            if ($UserData[2] > $BadLoginsAllowed && (!$UserData[3])) {
                $UserData[3] = time();
                $ErrMsg = "Wrong password. You have been banned for $BanTime minutes.";
            }
            if (!$ErrMsg) $ErrMsg = "Sorry, wrong password";
        }
        
        if ($ErrMsg) {
            //Записване на IP адреса и времето на последното неуспешно влизане.
            ///Ако го нямаше условието if ($ErrMsg), тогава щеше да записва
            //данните за последното влизане - независимо успешно или не.
            $UserData[4] = date("H:i:s d.m.Y", time());
            $UserData[5] = $_SERVER["REMOTE_ADDR"];
        }
        
        //След като сме дали или отказали достъп, е време да запишем новите данни
        //в текстовият файл, съдържащ данните за потребителите.
        //Всички промени в масива UserData се запазват в съответният ред на файла.
 
        $Users = file($PasswordsFile);
        //Намираме реда с данните за потребителя и го заместваме с новите данни;
        $LineToReplace = array_search($Line, $Users);
        $Users[$LineToReplace] = join("t", $UserData)."rn";
        if (@file_put_contents($PasswordsFile, $Users) === false)
            $ErrMsg .= " Access denied to change userdata";
		} //Край на действията за валиден потребител
    }
} //Край на проверката за влизане
 
//Да поглезим потребителя :)
$_POST["usrName"] = trim(addslashes($_POST["usrName"]));
 
//Ако влизането е успешно, препращаме потребителя към необходимата страница -
//настройки,приветствена страница или нещо подобно 
if ($_SESSION['User']) {
    header("Location: welcome.php");
	//Ако браузърът не приеме хедъра за пренасочване:
	echo 'Click <a href="welcome.php">here</a> to redirect.';
	die();
}
 
?><!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
  <head>
      <meta http-equiv="Content-Type" content="text/html;charset=iso-8859-1" />
      <title>The truth is out there</title>
</head>
<body>
<?php echo "<p>$ErrMsg</p>"; ?>
<form action="<?php echo $_SERVER[PHP_SELF]; ?>" method="post">
    <p>
    User name: <input type="text" name="usrName" value="<?php echo $_POST["usrName"]; ?>" /><br />
    Password: <input type="password" name="usrPass" /><br />
    <input type="submit" value="Login" />
    </p>
</form>
 
</body>
</html>

Примерно използване на тази схема можете да видите тук: http://InsectEater.homeip.net/login/login.php
Потр. име "admin". Парола "123" (паролата е 123, поне докато някой от вас не я смени )
Ето и сорс кода на примера: login.zip

Обърнете внимание и на тези две картинки:
ff.png и lynx.png

Едната е как изглежда страницата във firefox а другата в текстовия браузър lynx.
Въпреки изключването на стиловете, страницата се държи и изглежда съвсем прилично и разбираемо. Което за съжаление не може да се каже за множество от страниците на които сме свидетели днес. Макар че при по-сложните става трудно.

редактиран от insecteater на 23.08.07 12:27

Коментар

за нас | за разработчици | за реклама | станете автори | in english  © 1998-2024   Experta Ltd.