Перейти к содержимому

 Друзья: Всё для вебмастера | [ Реклама на форуме ]


Rutor
Rutor


[ DDos Услуги. DDos атака. Заказать ДДос ]


Написание безопасных web-приложений


  • Авторизуйтесь для ответа в теме
В этой теме нет ответов

#1
ComitaSEO

ComitaSEO

    Мегабайт

  • Members
  • PipPipPip
  • 266 сообщений
Написание безопасных web-приложений.

Содержание:
Аннотация или суть проблемы
Типы инъекций
Code injection
SQL injection
XSS injection
Советы по проектированию безопасного web приложения.
Избавляемся от Code injection
Избавялемся от SQL injection
Сообщения об ошибках
Обработка данных
Оператор Like
Избавляемся от XSS injection
Проверка файлов при загрузке данных по http
Mysqli параметризированные запросы, placeholders

Аннотация или суть проблеммы:
Приходится писать много кода. Иметь дело с огромным количеством вэб приложений. К сожалению, раз за разом среди совсем различных скриптов встречаю одни и те же ошибки, которые приводят к большим проблемам безопасности в среде разрабатываемого вэб приложения. Это и типичные ошибки, связанные с отсутствием или недостаточной фильтрацией данных, передаваемых от пользователя на сервер, и предоставление отладочной информации, информации об ошибках приложения и предупреждениях прямо в браузер на клиентской вэб странице, и недостаточная проверка содержимого файлов, загружаемых через POST.

Целью этой статьи будет вкратце ознакомить Вас с основными видами угроз безопасности web приложений и привести пример построения защиты стандартными средствами функций обработки данных и настройки выполнения скриптов в среде приложения, использующей для своего функционирования PHP 4+ Mysql 3.2+.

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

Согласитесь, не слишком радужная перспектива?

Примечание:
Статья от Вас потребует определенного уровня знаний. Вы должны быть как минимум поверхностно знакомы с базами данных и синтаксисом запросов SQL, иметь представление о работе с функциями в языке PHP, и о разметке HTML страниц, а так же о способе передачи информации от клиента к серверу.

Начнем с обзора основных уязвимостей:

Итак, какие виды уязвимостей бывают, об этом было довольно много сказано.
Рассмотрим три основные из них: Сode injection (php including), sql injection, xss scripting. (Краткая информация с описанием инъекций размещенная и дополненная здесь, взята со страниц журнала www.xakep.ru Внутримышечно и внутривенно, Спецвыпуск: Хакер, номер #075, стр. 030, Обзор технологий взлома веб-ресурсов).
Сode injection
В теории все выглядит просто: есть скрипт, исполняемый на сервере, в который взломщику необходимо встроить свой код. Провернуть такую махинацию довольно просто, если соблюдены два условия. Во-первых, веб-разработчик должен использовать конструкцию include с параметром переменной, а во-вторых, должен плохо проверять данные, поступающие от пользователя. Наиболее часто такая ситуация возникает в простых скриптах:

Язык: HTML + PHP
PHP код:
<!- Заголовок -->
<?php
include ($page);
?>
<!- Завершающая часть -->
Соответственно, работа идет со ссылками типа ]]>http://tralivali/ind...?page=about.php]]>. Самое безобидное, что можно сделать  это просмотреть информацию о PHP (и не только):

Листинг
Язык: PHP
PHP код:
<?php
phpinfo();
?>
Создав такой файл у себя на сервере, просто включаем его в запрос вместо about.php  и видим всю информацию на экране. Таким образом, мы внедрили произвольный код в скрипт на сервере и получили необходимую информацию. Но это только цветочки, ведь можно при помощи PHP сделать все, что нашей душе угодно.

SQL Injection

Давным-давно, когда по земле еще ходили динозавры, веб-программеры использовали для хранения данных текстовые файлы. Потом, когда человек изобрел колесо, веб-разработчики придумали базы данных и стали хранить все в них. И было всем счастье  и список пользователей туда засунуть можно, и все документы на сайте положить. Но однажды один умелец случайно ввел в форму апостроф, и выдал скрипт SQL-ошибку. Прочитал умелец сообщение, подумал немного и ввел вместо апострофа

