Какой самый лучший метод для дезинфекции ввода пользователя с помощью PHP?

Есть ли где-нибудь функция-ловушка, которая хорошо работает для дезинфекции пользовательского ввода для SQL-инъекций и XSS-атак, но при этом допускает определенные типы HTML-тегов?

20 ответов

Решение

Это распространенное заблуждение, что пользовательский ввод может быть отфильтрован. У PHP даже есть (теперь устаревшая) "особенность", называемая магическими кавычками, которая основывается на этой идее. Это чепуха. Забудьте про фильтрацию (или очистку, или как люди это называют).

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

Другой пример - HTML: если вы встраиваете строки в разметку HTML, вы должны экранировать их с помощью htmlspecialchars, Это означает, что каждый echo или же print заявление должно использовать htmlspecialchars,

Третьим примером могут быть команды оболочки: если вы собираетесь встраивать строки (например, аргументы) во внешние команды и вызывать их с помощью exec, то вы должны использовать escapeshellcmd а также escapeshellarg,

И так далее...

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

Не пытайтесь предотвратить внедрение SQL путем очистки входных данных.

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

Пожалуйста, посетите мой веб-сайт http://bobby-tables.com/ для получения дополнительной информации о предотвращении внедрения SQL.

Нет. Вы не можете в общем фильтровать данные без контекста того, для чего они нужны. Иногда вы хотите принять запрос SQL в качестве входных данных, а иногда вы хотите принять HTML в качестве входных данных.

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

Процесс экранирования данных для SQL - для предотвращения внедрения SQL - очень отличается от процесса экранирования данных для (X)HTML, чтобы предотвратить XSS.

В PHP появились новые приятные функции filter_input, которые, например, освобождают вас от поиска "конечного регулярного выражения электронной почты" теперь, когда есть встроенный тип FILTER_VALIDATE_EMAIL

Мой собственный класс фильтра (использующий javascript для выделения ошибочных полей) может быть инициирован либо запросом ajax, либо обычной формой публикации. (см. пример ниже)

/**
 *  Pork.FormValidator
 *  Validates arrays or properties by setting up simple arrays. 
 *  Note that some of the regexes are for dutch input!
 *  Example:
 * 
 *  $validations = array('name' => 'anything','email' => 'email','alias' => 'anything','pwd'=>'anything','gsm' => 'phone','birthdate' => 'date');
 *  $required = array('name', 'email', 'alias', 'pwd');
 *  $sanatize = array('alias');
 *
 *  $validator = new FormValidator($validations, $required, $sanatize);
 *                  
 *  if($validator->validate($_POST))
 *  {
 *      $_POST = $validator->sanatize($_POST);
 *      // now do your saving, $_POST has been sanatized.
 *      die($validator->getScript()."<script type='text/javascript'>alert('saved changes');</script>");
 *  }
 *  else
 *  {
 *      die($validator->getScript());
 *  }   
 *  
 * To validate just one element:
 * $validated = new FormValidator()->validate('blah@bla.', 'email');
 * 
 * To sanatize just one element:
 * $sanatized = new FormValidator()->sanatize('<b>blah</b>', 'string');
 * 
 * @package pork
 * @author SchizoDuckie
 * @copyright SchizoDuckie 2008
 * @version 1.0
 * @access public
 */
class FormValidator
{
    public static $regexes = Array(
            'date' => "^[0-9]{1,2}[-/][0-9]{1,2}[-/][0-9]{4}\$",
            'amount' => "^[-]?[0-9]+\$",
            'number' => "^[-]?[0-9,]+\$",
            'alfanum' => "^[0-9a-zA-Z ,.-_\\s\?\!]+\$",
            'not_empty' => "[a-z0-9A-Z]+",
            'words' => "^[A-Za-z]+[A-Za-z \\s]*\$",
            'phone' => "^[0-9]{10,11}\$",
            'zipcode' => "^[1-9][0-9]{3}[a-zA-Z]{2}\$",
            'plate' => "^([0-9a-zA-Z]{2}[-]){2}[0-9a-zA-Z]{2}\$",
            'price' => "^[0-9.,]*(([.,][-])|([.,][0-9]{2}))?\$",
            '2digitopt' => "^\d+(\,\d{2})?\$",
            '2digitforce' => "^\d+\,\d\d\$",
            'anything' => "^[\d\D]{1,}\$"
    );
    private $validations, $sanatations, $mandatories, $errors, $corrects, $fields;


