关于安全性:用PHP清理用户输入的最佳方法是什么?

What's the best method for sanitizing user input with PHP?

是否有一个catchall函数可以很好地清除用户输入中的SQL注入和XSS攻击,同时仍然允许某些类型的HTML标记?


人们普遍误解用户输入可以被过滤。PHP甚至有一个(现在已被弃用的)"特性",称为magic quotes,它建立在这个想法的基础上。这是胡说八道。忘记过滤(或者清洁,或者人们称之为过滤)。

为了避免出现问题,您应该做的是非常简单的:每当您在外部代码中嵌入一个字符串时,您必须根据该语言的规则对其进行转义。例如,如果您在一些针对MySQL的SQL中嵌入了一个字符串,那么就必须使用MySQL的函数来转义该字符串(mysqli_real_escape_string)。(或者,对于数据库,如果可能,使用准备好的语句是更好的方法)

另一个例子是HTML:如果您在HTML标记中嵌入字符串,则必须使用htmlspecialchars对其进行转义。这意味着每一个echoprint语句都应该使用htmlspecialchars

第三个例子可能是shell命令:如果要将字符串(如参数)嵌入到外部命令中,并使用exec调用它们,则必须使用escapeshellcmdescapeshellarg

等等……

唯一需要主动过滤数据的情况是,如果您接受预先格式化的输入。如果你让你的用户发布HTML标记,你计划在网站上显示。然而,您应该明智地不惜一切代价避免这种情况,因为无论您过滤得多么好,它始终是一个潜在的安全漏洞。


不要试图通过清理输入数据来阻止SQL注入。

相反,不允许在创建SQL代码时使用数据。使用使用绑定变量的准备好的语句(即在模板查询中使用参数)。这是保证不受SQL注入影响的唯一方法。

有关防止SQL注入的更多信息,请访问我的网站http://bobby-tables.com/。


不,如果没有任何上下文,就不能对数据进行一般性的筛选。有时您希望将SQL查询作为输入,有时您希望将HTML作为输入。

您需要过滤白名单上的输入——确保数据符合您所期望的某种规范。然后您需要在使用它之前对其进行转义,这取决于您使用它的上下文。

为SQL转义数据的过程(防止SQL注入)与为(x)HTML转义数据以防止XSS的过程非常不同。


php现在有了新的nice filter_输入函数,例如,现在有了内置的filter_validate_email类型,就可以让您从查找"终极电子邮件regex"中解放出来。

