Pull to refresh

Основы безопасности PHP

Reading time 5 min
Views 28K
Данный материал для начинающих программистов.

Содержание





Демонстрация ошибок


Почему так часто я вижу, зайдя на какой-нибудь сайт что-то подобное этому:
Warning: Use of undefined constant LOCAL_SERVER — assumed 'LOCAL_SERVER' in /web/includes/page-definitions.php on line 13


Это одна из стандартных PHP ошибок, которая а) некрасива для пользователя; б) потенциально опасна.
Поэтому их необходимо перехватывать и упорядочивать.

Во первых, функция error_reporting позволяет нам решить, какие ошибки мы хотим видеть.
В принципе, достаточно просто выключить показ всех ошибок (error_reporting(0)), но нам нужно не это, потому что об ошибках мы хотим знать.
Константа всех ошибок — E_ALL.
В пятой версии появилась константа E_STRICT, показывающая строгие замечания по поводу кода.
Разумеется, их желательно видеть, но они не входят в E_ALL, потому будем использовать числовое значение error_reporting(8191), которое вбирает всё, вплоть до новых ошибок шестой версии.

Примечание для любознательных: error_reporting(E_ALL | E_STRICT) не подходит, ибо тогда PHP 4 будет ругаться, не зная, что такое E_STRICT. С численным значением никаких проблем не будет.


Добавляем проверку на DEBUG — константу, выставленной в конфиге, и, с помощью set_error_handler, будем отлавливать ошибки в уже запущеном сервисе. Кстати, свой репортер ошибок должен возвращать true, иначе PHP выбросит стандартную ошибку.

Результат:
(Насчёт сравнения переменной с пятью параметрами я не уверен в выборе метода: in_array красивее, и гораздо медленее, а switch case case быстрее, но совсем некрасиво. Красота — субъективное дело...)
	<?php

		error_reporting(8191);
		if (!DEBUG)
		{
			function errorHandler ($errno, $errstr, $errfile, $errline)
			{
				// Запись в БД или отсылка по почте вебмастеру.

				if	($errno == E_ERROR ||
					$errno == E_PARSE ||
					$errno == E_CORE_ERROR ||
					$errno == E_COMPILE_ERROR ||
					$errno == E_USER_ERROR)
				{	
					// Сообщение пользователю. Мол, «простите, облажались маленько»...

				}
				return true;
			}
			set_error_handler('errorHandler');
		}
	?>
	



register_globals


До версии 4.2.0 директива register_globals была в PHP включена по умолчанию.
Привело это к тому, что многие привыкли, что если в форме есть , то в PHP коде можно проверять if ($username == 'admin')…

Однако это потенциальная дыра, которая привела ко множеству взломов.
Поэтому к POST, GET, COOKIE переменным надо обращаться через superglobals $_POST, $_GET, $_COOKIE.
Многим это показалось слишком трудно и стала очень популярной команда import_request_variables, возвращающая всё на круги своя.
Так вот.
Не делайте этого.

Другая проблема с register_globals:
	<?php
		...
		if (check_admin($..., $...))
		{
			...
			$user_level = 169;
		}
		...
		if ($user_level > 150)
		{
			echo 'Boom!';
		}
	?>
	

Если пользователь — не администратор, а переменная $user_level не инициализирована
(ей не придано значение 0 в начале скрипта, в надежде, что оно 0 автоматически),
то нехороший человек может дописать в адресной строке foo.php?user_level=999 и получить доступ.


SQL injection и magic_quotes


Так популярна среди начинающих конструкция
	<?php
		$user = mysql_fetch_assoc(mysql_query("SELECT * FROM `users` WHERE `username` = '{$_POST['username'}' AND `password` = '{$_POST['password']}'"));
	?>

	

опасна. Если пользователь введёт вместо пароля ' OR `username` = 'admin, то система впустит его как админа.