    public function __construct($validations=array(), $mandatories = array(), $sanatations = array())
    {
        $this->validations = $validations;
        $this->sanatations = $sanatations;
        $this->mandatories = $mandatories;
        $this->errors = array();
        $this->corrects = array();
    }

    /**
     * Validates an array of items (if needed) and returns true or false
     *
     */
    public function validate($items)
    {
        $this->fields = $items;
        $havefailures = false;
        foreach($items as $key=>$val)
        {
            if((strlen($val) == 0 || array_search($key, $this->validations) === false) && array_search($key, $this->mandatories) === false) 
            {
                $this->corrects[] = $key;
                continue;
            }
            $result = self::validateItem($val, $this->validations[$key]);
            if($result === false) {
                $havefailures = true;
                $this->addError($key, $this->validations[$key]);
            }
            else
            {
                $this->corrects[] = $key;
            }
        }

        return(!$havefailures);
    }

    /**
     *
     *  Adds unvalidated class to thos elements that are not validated. Removes them from classes that are.
     */
    public function getScript() {
        if(!empty($this->errors))
        {
            $errors = array();
            foreach($this->errors as $key=>$val) { $errors[] = "'INPUT[name={$key}]'"; }

            $output = '$$('.implode(',', $errors).').addClass("unvalidated");'; 
            $output .= "new FormValidator().showMessage();";
        }
        if(!empty($this->corrects))
        {
            $corrects = array();
            foreach($this->corrects as $key) { $corrects[] = "'INPUT[name={$key}]'"; }
            $output .= '$$('.implode(',', $corrects).').removeClass("unvalidated");';   
        }
        $output = "<script type='text/javascript'>{$output} </script>";
        return($output);
    }


    /**
     *
     * Sanatizes an array of items according to the $this->sanatations
     * sanatations will be standard of type string, but can also be specified.
     * For ease of use, this syntax is accepted:
     * $sanatations = array('fieldname', 'otherfieldname'=>'float');
     */
    public function sanatize($items)
    {
        foreach($items as $key=>$val)
        {
            if(array_search($key, $this->sanatations) === false && !array_key_exists($key, $this->sanatations)) continue;
            $items[$key] = self::sanatizeItem($val, $this->validations[$key]);
        }
        return($items);
    }


    /**
     *
     * Adds an error to the errors array.
     */ 
    private function addError($field, $type='string')
    {
        $this->errors[$field] = $type;
    }

    /**
     *
     * Sanatize a single var according to $type.
     * Allows for static calling to allow simple sanatization
     */
    public static function sanatizeItem($var, $type)
    {
        $flags = NULL;
        switch($type)
        {
            case 'url':
                $filter = FILTER_SANITIZE_URL;
            break;
            case 'int':
                $filter = FILTER_SANITIZE_NUMBER_INT;
            break;
            case 'float':
                $filter = FILTER_SANITIZE_NUMBER_FLOAT;
                $flags = FILTER_FLAG_ALLOW_FRACTION | FILTER_FLAG_ALLOW_THOUSAND;
            break;
            case 'email':
                $var = substr($var, 0, 254);
                $filter = FILTER_SANITIZE_EMAIL;
            break;
            case 'string':
            default:
                $filter = FILTER_SANITIZE_STRING;
                $flags = FILTER_FLAG_NO_ENCODE_QUOTES;
            break;

        }
        $output = filter_var($var, $filter, $flags);        
        return($output);
    }

    /** 
     *
     * Validates a single var according to $type.
     * Allows for static calling to allow simple validation.
     *
     */
    public static function validateItem($var, $type)
    {
        if(array_key_exists($type, self::$regexes))
        {
            $returnval =  filter_var($var, FILTER_VALIDATE_REGEXP, array("options"=> array("regexp"=>'!'.self::$regexes[$type].'!i'))) !== false;
            return($returnval);
        }
        $filter = false;
        switch($type)
        {
            case 'email':
                $var = substr($var, 0, 254);
                $filter = FILTER_VALIDATE_EMAIL;    
            break;
            case 'int':
                $filter = FILTER_VALIDATE_INT;
            break;
            case 'boolean':
                $filter = FILTER_VALIDATE_BOOLEAN;
            break;
            case 'ip':
                $filter = FILTER_VALIDATE_IP;
            break;
            case 'url':
                $filter = FILTER_VALIDATE_URL;
            break;
        }
        return ($filter === false) ? false : filter_var($var, $filter) !== false ? true : false;
    }       



}