Листинг
Язык: SQL
Код:
' OR '1'='1
еще немного поколдовал и стал с тех пор администратором. Работа с базой ведется на языке SQL, например, чтобы проверить, что пользователь существует, можно сделать такой запрос по логину:

Листинг
Язык: SQL
Код:
' SELECT * FROM users WHERE username='$username'
А теперь подставь в запрос апостроф или ' OR '1'='1 и посмотри, что получится. Фактически, мы можем выполнить произвольный SQL-запрос, и случиться что-нибудь нехорошее:

Листинг
Язык: SQL
Код:
' '; DELETE FROM customers WHERE 1 or username = '
Хочу кинуть еще пару хороших идей, как можно воспользоваться SQL-инъекцией. Для этого мы рассмотрим детали диалектов языка SQL у разных производителей. Начнем с MySQL, который делает то, что я от него ни как не ожидал . А проблема проста: если SELECT-запрос подвержен инъекции, то не факт, что его результат будет выведен на экран, но если внимательно почитать руководство по MySQL, то можно найти замечательный функционал  результат запроса можно перенаправить в файл!

Листинг
Язык: SQL
Код:
SELECT <поля> FROM <таблица> INTO OUTFILE '<файл>';
Теперь осталось найти каталог, который нас приютит. В случае с CMS все решается довольно просто  почти всегда есть каталог для загрузки файлов upload. Именно в такой каталог и стоит перенаправлять вывод.
Начиная с четвертой версии, MySQL поддерживает объединение запросов при помощи команды UNION. Таким образом, можно вывести дополнительную информацию из произвольной таблицы. Посмотрим, как это выглядит на примере:

Листинг
Язык: SQL
Код:
SELECT title, description FROM articles WHERE id=$id;
Title и description имеют тип varchar, поэтому переменной $id нужно присвоить такое значение, чтобы при подстановке получился следующий запрос:

Листинг
Язык: SQL

