题 使用PHP清理用户输入的最佳方法是什么?


是否有一个catchall函数适用于清理SQL注入和XSS攻击的用户输入,同时仍允许某些类型的html标记?


975
2017-09-24 20:20


起源


如今,为避免sql注入,请使用PDO或MySQLi。 - Francisco Presencia
使用PDO或MySQLi是不够的。如果使用不受信任的数据构建S​​QL语句,例如 select * from users where name='$name',那么使用PDO或MySQLi或MySQL并不重要。你还处于危险之中。您必须使用参数化查询,或者,如果必须,您必须对数据使用转义机制,但这不太可取。 - Andy Lester
@AndyLester你暗示有人在没有准备好的陈述的情况下使用PDO吗? :)
我说“使用PDO或MySQLi”不足以向新手解释如何安全地使用它们。你和我都知道准备好的陈述很重要,但我不认为读这个问题的每个人都会知道。这就是我添加明确指示的原因。 - Andy Lester
安迪的评论完全有效。我最近将我的mysql网站转换为PDO,认为我现在在某种程度上可以安全地免受注入攻击。只是在这个过程中我意识到我的一些sql语句仍然是使用用户输入构建的。然后我使用预处理语句修复了它。对于一个完整的新手来说,并不完全清楚存在区别,因为许多专家抛弃了关于​​使用PDO的评论,但没有说明是否需要准备好的语句。假设这很明显。但不是新手。 - GhostRider


答案:


这是一种常见的误解,即用户输入可以被过滤。 PHP甚至有一个(现已弃用的)“功能”,称为魔术引号,它建立在这个想法的基础之上。这是无稽之谈。忘记过滤(或清洁,或任何人称之为)。

为了避免出现问题,你应该做的很简单:每当你在国外代码中嵌入一个字符串时,你必须根据该语言的规则来逃避它。例如,如果在某些SQL目标MySql中嵌入字符串,则必须为此目的使用MySql函数转义字符串(mysqli_real_escape_string)。 (或者,对于数据库,如果可能,使用预准备语句是更好的方法)

另一个例子是HTML:如果在HTML标记中嵌入字符串,则必须使用它进行转义 htmlspecialchars。这意味着每一个 echo 要么 print 声明应该使用 htmlspecialchars

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

等等等等 ...

只要 如果您正在接受预先格式化的输入,则需要主动过滤数据的情况。例如。如果您允许用户发布HTML标记,那么您计划在网站上显示。但是,你应该不惜一切代价避免这种情况,因为无论你如何过滤它,它都将是一个潜在的安全漏洞。


1079
2017-09-24 22:30



“这意味着每个echo或print语句都应该使用htmlspecialchars” - 当然,你的意思是“每个...语句输出用户输入”; htmlspecialchars() - ifying“echo'Hello,world!';”会疯了;) - Bobby Jack
优秀简洁的答案!当我听到消毒输入而不考虑背景时,我感到畏缩。 - Cory House
有一种情况我认为过滤是正确的解决方案:UTF-8。您不希望在整个应用程序中使用无效的UTF-8序列(根据代码路径,您可能会获得不同的错误恢复),并且可以轻松过滤(或拒绝)UTF-8。 - Kornel
@porneL:是的,此时过滤掉换行以外的控制字符也是值得的。然而,鉴于大多数PHP应用程序甚至无法获得HTML转义,但我不打算推​​出过长的UTF-8序列问题(它们只是IE6 pre-Service-Pack-2和旧版本中的一个问题。歌剧)。 - bobince
@jbyrd - 不,LIKE使用专门的正则表达式语言。您必须两次转义输入字符串 - 一次用于regexp,一次用于mysql字符串编码。它是代码中代码内的代码。 - troelskn


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

代替, 不允许在创建SQL代码时使用数据。使用使用绑定变量的Prepared Statements(即使用模板查询中的参数)。这是防止SQL注入的唯一方法。

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


184
2017-10-09 06:28



