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

是否有一个包罗万象的功能可以很好地处理用户输入的SQL注入和XSS攻击,同时仍然允许某些类型的超文本标记语言?

645875 次浏览

不能。您不能在没有任何上下文的情况下过滤数据。有时您希望将SQL查询作为输入,有时您希望将超文本标记语言作为输入。

您需要过滤白名单上的输入——确保数据与您期望的某些规范相匹配。然后,您需要在使用它之前对其进行转义,具体取决于您使用它的上下文。

SQL转义数据的过程-防止SQL注入-与(X)超文本标记语言转义数据的过程非常不同,以防止XSS。

有过滤器扩展(howto-link手册),它可以很好地与所有GPC变量一起使用。不过,这不是什么神奇的事情,您仍然必须使用它。

要解决XSS问题,请查看超文本标记语言净化器。它是相当可配置的,并且具有良好的跟踪记录。

至于SQL注入攻击,解决方案是使用准备好的语句。PDO库和mysqli扩展支持这些。

不,没有。

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

基本的经验法则

  • 对于SQL查询,绑定参数
  • 使用#0过滤掉不需要的超文本标记语言
  • 使用#0转义所有其他输出,并注意此处的第二个和第三个参数。

一个常见的误解是用户输入可以被过滤。PHP甚至有一个(现在不建议使用的)“特性”,称为魔术引号,它建立在这个想法的基础上。这是无稽之谈。忘记过滤(或清理,或任何人们称之为过滤的东西)。

为了避免出现问题,你应该做的事情很简单:每当你在外部代码中嵌入一段数据时,你必须根据该代码的格式规则来处理它。但是你必须明白,这样的规则可能太复杂了,不能试图手动遵循它们。例如,在SQL中,字符串、数字和标识符的规则都是不同的。为了你的方便,在大多数情况下,有一个专门的工具来进行这种嵌入。例如,当你需要在SQL查询中使用PHP变量时,你必须使用准备好的语句,它会负责所有正确的格式/处理。

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

第三个例子可能是shell命令:如果您要将字符串(例如参数)嵌入到外部命令中,并使用#0调用它们,那么您必须使用#1#2

此外,一个非常引人注目的例子是JSON。规则如此众多和复杂,以至于您永远无法手动遵循它们。这就是为什么您永远不应该手动创建JSON字符串,而是始终使用专用函数#0来正确格式化每一位数据。

等等等等…

您需要主动过滤数据的只有情况是接受预格式化输入。例如,如果您让您的用户发布您计划在网站上显示的超文本标记语言标记。但是,您应该明智地不惜一切代价避免这种情况,因为无论您如何过滤,它始终是一个潜在的安全漏洞。

PHP现在有了新的漂亮的#0函数,例如,现在有一个内置的FILTER_VALIDATE_EMAIL类型,它可以让你从寻找“终极电子邮件正则表达式”中解放出来


我自己的过滤器类(使用JavaScript突出显示错误字段)可以由ajax请求或普通表单帖子启动。(参见下面的示例)<?/***猪肉格式验证器。通过正则表达式验证字段并对其进行清理。使用PHPfilter_var内置函数和额外的正则表达式*@包猪肉*/