Код:
SELECT title, description FROM articles WHERE id=123123
UNION
SELECT login, password FROM users;
/*
;
Обрати внимание, как я использовал комментарий  при Union-инъекциях  это стандартная практика отсечения ненужной части строки.

Примечание:

На самом деле здесь требуется выполнение еще одного условия, чтобы подобрать нужный тип запроса для SQL инъекций, необходимо чтобы разработчик использовал вывод сообщения об ошибках в стандартный поток браузера. Что практикуется очень часто.

Пример:


PHP код:
<?php 
$q = 'SELECT * FROM `bd` WHERE `id`='.$_GET['id'];
$r = mysql_query(q);

if(!is_resource($r)){ //!$r || mysql_error()
die(mysql_error()); //echo(mysql_error()); 
}
?>
Примерно вот так разработчик «выводит» возникшую при запросе к базе данных ошибку в браузер, что, зачастую, помогает злоумышленнику, предоставляя необходимую информацию для составления запроса с SQL injection.

XSS

В наше время на большинстве сайтов пользователи имеют возможность создавать свои материалы и комментировать чужие. Для красивого оформления пользователю дается возможность вводить данные в формате HTML. Ввод HTML-кода может быть разрешен напрямую, либо при помощи WYSIWYG-редактора. Казалось бы, все довольны! Особенно взломщики . Есть хорошая пословица: «Где HTML, там и Javascript». Таким образом, при вводе можно использовать тег <script> для исполнения произвольного Javascript (и не только его, кстати). А если мы можем использовать скрипты, которые исполняются на стороне пользователя, значит, мы можем украсть его куки! А тут уже и до взлома аккаунта недалеко

Примечание:

Здесь не все так просто, получить можно не только cookie клиента, но можно изменить поведение клиентской страницы буквально произвольно, по желанию, от нарушения дизайна сайта, дефейса уязвимого сайта, вплоть до полного перенаправления всей информации с сайта источника на другой сайт. И дело не только в javascript, например без использования javascript вполне возможно добавить <iframe>, <frameset> и многие другие не желательные теги, включить flash тэги <embeded>, <object> и апллеты в html в содержимое страницы.

Фрагментированные XSS-атаки

Часто бывает так, что информация, вводимая пользователем, хорошо фильтруется, и к форме на кривой козе не подступишься. Но если в ней несколько полей, например, «название материалов» и «содержание материалов», то можно попробовать более сложный вид атаки  фрагментированный. При таком подходе, необходимо, чтобы данные из разных полей формы выводились последовательно, и тогда появляется вероятность того, что вместе «безобидные строчки» станут грозным оружием.


Советы по проектированию безопасного web приложения.

Избавляемся от Code injection:


Как обезопаситься? Здесь все просто. Передавайте через GET,POST массивы скрипты, которые нужно подключить в данном приложении, не напрямую, а косвенно, через определенную переменную, и, на основании ее значения, подключайте тот или иной скрипт, используя include или require.

Пример:

Нам нужно подключить содержимое файла example.php в index.php.
Передаем в GET переменную file значение 1.
Index.php?file=1
Код файла index.php:

PHP код:
<?php
	$file = intval($_GET['file']);
	if(isset($file) && !empty($file)){
	   switch($file){
case 1:
 include($_SERVER['DOCUMENT_ROOT'].'/ example.php');
break;
//case 2 case 3. И так далее перечисление других подключаемых файлов
default:
break;
}			
}
?>
Замечание:

Иногда угроза CODE injection возникает еще по одной причине. Некоторые начинающие вэб разработчики подключение самых обыкновенных файлов используют с помощью include,require, либо стараются придать конфигурационным файлам отличное от php расширение, .ini и прочее.
Предостерегаю вас от подобных ошибок. Для функции работы с файлами, пожалуйста, используйте набор стандартных функций для получения информации из файлов (file,file_get_contents,fread,fgetc,fpassthru,fget ss). Но не в коем случае не выполняйте их содержимое, как код PHP. Для конфигурационных файлов пожалуйста сохраняйте их с расширением .php. Web сервер сам поможет Вам скрыть содержимое данных файлов от просмотра через web браузер.

Избавляемся от SQL injection.

Первая проблема: Вывод информации об ошибках базы данных в браузер пользователя.
Нам нужно определиться, когда этап отладки миновал, стоит ли нам информацию об ошибках SQL, возникшую при работе скриптов, выводить в браузере клиента? По-моему нет. Но иногда требуется все же иметь доступ к этой информации, которая во многом в процессе работы приложения помогает устранять ошибки и недостатки. Как же быть?
Казалось бы, выход очевиден.
Используем попытку запрета вывода об ошибках вообще:

PHP код:
<?php 
ini_set('display_errors','off');
error_reporting(0);
?>
Но, ошибки базы данных, выводимые в браузер разработчиками с помощью
PHP код:
echo(mysql_error()); die(mysql_error())
всеже останутся.
Не следует действительно писать:

PHP код:
<?php
echo(mysql_error());
die(mysql_error())
?>
PHP предоставляет для этих случаев функцию trigger_error.

bool trigger_error ( string $error_msg [, int $error_type= E_USER_NOTICE ] )
string $error_msg  наша ошибка, например mysql_error()
int $error_type= E_USER_NOTICE
может принимать значение трех констант
E_USER_WARING,E_USER_NOTICE,E_USER_ERROR
Итак, перепишем наше сообщение об ошибках в одном из примеров с использованием данной функции:
Пример приведенный выше:

PHP код:
<?php 
$q = SELECT * FROM `bd` WHERE `id`=.$_GET[id];
$r = mysql_query(q);

if(!is_resource($r)){ //!$r || mysql_error()
trigger_error(mysql_error(),E_USER_ERROR); 
}
?>
Результат работы в итоге будет тот же самым, скрипт завершит свое действие в той же строке при возникновении ошибки запроса, но с одним существенным отличием, теперь мы можем перехватить содержимое ошибки, и отправить его в лог файл ошибок.

Совет:

Директорию для хранения файлов с ошибками лучше всего определять за пределами директории вэб сервера. Либо ограничить доступ к ней с помощью .htaccess, или же хотя бы файлам с ошибками присвоить расширение .php и первой строчкой каждого файла сделать:

PHP код:
<?php die("");?>
Чтобы было невозможно со страниц сайта прочесть содержимое файла, который будет нести столь важную информацию.

Вопрос: почему же перехватывать сообщения об ошибках именно в файл? Не в базу.

Ответ: ошибки могут возникнуть еще при подключении к базе данных, и их нужно корректно обработать и перехватить и на этом этапе тоже.

Чтобы перехватить вывод ошибок из браузера в файл мы воспользуемся следующими функциями.
mixed set_error_handler ( callback $error_handler [, int $error_types= E_ALL | E_STRICT ] )
Первым параметром функции set_error_handler указываем свою функцию перехватчик ошибок, вторым необязательным параметром указываем типы перехватываемых ошибок.

Функция, которая будет перехватывать ошибки, указываемая в параметре callback $error_handler должна принимать следующие параметры:
$error_num  номер ошибки,
$error_var  описание ошибки
$error_file  где произошла ошибка,
$error_line  строка, где произошла ошибка

Вот сокращенный пример перехвата ошибок в файл, работающий в нескольких моих вэб проектах. Добавляем этот код с первым подключаемым на каждой странице файлом, и тем самым организуем перехват ошибок в файлы. Кроме этого с ошибкой можно логировать достаточно много интересной информации, как например у меня  ай пи пользователя, страница, с которой он пришел на эту страницу, если таковая есть, время, когда произошла ошибка, и прочее.
Функциональность этого скрипта можно улучшить в сторону отсылки лога ошибок на указанный емейл, архивирования логов ошибок при достижении определенного размера файла ошибок и т.п.

PHP код:
<?php

define ('ROOT_PATH', $_SERVER['DOCUMENT_ROOT']."/");//корневая директория для файлов с ошибками
define('_ERR_HANDLING',true); //запретить - true/разрешить  false вывод ошибок в браузер
define('_ERR_DIR',ROOT_PATH.'/errs/'); //где будем хранить файлы ошибок, в какой папке?
err_handler();

function err_handler(){
		
		
		if(_ERR_HANDLING){
		
		$error_reporting						= '';	
		$error_reporting						= ini_get('error_reporting');
		$error_reporting						= $error_reporting?$error_reporting:E_ALL;
		
		
		error_reporting(E_ERROR);
		
			
			
		$date_file								 = date('dmY').'.php';
		$dir									= _ERR_DIR;
		$path									= $dir.$date_file;
		$logfile								= '';


		if(!is_dir($dir) || !is_writable($dir)){
			
			if(is_dir($dir)&&!is_writable($dir)){
				
				chmod($dir,0775);
				
			} else if(!is_dir($dir)){
			
				$isdir							= false;
				$isdir							= mkdir($dir,0775);
			
			}
			
			if(!$isdir&&!is_writable($dir)){
				$dir							= ROOT_PATH;
				$path							= $date_file;	
			}
		}


		if(is_dir($dir) && is_writable($dir)){
	

		if(!is_file($path)){
			
			
			$fp			= fopen($path,'w+');
			if($fp && is_resource($fp)){
				
				$secuire	= '<?php die("Forbidden."); ?>';
				flock($fp,LOCK_EX);
				fwrite($fp,$secuire."\n");
				flock($fp,LOCK_UN);
				fclose($fp);
				$fp		= null;
				
				
						unset($secuire);
			}
		}	
		
		
		
		if(is_file($path) && !is_writable($path)){
			
				chmod($path,0775);
		
		}
		
		if(is_file($path) && is_writable($path)){
			
		ini_set('display_errors',0);
		set_error_handler('error_reporting_log', (E_ALL & ~E_NOTICE));
		$logfile							= $path;
				
			define('LOG_FILE',$logfile);
		
		}
			
				unset($date_file,$dir,$path,$logfile);
		
		}
		
}

		error_reporting($error_reporting);
		
		unset($error_reporting);

}


function error_reporting_log($error_num, $error_var=null, $error_file=null, $error_line=null) {
				 
						
						
						$error_desc		= '';
						$error_desc	  = 'Error';
						
						switch ($error_num){
							case E_WARNING:
								$error_desc = 'E_WARNING';
								break;
							case E_USER_WARNING:
								$error_desc = 'E_USER_WARNING';
								break;
							case E_NOTICE:
								$error_desc = 'E_NOTICE';
								break;
							case E_USER_NOTICE:
								$error_desc = 'E_USER_NOTICE';
								break;
							case E_USER_ERROR:
								$error_desc = 'E_USER_ERROR';
								break;
							default:	
								$error_desc  = 'E_ALL';
								break;
							
						}
						
						$date_file		 = date('y-m-d H:I:S');
						$logfile		= LOG_FILE;
						$url			 = $_SERVER['HTTP_HOST'].$_SERVER['REQUEST_URI'];
						$date_time		 = date('d.m.y - H:i:s');
						$ip				= $_SERVER['REMOTE_ADDR'];
						$from			= isset($_SERVER['HTTP_REFERER'])&&!empty($_SERVER['HTTP_REFERER'])?$_SERVER['HTTP_REFERER']:'';
						
						
						$errortext		 = $error_desc.': '.$error_var."\t".'Line: '.$error_line."\t".'File: '.$error_file."\t".'Link: '.$url."\t".'Date: '.$date_time."\t".'IP: '.$ip."\t".' FROM:'.$from."\n";

										unset($from,$error_desc,$error_var,$error_line,$error_file,$url,$date_time,$error_write);
						
						$secuire		= '<?php die("Forbidden."); ?>';
						
						if(is_file($logfile)&&is_writeable($logfile)){
							$strings	= file($logfile);
							if(isset($strings[0])&&!empty($strings[0])&&strpos($strings[0],$secuire)===false){
								unlink($logfile);
							}
								unset($strings);
						}
						
						if(!is_file($logfile)){
							$dir		= dirname($logfile);
							if(is_dir($dir)&&is_writable($dir)){
							$fp				 = fopen($logfile,'w+');
										if(is_resource($fp)){
											flock($fp,LOCK_EX);
											fwrite($fp,$secuire."\n");
											flock($fp,LOCK_UN);
											fclose($fp);
											$fp		= null;
										}		
													unset($dir,$fp);
							}
						}
													unset($secuire);
						if(is_file($logfile)&&!is_writable($logfile)){
							chmod($logfile,0775);
						}
						
						if(is_file($logfile)&&is_writeable($logfile)){
							
							$fp				 = fopen($logfile,'a+');
							if(is_resource($fp)){
								flock($fp,LOCK_EX);
								fwrite($fp,$errortext);
								flock($fp,LOCK_UN);
								fclose($fp);
								$fp		= null;
										unset($fp);
							}
						
						}
										unset($logfile);
						
				
				return true;
}


?>
Мне могут возразить, что на самом деле на серверах ведутся access_log, error_log логи доступа и ошибок вэб приложений. Но, не всегда есть доступ к этой информации, и свой перехватчик ошибок можно настроить гораздо гибче на предоставление именно той информации, которая нам нужна более всего.
Итак, после включения логирования информации и изменении вывода информации об ошибках в виде
PHP код:
trigger_error(mysql_error(),E_USER_ERROR);
на странице пользователя больше не отобразится информация о ошибке mysql, что добавит работы хакеру при выявлении возможности sql injection.

А в папке с ошибками появится файл с именем в виде текущего числа и содержащий следующую информацию:

PHP код:
<?php die("Forbidden"); ?>
E_USER_WARNING: MySQL error: 1064		You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near ')' at line 1		Query:SELECT * FROM mybb_attachments WHERE pid IN ()	Line: 348	File: /forum/inc/db_mysql.php	Link: */forum/archive/index.php/thread-796.html	Date: 27.02.09 - 07:23:07	IP: 66.249.72.99	 FROM: http://www.ya.ru
Замечание:

