[Guide] Как да защитим сайта си?

RaFa

Team Member
Joined
Jan 24, 2009
Messages
785
Reaction score
484
Здравейте, съфорумници,

Реших да напиша един урок за това как да защитите максимално кода си от външни фактори (aka пишлегари, научили 1-2 неща).

Отсега казвам, че това не са най-правилните начини, но вършат работа на 100%.

Филтрация
* Нещо, което не трябва да се използва според мен.

Най-често срещата грешка (поне в сайтовете тук) е, че всички разчитате на филтрацията, най-често с str_replace, което може да изиграе лоша шега. Имате по един масив (array) със забранените символи и ги махате от дадения стринг.

PHP:
<?php
$forbidden = array('shutdown', 'drop', 'update');
$username = str_replace($forbidden, '', $_POST['username']);

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

PHP:
<?php 
$username = preg_replace('/[^A-Za-z0-9\_]/', '', $_POST['username']);

[^A-Za-z0-9\_] - този regex ще позволи само малки и големи латински букви (A-Z & a-z), числа (0-9) и долна черта (\_). Някой сеща ли се за начин за минаване на този филтър? :)

Валидация

Винаги, винаги, винаги, валидирайте дали стойността взета от протребителя отговаря на вашите очаквания.

Пример:
Очаквам потребителското име да е между 4 и 10 символа и да съдържа само букви, цифри и долна черта:

PHP:
<?php 
$username = $_POST['username'];
if(!preg_match('/^[A-Za-z0-9\_]{4,10}$/', $username)) {
    // Потребителското име на отговаря на горните критерии.
    exit; // Ако нещо не е наред, не продължавайте изпълнението на скрипта!
}

^[A-Za-z0-9\_]{4,10}$
^ - търсенето да започне от самото начало на $username
$ - търсенето да е до самия край на $username
A-Za-z - големи и малки латински букви
\_ - долна черна
{4,10} - между 4 и 10 символа

^ и $ са важна част от regex-a, винаги ги използвайте, иначе тази валидация може да бъде лесно преодоляна.

Повече:
RegexOne - Learn Regular Expressions - Lesson 1: An Introduction, and the ABCs

Типове данни

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

Когато потребителя е длъжен да въведе число, използвате int cast или intval
PHP:
<?php
$strength = (int) $_POST['strength'];
$agility = (int) $_POST['agility'];

// Каквото и да е въвел потребителят, стойностите ще бъдат преобразувани в числа.
// Дори да е въвел някакъв супер-мега-ултра хакерски скрипт... ще ви бъде върнато числото 0

// Отново валидация
if ($strength < 0 && $strength > 1000) {
    // Стойността на strength трябва да е по-голяма или равна на 0 и по-малка или равна на 1000
    exit;
}

Повече информация:
PHP: Type Juggling - Manual

Prepared Statements

Prepared statements спират 99% от SQL инжекциите, но все пак трябва да е последното ниво на защита, не трябва да предавате никакви стойности без да са минали през валидация и типизиране (типове данни). Филтрацията не е задължителна, ако имате валидация и типизиране.
Ако трябва да ги обяснявам тук... поста ще стане километричен. Има доста материали по темата и можете да ги погледнете:

PHP Prepared Statements
�5.4.MySQLi - Базово използване - част 2 - Prepared Statements (Gatakka �7.1�.2�13) - ��.34.32h - YouTube - на български!! :)

Често използвани Regex-и