Конечно, имейте в виду, что вам также нужно выполнять экранирование SQL-запросов в зависимости от того, какой тип БД вы используете (mysql_real_escape_string() бесполезен, например, для SQL-сервера). Возможно, вы захотите обработать это автоматически на соответствующем прикладном уровне, например, ORM. Также, как упоминалось выше: для вывода в html используйте другие специальные функции php, такие как htmlspecialchars;)

Для того, чтобы действительно разрешить ввод HTML с подобными раздетыми классами и / или тегами, используйте один из выделенных пакетов проверки xss. НЕ ПИШИТЕ СВОИ СОБСТВЕННЫЕ РЕКЕКСЫ ДЛЯ РАЗБОРКИ HTML!

Нет, нет

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

Основные правила большого пальца

  • Для SQL-запроса связывайте параметры (как с PDO) или используйте встроенную в драйвер функцию экранирования для переменных запроса (таких как mysql_real_escape_string())
  • использование strip_tags() отфильтровать нежелательный HTML
  • Избегайте всех других выходов с htmlspecialchars() и помните о 2-м и 3-м параметрах здесь.

Чтобы решить проблему XSS, взгляните на HTML Purifier. Это довольно настраиваемый и имеет достойный послужной список.

Что касается атак с использованием SQL-инъекций, убедитесь, что вы проверили ввод пользователя, а затем запустили его, хотя mysql_real_escape_string(). Однако эта функция не победит все атаки с использованием инъекций, поэтому важно проверить данные перед тем, как вывести их в строку запроса.

Лучшее решение - использовать подготовленные заявления. Библиотека PDO и расширение mysqli поддерживают их.

В PHP 5.2 появилась функция filter_var.

Он поддерживает множество фильтров SANITIZE, VALIDATE.

http://php.net/manual/en/function.filter-var.php

Методы для очистки пользовательского ввода с помощью PHP:

  • Используйте современные версии MySQL и PHP.

  • Установите кодировку явно:

    •  $ Mysqli->set_charset("utf8"); 
      руководство
    •  $ pdo = новый PDO('mysql:host=localhost;dbname=testdb;charset=UTF8', $user, $password); 
      руководство
    •  $pdo->exec("set names utf8"); 
      руководство
    •  $ pdo = новый PDO(
      "mysql:host=$host;dbname=$db", $user, $pass, 
      массив (
      PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
      PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES utf8")); 
      руководство
    •  mysql_set_charset ('utf8') 
      [устарело в PHP 5.5.0, удалено в PHP 7.0.0].
  • Используйте безопасные кодировки:

    • Выберите utf8, latin1, ascii.., не используйте уязвимые наборы символов big5, cp932, gb2312, gbk, sjis.
  • Используйте пространственную функцию:

    • MySQLi подготовил заявления:
       $ stmt = $ mysqli-> prepare ('SELECT * FROM test WHERE name =? LIMIT 1'); 
      $ param = "'OR 1 = 1 / *";
      $ stmt-> bind_param ('s', $ param);
      $ Stmt-> Execute ();
    • PDO:: quote () - помещает кавычки вокруг входной строки (если требуется) и экранирует специальные символы во входной строке, используя стиль цитирования, соответствующий базовому драйверу:

       $ pdo = новый PDO('mysql:host=localhost;dbname=testdb;charset=UTF8', $user, $password); явный набор символов 
      $pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false); отключите эмуляцию подготовленных операторов, чтобы предотвратить переход к эмуляции операторов, которые MySQL не может подготовить изначально (для предотвращения внедрения)
      $ var = $ pdo-> quote ("'OR 1 = 1 / *"); не только экранирует литерал, но и заключает его в кавычки (в одинарных кавычках) $ stmt = $ pdo-> query ("SELECT * FROM test WHERE name = $ var LIMIT 1");

    • Подготовленные операторы PDO: против подготовленных MySQLi операторов поддерживается больше драйверов базы данных и именованных параметров:

       $ pdo = новый PDO('mysql:host=localhost;dbname=testdb;charset=UTF8', $user, $password); явный набор символов 
      $pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false); отключите эмуляцию подготовленных операторов, чтобы предотвратить переход к эмуляции операторов, которые MySQL не может подготовить изначально (для предотвращения внедрения) $ stmt = $ pdo-> prepare ('SELECT * FROM test WHERE name =? LIMIT 1'); $ stmt-> execute (["'OR 1 = 1 / *"]);

    • mysql_real_escape_string [устарело в PHP 5.5.0, удалено в PHP 7.0.0].
    • mysqli_real_escape_string Экранирует специальные символы в строке для использования в операторе SQL с учетом текущей кодировки соединения. Но рекомендуется использовать подготовленные операторы, поскольку они не являются просто экранированными строками, оператор предлагает полный план выполнения запроса, включая таблицы и индексы, которые он будет использовать, это оптимизированный способ.
    • Используйте одинарные кавычки (' ') вокруг переменных внутри вашего запроса.
  • Проверьте, что переменная содержит то, что вы ожидаете:

    • Если вы ожидаете целое число, используйте:
       ctype_digit - проверять числовые символы; 
      $ value = (int) $ value;
      $ value = intval ($ value);
      $ var = filter_var ('0755', FILTER_VALIDATE_INT, $ options);
    • Для строк используйте:
       is_string () - определяет, является ли тип переменной строковым 

      Использовать функцию фильтра filter_var () - фильтрует переменную с указанным фильтром:
       $ email = filter_var ($ email, FILTER_SANITIZE_EMAIL); 
      $ newstr = filter_var ($ str, FILTER_SANITIZE_STRING);
      более предопределенные фильтры
    • filter_input () - получает определенную внешнюю переменную по имени и дополнительно фильтрует ее:
       $ search_html = filter_input (INPUT_GET, 'search', FILTER_SANITIZE_SPECIAL_CHARS); 
    • preg_match () - выполняет сопоставление с регулярным выражением;
    • Напишите свою собственную функцию проверки.