Приведённый пример, разумеется, элементарен.
Но если не решить проблему глобально, всегда можно пропустить какой-нибудь запрос, подверженный SQL injection.
Для борьбы с этим разработчики PHP решили сделать так, чтобы вся информация, поступающая от пользователя, подвергалась обработке и все кавычки escapeились (перед ними ставится слэш, что делает команда addslashes).
Что случилось? Вся информация от пользователя приходит со слэшами. Даже та, что вроде слэши получить не должна. Например, комментарии к статье.
Мало того, это не 100-процентный способ защиты от SQL injection.

Решение. а) со всей входящей информации снимаеи слэши, если они есть. б) Всю информацию, поступающую в SQL запрос фильтруем специально для этого созданной функцией mysql_real_escape_string (или аналогом для другой базы данных).

Снимаем слэши:
	<?php
		{
		if (function_exists('get_magic_quotes_gpc') && get_magic_quotes_gpc())
		{
			function stripslashes_deep($value)
			{
				if(is_array($value))
				{
					$value = array_map('stripslashes_deep', $value);
				}
				elseif (!empty($value) && is_string($value))
				{
					$value = stripslashes($value);
				}
				return $value;
			}
	
			$_POST = stripslashes_deep($_POST);
			$_GET = stripslashes_deep($_GET);
			$_COOKIE = stripslashes_deep($_COOKIE);
		}
	}
	?>

	


Создаём функцию для фильтрации (mysql_real_escape_string — длинно, да и привязанно к формату проверки. А если понадобиться поменять фильтр?)
	<?php
		function quote($value) {
			if (!is_numeric($value)) {
				$value = "'".mysql_real_escape_string($value)."'";
			}
			return $value;
		}
	?>

	

И используем её везде. Как только какие-то динамические данных отсылаются SQLу, сразу используем quote:
	<?php
		$user = mysql_fetch_assoc(mysql_query('SELECT * FROM `users` WHERE `username` = '.quote($_POST['username']).' AND `password` = '.quote($_POST['password'])));	
	?>

	



Проверка данных


Проверяйте всё, что вводит пользователь.
По умолчанию он злоумышленник.

Старайтесь не фильтровать, старайтесь валидировать.
Другими словами, не создавайте чёрного списка, создавайте белый.
Вместо
	<?php
		if (are_bad_symbols($data)) boo();
	?>
	

используйте
	<?php
		if (!all_good_symbols($data)) boo();
		// Например:
		is_numeric($data);
		preg_match('/[a-z0-9_-]*/i', $data)
		...
	?>

	

Так вы будете уверены, что информация чиста и никаких неожиданностей не будет.
Если вы составляете список запрещённых символов, всегда можете просмотреть какой-нибудь нехороший %00 и подобные, о которых, скорее всего, не догадываетесь.

Разумеется, есть ситуации, когда нужна фильтрация, например, когда пользователь пишет комментарий.
Тогда надо отсекать плохие символы.
Но в принципе стараться надо валидировать.

Есть несколько команд, с которыми надо обращаться очень осторожно.
Это include, require, readfile, eval, ``, system, exec, create_function, dir, fopen и подобные.
Всегда посмотрите трижды, когда используете их, если в них используются данные, которые могут прийти от пользователя, будьте уверены — кто-то обязательно этим воспользуется.
	<?php
		include($_GET['module'] . '.php');
	?>
	

Этот кусок опасен. Если злоумышленник введёт '../../../../../etc/passwd%00', будет рад, а вы — вряд-ли.

Аутентификация


Не забывайте, что cookies редактируются ни чуть не сложнее, чем то, что видно в адресной строке.
Поэтому всё, что приходит как печенье, потенциально — атака.
Так что не надо хранить в cookies уровень доступа пользователя или его ID.
Лучше всего дать PHP самому разбираться с этим, используя сессии.
	<?php
		session_start();
		$_SESSION['userid'] = 168;
		session_write_close();
	?>
	


Кстати, в cookies вообще хранить что-либо надо очень скромно и три раза подумать, а надо ли?


Вывод


Всё время думайте о данных в переменных $_GET, $_POST, $_COOKIE, как об атаке злоумышленника.
Trust no one! :)
Tags:
Hubs:
+42
Comments 185
Comments Comments 185

Articles