Правда не все ошибки можно перехватить в PHP, например фатальные ошибки E_ERROR без хаков перехватить не получится.

Вторая проблема: недостаточная обработка данных перед помещением в базу данных.

Собственно здесь и возникают те самые ошибки, которые могут помочь удаленному пользователю произвести SQL injection.

Чтобы этого избежать советую перед помещением данных в запрос к базе применять фильтрацию данных.
Какие функции нам здесь могут помочь при проверке переменных перед построением запроса к базе данных?
Функции:

Для проверки существования непустых данных:

bool isset ( mixed $var [, mixed $var [, $... ]] )
Устанавливает определена ли переменная.

bool empty ( mixed $var )
Если переменная не должна быть пустой или не должна принимать отрицательное нулевое значение.

Для обработки целочисленных данных:

Функция

int intval ( mixed $var [, int $base ] )
Возвращает целое значение переменной var , используя указанное основание системы исчисления base для преобразования (основание по умолчанию 10).

bool is_numeric ( mixed $var )

Проверяет, является ли данная переменная числовой.

Если данные должны быть еще к тому же исключительно положительны (например: уникальный столбец в базе данных с атрибутом AUTO_INCREMENT) советую использовать:

number abs ( mixed $number )

Возвращает абсолютное значение number . Если number имеет тип float, возвращаемое значение также будет иметь тип float, иначе - integer.