Там нет функции catchall, потому что есть несколько проблем, которые необходимо решить.

  1. SQL-инъекция. Сегодня, как правило, каждый PHP-проект должен использовать подготовленные операторы через PHP Data Objects (PDO) в качестве лучшей практики, предотвращающей ошибку из-за случайных цитат, а также полнофункциональное решение для внедрения. Это также самый гибкий и безопасный способ доступа к вашей базе данных.

    Прочтите (Единственное правильное) руководство по PDO, чтобы узнать почти все, что вам нужно знать о PDO. (Искренне благодарим ведущего SO SO, @YourCommonSense, за этот замечательный ресурс по этой теме.)

  2. XSS - очистить данные на пути в...

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

    • В других случаях, когда мы вообще не хотим принимать HTML/Javascript, я обнаружил, что эта простая функция полезна (и прошла множество проверок XSS):

      /* Prevent XSS input */ function sanitizeXSS () { $_GET = filter_input_array(INPUT_GET, FILTER_SANITIZE_STRING); $_POST = filter_input_array(INPUT_POST, FILTER_SANITIZE_STRING); $_REQUEST = (array)$_POST + (array)$_GET + (array)$_REQUEST; }

  3. XSS - Санитарная обработка данных на выходе... если вы не гарантируете, что данные были должным образом очищены, прежде чем добавлять их в базу данных, вам нужно будет санировать их перед отображением для пользователя, мы можем использовать эти полезные функции PHP:

    • Когда вы звоните echo или же print для отображения предоставленных пользователем значений используйте htmlspecialchars если данные не были должным образом очищены безопасным и разрешено отображать HTML.
    • json_encode это безопасный способ предоставления пользовательских значений из PHP в Javascript
  4. Вы вызываете команды внешней оболочки, используя exec() или же system() функции, или к backtick оператор? Если это так, в дополнение к SQL-инъекциям и XSS у вас может возникнуть дополнительная проблема для пользователей, запускающих вредоносные команды на вашем сервере. Вам нужно использовать escapeshellcmd если вы хотите избежать всей команды ИЛИ escapeshellarg избежать индивидуальных аргументов.

Один трюк, который может помочь в определенных обстоятельствах, когда у вас есть страница, как /mypage?id=53 и вы используете идентификатор в предложении WHERE, чтобы убедиться, что идентификатор определенно является целым числом, например так:

if (isset($_GET['id'])) {
  $id = $_GET['id'];
  settype($id, 'integer');
  $result = mysql_query("SELECT * FROM mytable WHERE id = '$id'");
  # now use the result
}

Но, конечно, это исключает только одну конкретную атаку, поэтому прочитайте все остальные ответы. (И да, я знаю, что приведенный выше код не очень хорош, но он показывает конкретную защиту.)

То, что вы описываете здесь, это две отдельные проблемы:

  1. Санитарная обработка / фильтрация пользовательских данных.
  2. Выходящий выход.

1) Пользовательский ввод всегда следует считать плохим.