或者访问 官方文件 并学习PDO和准备好的陈述。微小的学习曲线,但如果你很熟悉SQL,你就可以轻松适应。 - a coder
对于SQL注入的特定情况, 这个 是正确的答案! - Scott Arciszewski
请注意,预准备语句不会添加任何安全性,参数化查询会这样做。它们碰巧很容易在PHP中一起使用。 - Basic
它不是唯一有保障的方式。 Hex中的输入和unhex在查询中也会阻止。如果您正确使用六边形,则无法进行十六进制攻击。 - Ramon Bakker
如果您输入的是专门的内容,例如电子邮件地址或用户名,该怎么办? - Black_Stormy


不可以。如果没有任何上下文,您无法对数据进行一般过滤。有时您希望将SQL查询作为输入,有时您希望将HTML作为输入。

您需要过滤白名单上的输入 - 确保数据符合您期望的某些规范。然后,您需要在使用它之前将其转义,具体取决于您使用它的上下文。

转义SQL数据的过程 - 防止SQL注入 - 与转换(X)HTML数据的过程非常不同,以防止XSS。


72
2017-09-24 20:24



+1“你不能在没有任何上下文的情况下一般过滤数据。”应该经常重复。 - Ben


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 Server无用)。您可能希望在适当的应用程序层(如ORM)自动处理此问题。另外,如上所述:输出到html使用其他php专用函数,如htmlspecialchars;)

对于真正允许带有类似剥离类和/或标记的HTML输入,取决于其中一个专用的xss验证包。不要将自己的规则写入PARSE HTML!


44
2017-09-24 23:12



这看起来像是一个用于验证输入的方便脚本,但确实如此 全然 与问题无关。 - rjmunro
S / sanatize /消毒/克; - Brock Hensley
S /消毒/消毒/克; - Christian
茶和烤饼?消毒:消毒 - ch1pn3ss


不,那里没有。

首先,SQL注入是一个输入过滤问题,XSS是一个输出转义问题 - 所以你甚至不能在代码生命周期中同时执行这两个操作。

基本的经验法则

  • 对于SQL查询,绑定参数(与PDO一样)或对查询变量使用驱动程序本机转义函数(例如 mysql_real_escape_string()
  • 使用 strip_tags() 过滤掉不需要的HTML
  • 用它来逃避所有其他输出 htmlspecialchars() 并注意这里的第二和第三参数。

39
2017-09-24 20:30



因此,当您知道输入包含要分别删除或转义的HTML时,您只使用strip_tags()或htmlspecialchars() - 您是不是出于任何安全目的使用它?另外,当你进行绑定时,它对Bobby Tables这样的东西有什么作用? “Robert'); DROP TABLE学生; - ”它是否只是逃避了引号? - Robert Mark Bram
如果您的用户数据将进入数据库并稍后显示在网页上,那么它的阅读通常不会比编写的多得多吗?对我来说,在存储它之前过滤它一次(作为输入)更有意义,而不是每次显示它时都必须过滤它。我是否遗漏了一些东西,或者是否有很多人投票支持这个和接受的答案中的不必要的性能开销? - jbo5112
对我来说最好的答案。它很简短,如果你问我,我会很好地解决这个问题。是否有可能通过$ _POST或$ _GET以某种方式攻击PHP,或者这是不可能的? - Jo Smo


要解决XSS问题,请查看 HTML净化器。它相当可配置,并具有良好的跟踪记录。

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

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


20
2017-09-24 20:29



没有“最好的方法”来做一些像清理输入的东西..使用一些库,html净化器是好的。这些图书馆多次受到抨击。所以它比你自己出现的任何东西都更加防弹 - paan
也可以看看 bioinformatics.org/phplabware/internal_utilities/htmLawed 。从我的理解,WordPress使用旧版本, core.trac.wordpress.org/browser/tags/2.9.2/wp-includes/kses.php - Steve Clay


PHP 5.2引入了 filter_var 功能。

它支持大量的SANITIZE,VALIDATE过滤器。

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


18
2017-10-15 08:40





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

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

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


15
2018-03-08 23:14



我使用$ id = intval($ id)代替:) - Silentbang
转换整数是确保仅插入数字数据的好方法。 - nn2
$id = (int)$_GET['id'] 和 $que = sprintf('SELECT ... WHERE id="%d"', $id) 也很好 - vladkras