/***  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查询转义(例如,mysql_real_escape_string()对于sql服务器来说是无用的)。您可能希望在适当的应用程序层像ORM一样自动处理此问题。此外,如上所述:为了输出到html,请使用其他php专用函数,如html特别字符;)

要真正允许超文本标记语言输入,如剥离类和/或标记,取决于专用的xss验证包之一。不要编写自己的REGEXES来解析超文本标记语言!

不要试图通过消毒输入数据来防止SQL注射。

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

请参阅我的网站http://bobby-tables.com/了解更多关于预防SQL注射。

在你有一个像/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}

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

你在这里描述的是两个不同的问题:

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

1)用户输入应始终被假定为不好。

使用准备好的语句,或/和mysql_real_escape_string过滤绝对是必须的。PHP还内置了filter_input,这是一个很好的起点。

2)这是一个很大的主题,它取决于输出数据的上下文。对于超文本标记语言,有一些解决方案,例如html的。根据经验,总是转义你输出的任何内容。

这两个问题都太大了,不能在一篇文章中讨论,但是有很多帖子更详细:

PHP输出方法

更安全的PHP输出

PHP 5.2引入了#0函数。

它支持大量SANITIZEVALIDATE过滤器。

只是想补充一下,关于输出转义的主题,如果您使用php DOMDocument进行html输出,它将在正确的上下文中自动转义。属性(value="")和的内部文本不相等。要对XSS安全阅读:OWASP XSS预防备忘单

如果您使用的是PostgreSQL,则可以使用pg_escape_literal()对PHP的输入进行转义

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

留档

pg_escape_literal()转义用于查询PostgreSQL数据库的文字。它返回PostgreSQL格式的转义文字。

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

模板引擎,如树枝或Latte,默认情况下输出转义-如果您根据上下文(超文本标记语言或网页的Javascript部分)正确转义了输出,则不必手动解决。

框架会自动清理输入,您不应该直接使用$_POST,$_GET或$_SESSION变量,而是通过路由,会话处理等机制。

对于数据库(模型)层,有像Doctrine这样的ORM框架或像Nette Database这样的PDO包装器。

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

没有包罗万象的函数,因为有多个关注点需要解决。

  1. SQL注射-今天,一般来说,每个PHP项目都应该使用通过PHP数据对象(PDO)准备的语句作为最佳实践,防止错误的杂散报价以及针对注入的全功能解决方案。这也是访问数据库的最灵活和最安全的方式。

查看(唯一正确的)PDO教程,了解您需要了解的关于PDO的几乎所有信息。(衷心感谢SO顶级贡献者@YourCommon Sense,提供了关于该主题的大量资源。)

  1. XSS-在进入途中清理数据…
  • 超文本标记语言净化器已经存在了很长时间,并且仍然在积极更新。您可以使用它来净化恶意输入,同时仍然允许大量可配置的标签白名单。适用于许多所见即所得的编辑器,但对于某些用例可能会很重。

  • 在其他情况下,我们根本不想接受超文本标记语言/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;}
  1. XSS-清理数据在出路…除非您保证在将数据添加到数据库之前对其进行了正确的清理,否则您需要在将其显示给用户之前对其进行清理,我们可以利用这些有用的PHP函数:
  • 当您调用echoprint来显示用户提供的值时,请使用#2,除非数据经过适当的安全处理并允许显示超文本标记语言。
  • #0是将用户提供的值从PHP提供到Javascript的安全方法
  1. 你调用外部shell命令使用#0#1函数,或#2运算符?如果是这样,除了SQL注入和XSS之外,您可能还有一个额外的问题需要解决,用户在您的服务器上运行恶意命令。如果您想转义整个命令,您需要使用#3#4来转义单个参数。

你永远不会净化输入。

你总是净化输出。

您应用于数据以使其安全包含在SQL语句中的转换与您申请包含在超文本标记语言中的转换完全不同,与您申请包含在Javascript中的转换完全不同,与您申请包含在LDIF中的转换完全不同,与您申请包含在CSS中的转换完全不同,与您申请包含在电子邮件中的转换完全不同。

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

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

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

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

  • 显式设置字符集:

    • $mysql i->set_charset("utf 8");
      手册
    • $pdo=new PDO('mysql: host=localhost; dbname=testdb; charset=UTF8',$user,$密码);
      手册
    • $pdo->exec("设置名称utf8");
      手册
    • $pdo=new 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('utf 8')
      [在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() - places quotes around the input string (if required) and escapes special characters within the input string, using a quoting style appropriate to the underlying driver:

      $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 Statements: vs MySQLi prepared statements supports more database drivers and named parameters:

      $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 [deprecated in PHP 5.5.0, removed in PHP 7.0.0].
    • mysqli_real_escape_string Escapes special characters in a string for use in an SQL statement, taking into account the current charset of the connection. But recommended to use Prepared Statements because they are not simply escaped strings, a statement comes up with a complete query execution plan, including which tables and indexes it would use, it is a optimized way.
    • Use single quotes (' ') around your variables inside your query.
  • Check the variable contains what you are expecting for:

    • If you are expecting an integer, use:
      ctype_digit — Check for numeric character(s);
      $value = (int) $value;
      $value = intval($value);
      $var = filter_var('0755', FILTER_VALIDATE_INT, $options);
    • For Strings use:
      is_string() — Find whether the type of a variable is string

      Use Filter Function filter_var() — filters a variable with a specified filter:
      $email = filter_var($email, FILTER_SANITIZE_EMAIL);
      $newstr = filter_var($str, FILTER_SANITIZE_STRING);
      more predefined filters
    • filter_input() — Gets a specific external variable by name and optionally filters it:
      $search_html = filter_input(INPUT_GET, 'search', FILTER_SANITIZE_SPECIAL_CHARS);
    • preg_match() — Perform a regular expression match;
    • Write Your own validation function.

使用此修剪空白并删除不可打印的字符

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

PHP过滤器扩展具有检查外部用户输入所需的许多功能&它旨在使数据清理更容易和更快。

php过滤器可以轻松地清理和验证外部输入。