Для обработки строковых данных:

Функция
string trim ( string $str [, string $charlist ] )

Удаляет лишние пробельные последовательности символов из начала и конца строки.

Функция string mysql_real_escape_string ( string $unescaped_string [, resource $link_identifier ] )

Советую обрабатывать данные этой функцией перед помещением их непосредственно в запрос к базе данных. Предпочтительнее использовать ее, нежели addslashes или доморощенные аналоги, дело в том, что данная функция корректно определяет кодировку подключения к mysql базе данных.

Но здесь не все так просто, нужно также будет избежать двойного экранирования строки. Воистину говорится «дорога в Ад вымощена благими намерениями». Так вот, чтобы сделать автоматическим экранирование данных разработчики PHP добавили такую возможность как magic quotes  или автоматическое экранирование данных. Эта возможность будет полностью исключена в 6 версии PHP. А пока с ней не знают что и делать. Часть хостингов ее отключает. Часть  нет. В чем же суть проблемы:
К данным, которые пришли в приложение из post, get массивов, если magic_quotes_gps = on применяется автоматически функция addslashes:

string addslashes ( string $str )

Возвращает сроку str , в которой перед каждым спецсимволом добавлен обратный слэш (\).

Пример:
Index.php?info=hithere

В скрипте при включенном режиме magic_quotes_gps = on значение переменной $_GET[info] будет равно hi\there

Добавится лишний \ бэк слэш. И если перед помещением данных в базу данных мы обработаем их с помощью mysql_real_escape_string то будет добавлен лишний слэш $_GET[info] будет равно hi\\there
, что приведет к возникновению ошибки на стадии запроса.

Здесь нам поможет функция:

int get_magic_quotes_gpc ( void )

Которая вернет 1 если magic_quotes_gps включен. И мы сможем исключить лишние слэши из значения переменной с помощью функции:
string stripslashes ( string $str )

Удаляет экранирующие бэкслэши.
Index.php?info=hithere


Количество пользователей, читающих эту тему: 0

0 пользователей, 0 гостей, 0 анонимных