Това са regex-и, които аз използвам често и смятам, че биха били от полза и за вас:
^[A-Za-z0-9\_\.\-]{1,20}@[A-Za-z0-9\_\-]{1,20}\.[A-Za-z]{2,7}$ - Валидация на email (максимална дължина 49 символа)
^[A-Za-z0-9\_]{4,10}$ - потребителско име
^([a-zA-Z\-\']{2,20}\s?){1,3}$ - лично име (максимална дължина 62 символа, преди това трябва да мине през trim()). Ако сайтът ви е интернационален препоръчавм добавянето и на единична кавичка (') за имена като O'Sullivan. После трябва да бъде преобразувана в &quot;


Надявам се поста да е от полза, ако имате допълнителни въпроси пишете в темата!

Поздрави,
RaFa
 
Също бих могъл да добавя, че аз лично изпращам заявките под формата на числа, тъй като по-лесно се филтрират с (int), а в самият код си ги превключвам към стрингове със switch, писането е повече но е 100% сигурно.

Освен това специално за сесиите основна грешка е проверка от типа:

PHP:
if(isset($_SESSION['username'])){
 // koda
}

тъй като в този случай дори сесията да е NULL или 0 ще се приеме за true и кода ще се изпълни, за по голяма сигурност може да се добави :


PHP:
if(isset($_SESSION['username']) && !empty($_SESSION['username'])){
 // koda
}



Друго, което се сещам е следният код:

PHP:
if($var){
}

като var е определена заявка или някаква константа или стойност, в случая ние очакваме с този вид проверка да получим като резултат true, но тъй като езика не е типизиран, никъде не казваме че очакваме булев резултат и следователно всяка стойност различна от Null ще е true.
Тук аз лично ползвам следният вид изписване, когато очаквам булев резултат

PHP:
if((bool)$var == true){
}

PS.

Също така, когато не ползвате https и искате защитени постове е абсолютно задължително да си валидирате заявките. Нещо такова съм направил в DTweb market функциите:
PHP:
function decrypt($encrypted,$hashed){
	$get_key  = mssql_fetch_row(mssql_query("Select [encrypt_key] from [Market_Settings]"));
    $iv       = substr(md5("".substr($get_key[0],0,8).""),0,mcrypt_get_iv_size(MCRYPT_BLOWFISH, MCRYPT_MODE_CFB));
    $dec_val  = mcrypt_decrypt(MCRYPT_BLOWFISH, md5("".$get_key[0].""), base64_decode($encrypted), MCRYPT_MODE_CFB, $iv);
    $salt     = substr($hashed, 0, 25);
    $new_hash = $salt . substr(sha1($salt . $dec_val), -25);
    if($new_hash == $hashed){
	  return($dec_val); 
	}
	else{
		return false;
	}
}
function encrypt($original_value){
	$get_key    = mssql_fetch_row(mssql_query("Select [encrypt_key] from [Market_Settings]"));
    $iv         = substr(md5("".substr($get_key[0],0,8).""),0,mcrypt_get_iv_size(MCRYPT_BLOWFISH, MCRYPT_MODE_CFB));
    $enc_val    = base64_encode(mcrypt_encrypt(MCRYPT_BLOWFISH, md5("".$get_key[0].""), $original_value, MCRYPT_MODE_CFB, $iv));
    $salt       = substr(md5(uniqid(rand(), true)), 0, 25);
    $hash_val   = $salt . substr(sha1($salt . $original_value),-25);
	return array($enc_val,$hash_val);
}

Като $get_key[0] всъщност е някакъв хекс (ключова дума) по ваш избор, който се използва за криптиране на информацията, която се подава от самата форма било то стринг или интиджер към функцията. Преди да се изпълни функцията подадените стойности се декриптират и сравняват с криптираните, ако съответстват се изпълнява фунцкията, ако не се връща грешка. По този начин какъвто и да е едит с /Hackbar, TamperData на подадените стойности ще бъде бесмислен.
 
Last edited:
Браво за темата!
 
Езика не е толкова типизиран като C#, Java, C++ и други езици на ниско ниво, но от PHP 7 започва да се вижда как става все по-типизиран.

Примерно при деквариране на функция можем да й окажем тип на вход и изход.

PHP:
public function foo(int $integer, string $string, array $array) : int
{
    //function body
}

С примера по-горе показвам, че метода приема параметри които задължително трябва да са от предхождащия ги тип и задължително този метод трябва да връща стойност от тип Integer.

А за парсването към (bool), не е толкова задължително в IF-ELSE да бъде парсвано към (bool).

PHP:
<?php
var_dump((bool) "");        // bool(false)
var_dump((bool) 1);         // bool(true)
var_dump((bool) -2);        // bool(true)
var_dump((bool) "foo");     // bool(true)
var_dump((bool) 2.3e5);     // bool(true)
var_dump((bool) array(12)); // bool(true)
var_dump((bool) array());   // bool(false)
var_dump((bool) "false");   // bool(true)
?>

За начинаещи в PHP програмирането видеата по долу ще са им много полезни и ще разберат цялостно езика PHP.
Курс PHP Fundamentals - януари 2017 - Софтуерен университет (на български)
 
Last edited:
Yup. Ако сега започвате да се занимавате, скачайте направо на PHP7! :)