提问



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

最佳参考


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


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


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


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


等等等等 ...


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

其它参考1


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


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


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

其它参考2


不可以。你不能在没有任何上下文的情况下一般过滤数据。有时您希望将SQL查询作为输入,有时您希望将HTML作为输入。


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


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

其它参考3


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!

其它参考4


不,那里没有。


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


基本的经验法则



  • 对于SQL查询,绑定参数(与PDO一样)或对查询变量使用驱动程序本机转义函数(例如mysql_real_escape_string())

  • 使用strip_tags()过滤掉不需要的HTML

  • 使用htmlspecialchars()转义所有其他输出,并注意此处的第2和第3个参数。


其它参考5


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


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


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

其它参考6


PHP 5.2引入了 filter_var 函数。


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


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

其它参考7


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


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

其它参考8


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



  1. 清理/过滤用户输入数据。

  2. 转出输出。



1)应始终假定用户输入不良。


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


2)这是一个很大的主题,它取决于输出数据的上下文。对于HTML,有像htmlpurifier这样的解决方案。
根据经验,总是逃避你输出的任何东西。


这两个问题都太大了,无法在一篇文章中介绍,但有很多帖子更详细:


方法PHP输出[47]


更安全的PHP输出[48]

其它参考9



  

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





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


  • 明确设置charset:




    • $mysqli->set_charset("utf8");
      手册

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

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

    • $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");
      [49] [50] [51] [52] [53] [54]

    • 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 /*"]);
      [55]

    • 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

      使用过滤器函数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() - 执行正则表达式匹配;

    • 编写您自己的验证功能。



其它参考10


如果您正在使用PostgreSQL,PHP的输入可以通过pg_escape_string()[56] [57] [58] [59] [60] [61]进行转义


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


来自文档(http://php.net/manual/es/function.pg-escape-string.php):[62]



  pg_escape_string()转义用于查询数据库的字符串。它以PostgreSQL格式返回一个没有引号的转义字符串。


其它参考11


没有任何功能,因为有许多问题需要解决。



  1. SQL注入 - 今天,通常情况下,每个PHP项目都应该通过PHP数据对象(PDO)使用预准备语句作为最佳实践,防止错误引用错误以及一个全功能的注射解决方案。它也是访问数据库最灵活,最安全的方式。[63]


    查看(唯一正确的)PDO教程,了解有关PDO的所有相关信息。 (真诚地感谢顶级SO贡献者@YourCommonSense,关于这个主题的这个重要资源。)[64]

  2. XSS - 在......的路上消毒数据



    • HTML Purifier已经存在很长时间了,并且仍然在积极更新。您可以使用它来消毒恶意输入,同时仍然允许慷慨和可配置的标签白名单。适用于许多WYSIWYG编辑器,但对于某些用例可能很重。[65]

    • 在其他情况下,我们根本不想接受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函数:



    • 当您echoprint显示用户提供的值时,请使用htmlspecialchars,除非数据已正确清理并且允许显示HTML。

    • json_encode是一种从PHP提供用户提供的值到Javascript的安全方法


  4. 您是使用exec()system()函数调用外部shell命令,还是调用backtick运算符?如果是这样,除了SQL注入& XSS您可能还需要解决一些问题,用户在您的服务器上运行恶意命令。你需要使用escapeshellcmd如果你想逃避整个命令或escapeshellarg以逃避个别论点。[66] [67] [68] [69] [70] [71] [72]


其它参考12


避免清理输入和转义数据时出错的最简单方法是使用像Symfony,Nette等PHP框架或该框架的一部分(模板引擎,数据库层,ORM)。[73] [74]


像Twig或Latte这样的模板引擎默认情况下输出转义 - 如果您根据上下文(网页的HTML或Javascript部分)正确转义输出,则不必手动解决。[75]


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


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


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

其它参考13


只是想在输出转义的主题上添加它,如果你使用php DOMDocument来制作你的html输出,它将在正确的上下文中自动转义。属性(value =)和< span>的内部文本不平等
为了安全抵御XSS,请阅读:
OWASP XSS预防备忘单[77]

其它参考14


有过滤器扩展(howto-link,manual),它适用于所有GPC变量。虽然这不是一件神奇的事情,你仍然必须使用它。[78] [79]

其它参考15


您永远不会清理输入。



您始终清理输出。



您应用于数据以使其安全包含在SQL语句中的转换与您申请包含在HTML中的转换完全不同,您应用于Javascript中的转换完全不同于您申请包含在LDIF中的转换完全不同与您在CSS中包含的内容完全不同,与您在电子邮件中包含的内容完全不同....


通过各种方式验证输入 - 决定是否应接受它进行进一步处理或告诉用户它是不可接受的。但是,除非要离开PHP土地,否则不要对数据的表示进行任何更改。[80]


很久以前有人试图发明一种适用于转发数据的单一尺寸的机制,我们最终得到了magic_quotes,它没有正确地逃避所有输出目标的数据,导致不同的安装需要不同的代码才能工作。[81]

其它参考16


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



    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;
    }