如何使用 PHP 清理用户输入?

是否有某个功能全面的功能可以很好地用于清理用户针对 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 注入的唯一方法。

请访问我的网站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');
 *  $sanitize = array('alias');
 *
 *  $validator = new FormValidator($validations, $required, $sanitize);
 *                  
 *  if($validator->validate($_POST))
 *  {
 *      $_POST = $validator->sanitize($_POST);
 *      // now do your saving, $_POST has been sanitized.
 *      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 sanitize just one element:
 * $sanitized = new FormValidator()->sanitize('<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->sanitations = $sanitations;
        $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);
    }


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


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

    /**
     *
     * Sanitize a single var according to $type.
     * Allows for static calling to allow simple sanitization
     */
    public static function sanitizeItem($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 查询(例如,对于 SQL Server,mysql_real_escape_string()是无用的)。您可能希望在适当的应用程序层(如 ORM)自动处理此问题。另外,如上所述:要输出到 html,请使用其他 php 专用功能,例如 htmlspecialchars;)

要真正允许使用类似剥离的类和 / 或标签的 HTML 输入,取决于专用的 xss 验证程序包之一。不要写自己的正则表达式来解析 HTML!

不,那里没有。

首先,SQL 注入是一个输入过滤问题,而 XSS 是一个转义输出的问题 - 因此,您甚至都不会在代码生命周期中同时执行这两个操作。

基本经验法则

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

要解决 XSS 问题,请看HTML Purifier 。它是相当可配置的,并且具有良好的记录。

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

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

PHP 5.2 引入了filter_var函数。

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

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

在特定情况下(例如/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
}

但是,当然,这只会消除一种特定的攻击,因此请阅读所有其他答案。 (是的,我知道上面的代码不是很好,但是它显示了特定的防御。)

使用 PHP 清理用户输入的方法:

  • 使用 MySQL 和 PHP 的现代版本。

  • 显式设置字符集:

    • $mysqli->set_charset("utf8");
      手册
    • $pdo = new PDO('mysql:host=localhost;dbname=testdb;charset=UTF8', $user, $password);
      手册
    • $pdo->exec("set names utf8");
      手册
    • $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"
      )
      );
      手册
    • 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 = 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 准备语句 :vs MySQLi 准备语句支持更多的数据库驱动程序和命名参数:

      $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 语句使用。但是建议使用 Prepared Statements,因为它们不是简单的转义字符串,它提供了完整的查询执行计划,包括它将使用的表和索引,这是一种优化的方式。
    • 在查询中的变量周围使用单引号('')。
  • 检查变量是否包含您的期望:

    • 如果期望整数,请使用:
      ctype_digit — Check for numeric character(s);
      $value = (int) $value;
      $value = intval($value);
      $var = filter_var('0755', FILTER_VALIDATE_INT, $options);
    • 对于字符串,请使用:
      is_string() — Find whether the type of a variable is string

      Use Filter Function 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() —执行正则表达式匹配;
    • 编写您自己的验证功能。

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

  1. 消毒 / 过滤用户输入数据。
  2. 转义输出。

1)用户输入总是被认为是错误的。

绝对必须使用准备好的语句,或者使用 mysql_real_escape_string 进行过滤。 PHP 还内置了 filter_input,这是一个不错的起点。

2)这是一个很大的主题,它取决于输出数据的上下文。对于 HTML,有诸如 htmlpurifier 之类的解决方案。根据经验,始终不要输出任何内容。

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

方法 PHP 输出

更安全的 PHP 输出