我自己的过滤器类(使用javascript突出显示错误字段)可以通过Ajax请求或普通表单发布启动。(见下例)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
/**
 *  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');");
 *  }
 *  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('blah', '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}";
        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净化器。它是相当可配置的,并且具有良好的跟踪记录。

至于SQL注入攻击,请确保检查用户输入,然后通过mysql_real_escape_string()运行它。不过,该函数不能抵御所有注入攻击,因此在将数据转储到查询字符串之前检查数据是很重要的。

更好的解决方案是使用准备好的语句。PDO库和mysqli扩展支持这些。


php 5.2引入了filter_var函数。

它支持大量的消毒、验证过滤器。

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


一个技巧可以帮助您在特定情况下使用像/mypage?id=53这样的页面,并在where子句中使用id,以确保id绝对是整数,如下所示:

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

当然,这只会减少一次攻击,所以请阅读所有其他答案。(是的,我知道上面的代码不是很好,但它显示了具体的防御。)


Methods for sanitizing user input with PHP:

  • 使用现代版本的mysql和php。
  • 显式设置字符集:
    • 1
      $mysqli->set_charset("utf8");

      手册

    • 1
      $pdo = new PDO('mysql:host=localhost;dbname=testdb;charset=UTF8', $user, $password);

      手册

    • 1
      $pdo->exec("set names utf8");

      手册

    • 1
      2
      3
      4
      5
      6
      7
      $pdo = new PDO(
      "mysql:host=$host;dbname=$db", $user, $pass,
      array(
      PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
      PDO::MYSQL_ATTR_INIT_COMMAND =>"SET NAMES utf8"
      )
      );

      手册

    • 1
      <s>mysql_set_charset('utf8')</s>

      [在php 5.5.0中已弃用,在php 7.0.0中已删除]。

  • 使用安全字符集:
    • 选择utf8、latin1、ascii..,不要使用易受攻击的字符集big5、cp932、gb2312、gbk、sjis。
  • 使用空间化功能:
    • mysqli准备的声明:
      1
      $stmt = $mysqli->prepare('SELECT * FROM test WHERE name = ? LIMIT 1'); $param ="' OR 1=1 /*";$stmt->bind_param('s', $param);$stmt->execute();
    • pdo::quote()-将引号放在输入字符串周围(如果需要),并在输入字符串中转义特殊字符,使用适合于底层驱动程序的引用样式:

      1
      2
      $pdo = new PDO('mysql:host=localhost;dbname=testdb;charset=UTF8', $user, $password);explicit set the character set$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);disable emulating prepared statements to prevent  fallback to emulating statements that MySQL can't prepare natively (to prevent injection)$var  = $pdo->quote("' OR 1=1 /*");not only escapes the literal, but also quotes it (in single-quote ' characters)
      $stmt = $pdo->query("SELECT * FROM test WHERE name = $var LIMIT 1");

    • pdo-prepared语句:vs mysqli-prepared语句支持更多的数据库驱动程序和命名参数:

      1
      2
      3
      $pdo = new PDO('mysql:host=localhost;dbname=testdb;charset=UTF8', $user, $password);explicit set the character set$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);disable emulating prepared statements to prevent  fallback to emulating statements that MySQL can't prepare natively (to prevent injection)
      $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语句的字符串中的特殊字符进行转义。但建议使用准备好的语句,因为它们不是简单的转义字符串,语句会给出一个完整的查询执行计划,包括它将使用哪些表和索引,这是一种优化的方法。
    • 在查询中的变量周围使用单引号("")。
  • 检查变量是否包含您期望的内容:
    • 如果需要整数,请使用:
      1
      ctype_digit — Check for numeric character(s);$value = (int) $value;$value = intval($value);$var = filter_var('0755', FILTER_VALIDATE_INT, $options);
    • 对于字符串,请使用:
      1
      is_string() — Find whether the type of a variable is string

      使用filter函数filter_var()-用指定的过滤器过滤变量:

      1
      $email = filter_var($email, FILTER_SANITIZE_EMAIL);$newstr = filter_var($str, FILTER_SANITIZE_STRING);

      更多的预定义过滤器

    • filter_input()-按名称获取特定的外部变量,并可选地对其进行筛选:
      1
      $search_html = filter_input(INPUT_GET, 'search', FILTER_SANITIZE_SPECIAL_CHARS);

    • preg_match()-执行正则表达式匹配;
    • 编写自己的验证函数。

您在这里描述的是两个独立的问题:

  • 清理/过滤用户输入数据。
  • 正在转义输出。
  • 1)应始终假设用户输入错误。

    使用准备好的语句,或者/和用mysql_real_escape_字符串过滤,绝对是必须的。PHP还内置了过滤器输入,这是一个很好的开始。

    2)这是一个大主题,取决于所输出数据的上下文。对于HTML,有一些解决方案,比如HTML净化器。作为经验法则,总是逃避任何输出。

    这两个问题都太大了,无法在一个帖子中讨论,但有很多帖子更详细:

    方法PHP输出

    更安全的PHP输出


    如果您使用的是PostgreSQL,则可以使用pg_escape_string()对来自php的输入进行转义。

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

    从文档(http://php.net/manual/es/function.pg escape string.php)中:

    pg_escape_string() escapes a string for querying the database. It returns an escaped string in the PostgreSQL format without quotes.


    避免清理输入和转义数据错误的最简单方法是使用PHP框架(如symfony、nette等)或该框架的一部分(模板引擎、数据库层、ORM)。

    模板引擎(如twig或latte)在默认情况下有输出转义功能——如果您根据上下文(网页的HTML或javascript部分)正确转义了输出,则无需手动解决。

    框架正在自动清理输入,您不应该直接使用$u post、$u get或$u会话变量,而是通过路由、会话处理等机制。

    对于数据库(模型)层,有ORM框架,如条令或包装器,围绕PDO,如nette数据库。

    您可以在这里了解更多信息-什么是软件框架?


    没有catchall函数,因为有多个问题需要解决。

  • SQL注入——现在,通常情况下,每个PHP项目都应该使用通过PHP数据对象(PDO)准备好的语句作为最佳实践,以防止误报以及针对注入的全功能解决方案产生错误。这也是访问数据库最灵活和安全的方式。

    查看(唯一合适的)PDO教程,了解关于PDO的几乎所有信息。(真诚地感谢顶尖的SO贡献者@yourcommonsense,为这个主题提供了大量的资源。)

  • XSS-在传入过程中清理数据…

    • HTML净化器已经存在很长时间了,并且仍在积极更新。您可以使用它来清除恶意输入,同时仍然允许使用大量可配置的标记白名单。与许多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;
      }

  • XSS-在退出时清理数据…除非您保证在将数据添加到数据库之前已对其进行了正确的清理,否则您需要在向用户显示数据之前对其进行清理,我们可以利用这些有用的PHP函数:

    • 当您调用echoprint来显示用户提供的值时,请使用htmlspecialchars,除非数据经过适当的安全处理并允许显示HTML。
    • json_encode是一种安全的方法,可以提供从php到javascript的用户提供的值。
  • 使用exec()system()函数调用外部shell命令,还是调用backtick运算符?如果是这样,除了SQL注入和XSS之外,您可能还有其他需要解决的问题,即服务器上运行恶意命令的用户。如果您想退出整个命令,则需要使用escapeshellcmd,或者使用escapeshellarg来退出单个参数。


  • 只是想在输出转义的主题上增加一点,如果使用php domdocument来生成HTML输出,它将在正确的上下文中自动转义。属性(value=")和a的内部文本不相等。要安全防范XSS,请阅读以下内容:OWASP XSS预防备忘表


    您从不清理输入。你总是消毒输出。

    应用于数据以使其安全包含在SQL语句中的转换与应用于HTML中的转换完全不同于应用于javascript中的转换与应用于ldif中的转换完全不同于应用于inclusi的转换。CSS中的on与电子邮件中包含的内容完全不同….

    无论如何,验证输入——决定您是应该接受它进行进一步的处理,还是告诉用户它是不可接受的。但在数据即将离开php land之前,不要对数据的表示进行任何更改。

    很久以前,有人试图发明一种一刀切的数据转义机制,结果我们得到了"magic_quotes",它没有正确转义所有输出目标的数据,导致了不同的安装需要不同的代码来工作。


    从不信任用户数据。

    1
    2
    3
    4
    5
    6
    function&nbsp;clean_input($data) {
    &nbsp;&nbsp;$data = trim($data);
    &nbsp;&nbsp;$data = stripslashes($data);
    &nbsp;&nbsp;$data = htmlspecialchars($data);
    &nbsp;&nbsp;return&nbsp;$data;
    }

    trim()函数从字符串两边删除空白和其他预定义字符。

    stripslashes()函数删除反斜杠

    htmlspecialchars()函数将一些预定义字符转换为HTML实体。

    预定义字符包括:

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


    有过滤器扩展(howto-link,manual),它可以很好地处理所有GPC变量。这不是一个魔术,尽管做所有的事情,你仍然必须使用它。


    使用PHP清理用户输入的最佳基本方法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
        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;
        }