Использование подготовленных операторов или / и фильтрация с помощью mysql_real_escape_string, безусловно, является обязательным. PHP также имеет встроенный фильтр filter_input, с которого можно начать.

2) Это большая тема, и она зависит от контекста выводимых данных. Для HTML существуют такие решения, как htmlpurifier. как правило, всегда избегайте всего, что вы выводите.

Обе проблемы слишком велики, чтобы их можно было рассмотреть в одном посте, но есть много постов, в которых более подробно рассматриваются:

Методы вывода PHP

Более безопасный вывод PHP

Если вы используете PostgreSQL, ввод из PHP может быть экранирован с помощью pg_escape_string()

 $username = pg_escape_string($_POST['username']);

Из документации ( http://php.net/manual/es/function.pg-escape-string.php):

pg_escape_string() экранирует строку для запроса к базе данных. Возвращает экранированную строку в формате PostgreSQL без кавычек.

Вы никогда не дезинфицируете ввод.

Вы всегда дезинфицируете продукцию.

Преобразования, которые вы применяете к данным, чтобы сделать их безопасными для включения в оператор SQL, полностью отличаются от тех, которые вы применяете для включения в HTML, полностью отличаются от тех, которые вы применяете для включения в Javascript, и полностью отличаются от тех, которые вы применяете для включения в LDIF. полностью отличается от тех, которые вы применяете для включения в CSS, полностью отличается от тех, которые вы применяете для включения в электронную почту....

Обязательно проверяйте ввод - решите, хотите ли вы принять его для дальнейшей обработки или скажите пользователю, что это недопустимо. Но не применяйте никаких изменений к представлению данных, пока они не покинут землю PHP.

Давным-давно кто-то пытался изобрести универсальный механизм для экранирования всех данных, и мы в итоге получили " magic_quotes ", который неправильно экранировал данные для всех выходных целей и привел к разной установке, требующей разного кода для работы.

Самый простой способ избежать ошибок при очистке входных данных и экранировании данных - использовать PHP-фреймворк, такой как Symfony, Nette и т. Д., Или его часть (шаблонизатор, слой базы данных, ORM).

У движка шаблонов, такого как Twig или Latte, по умолчанию экранирование включено - вам не нужно решать вручную, если вы правильно экранировали свой вывод в зависимости от контекста (HTML или Javascript часть веб-страницы).

Framework автоматически очищает входные данные, и вы не должны использовать переменные $_POST, $_GET или $_SESSION напрямую, а через такой механизм, как маршрутизация, обработка сеанса и т. Д.

А для слоя базы данных (модели) существуют платформы ORM, такие как Doctrine, или оболочки вокруг PDO, такие как Nette Database.

Вы можете прочитать больше об этом здесь - Что такое программный каркас?

Просто хочу добавить, что по теме экранирования вывода, если вы используете php DOMDocument для вывода html-кода, он автоматически выйдет в нужном контексте. Атрибут (value="") и внутренний текст не равны. Чтобы быть в безопасности от XSS, прочтите это: Шпаргалка по профилактике OWASP XSS

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

Фильтры PHP могут удобно очищать и проверять внешний ввод.

Никогда не доверяйте пользовательским данным.

function clean_input($data) {
  $data = trim($data);
  $data = stripslashes($data);
  $data = htmlspecialchars($data);
  return $data;
}

trim() Функция удаляет пробелы и другие предопределенные символы с обеих сторон строки.

stripslashes() функция убирает обратную косую черту

htmlspecialchars() Функция конвертирует некоторые предопределенные символы в HTML-объекты.

Предопределенные символы:

& (ampersand) becomes &amp;
" (double quote) becomes &quot;
' (single quote) becomes &#039;
< (less than) becomes &lt;
> (greater than) becomes &gt;

Используйте это обрезание пустого пространства и удаление непечатаемого символа

      $data = trim(preg_replace('/[[:^print:]]/', '', $data));

Существует расширение фильтра ( howto-link, manual), которое работает очень хорошо со всеми переменными GPC. Это не волшебство делай все, хотя, тебе все равно придется его использовать.

Лучший базовый метод для очистки ввода пользователя с помощью PHP:


    function sanitizeString($var)
    {
        $var = stripslashes($var);
        $var = strip_tags($var);
        $var = htmlentities($var);
        return $var;
    }

    function sanitizeMySQL($connection, $var)
    {
        $var = $connection->real_escape_string($var);
        $var = sanitizeString($var);
        return $var;
    }
Другие вопросы по тегам