簡體   English   中英

如何使用 PHP 清理用戶輸入?

[英]How can I sanitize user input with PHP?

是否有一個 catchall function 可以很好地為 SQL 注入和 XSS 攻擊清理用戶輸入,同時仍然允許某些類型的 HTML 標簽?

用戶輸入可以被過濾是一種常見的誤解。 PHP 甚至有一個(現已棄用)“功能”,稱為magic-quotes ,它建立在這個想法之上。 這是胡說八道。 忘記過濾(或清潔,或任何人們所說的)。

為了避免出現問題,您應該做的很簡單:每當您將一段數據嵌入到外部代碼中時,您必須根據該代碼的格式規則來處理它。 但您必須明白,這些規則可能過於復雜,無法手動全部遵循。 例如,在 SQL 中,字符串、數字和標識符的規則都是不同的。 為了您的方便,在大多數情況下,有一個專門的工具用於這種嵌入。 例如,當您需要在 SQL 查詢中使用 PHP 變量時,您必須使用准備好的語句,它將負責所有正確的格式/處理。

另一個例子是 HTML:如果在 HTML 標記中嵌入字符串,則必須使用htmlspecialchars對其進行轉義。 這意味着每個echoprint語句都應該使用htmlspecialchars

第三個示例可能是 shell 命令:如果您要將字符串(例如參數)嵌入到外部命令中,並使用exec調用它們,那么您必須使用escapeshellcmdescapeshellarg

此外,一個非常引人注目的例子是 JSON。 規則如此眾多且復雜,您永遠無法手動全部遵守。 這就是為什么您永遠不應該手動創建 JSON 字符串,而應始終使用專用函數json_encode()來正確格式化每一位數據的原因。

等等等等 ...

您需要主動過濾數據的唯一情況是您接受預格式化的輸入。 例如,如果您讓您的用戶發布 HTML 標記,您打算在網站上顯示這些標記。 但是,您應該明智地不惜一切代價避免這種情況,因為無論您過濾得多么好,它始終是一個潛在的安全漏洞。

不要試圖通過清理輸入數據來防止 SQL 注入。

相反,不允許在創建 SQL 代碼時使用數據 使用使用綁定變量的准備好的語句(即在模板查詢中使用參數)。 這是唯一可以防止 SQL 注入的方法。

請訪問我的網站http://bobby-tables.com/了解更多關於防止 SQL 注入的信息。

不可以。您不能在沒有任何上下文的情況下對數據進行一般過濾。 有時您希望將 SQL 查詢作為輸入,有時您希望將 HTML 作為輸入。

您需要過濾白名單上的輸入——確保數據符合您期望的某些規范。 然后你需要在使用它之前轉義它,這取決於你使用它的上下文。

為 SQL 轉義數據的過程 - 防止 SQL 注入 - 與為 (X)HTML 轉義數據的過程非常不同,以防止 XSS。

PHP 現在有新的漂亮的filter_input函數,例如,現在有一個內置的FILTER_VALIDATE_EMAIL類型,讓你從尋找“終極電子郵件正則表達式”中解放出來


我自己的過濾器類(使用 JavaScript 來突出顯示錯誤字段)可以通過 ajax 請求或正常表單發布來啟動。 (見下面的例子) <? /** * 豬肉表格驗證器。 通過正則表達式驗證字段並可以清理它們。 使用 PHP filter_var 內置函數和額外的正則表達式 * @package pig */

/**
 *  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 專用函數,如 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函數。

它支持大量的SANITIZEVALIDATE過濾器。

使用 PHP 清理用戶輸入的方法:

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

  • 顯式設置字符集:

    •   $mysqli->set_charset("utf8");
      手動的
    •  $pdo = new PDO('mysql:host=localhost;dbname=testdb;charset=UTF8', $user, $password);
      手動的
    •  $pdo->exec("設置名稱utf8");
      手動的
    •  $pdo = 新 PDO(\n "mysql:host=$host;dbname=$db", $user, $pass, \n大批(\n PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,\n PDO::MYSQL_ATTR_INIT_COMMAND => "設置名稱 utf8"\n )\n );
      手動的
    •  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); 顯式設置字符集
      $pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false); 禁用模擬准備好的語句以防止回退到模擬 MySQL 無法本地准備的語句(以防止注入)
      $var = $pdo->quote("' OR 1=1 /*"); 不僅轉義文字,而且還引用它(單引號 ' 字符)\n $stmt = $pdo->query("SELECT * FROM test WHERE name = $var LIMIT 1");\n

    • PDO Prepared Statements : vs MySQLi Prepared statements 支持更多的數據庫驅動程序和命名參數:

        $pdo = new PDO('mysql:host=localhost;dbname=testdb;charset=UTF8', $user, $password); 顯式設置字符集
      $pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false); 禁用模擬准備好的語句以防止回退到模擬 MySQL 無法本地准備的語句(以防止注入)\n $stmt = $pdo->prepare('SELECT * FROM test WHERE name = ?LIMIT 1');\n $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 — 檢查數字字符;
      $value = (int) $value;
      $value = intval($value);
      $var = filter_var('0755', FILTER_VALIDATE_INT, $options);
    • 對於字符串使用:
        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() — 執行正則表達式匹配;
    • 編寫您自己的驗證函數。

在具有/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. SQL 注入- 今天,通常每個 PHP 項目都應該通過 PHP 數據對象 (PDO)使用准備好的語句作為最佳實踐,以防止錯誤引用以及針對注入的全功能解決方案 這也是訪問數據庫的最靈活和最安全的方式。

查看(唯一合適的)PDO 教程,了解您需要了解的有關 PDO 的幾乎所有內容。 (衷心感謝頂級 SO 貢獻者 @YourCommonSense,提供有關該主題的這一重要資源。)

  1. XSS - 在途中清理數據...
  • HTML Purifier已經存在很長時間了,並且仍在積極更新中。 您可以使用它來清理惡意輸入,同時仍然允許大量且可配置的標簽白名單。 適用於許多 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; }
  1. XSS - 在輸出時清理數據...除非您保證在將數據添加到數據庫之前已正確清理數據,否則您需要在將其顯示給用戶之前對其進行清理,我們可以利用這些有用的 PHP 函數:
  • 當您調用echoprint以顯示用戶提供的值時,請使用htmlspecialchars除非數據經過適當的安全處理並允許顯示 HTML。
  • json_encode是一種將用戶提供的值從 PHP 提供到 Javascript 的安全方式
  1. 您是使用exec()system()函數還是使用backtick運算符調用外部 shell 命令? 如果是這樣,除了 SQL 注入和 XSS,您可能還有其他問題需要解決,用戶在您的服務器上運行惡意命令 如果您想轉義整個命令或escapeshellarg以轉義單個參數,則需要使用escapeshellcmd

你在這里描述的是兩個不同的問題:

  1. 清理/過濾用戶輸入數據。
  2. 逃逸輸出。

1) 應始終假定用戶輸入是錯誤的。

使用准備好的語句,或/和使用 mysql_real_escape_string 過濾絕對是必須的。 PHP 還內置了 filter_input,這是一個很好的起點。

2)這是一個很大的話題,它取決於輸出數據的上下文。 對於 HTML,有一些解決方案,例如 htmlpurifier。 根據經驗,總是逃避你輸出的任何東西。

這兩個問題都太大了,無法在一個帖子中討論,但是有很多帖子可以更詳細地介紹:

方法 PHP 輸出

更安全的 PHP 輸出

如果您使用的是 PostgreSQL,則可以使用pg_escape_literal()對來自 PHP 的輸入進行轉義

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

文檔

pg_escape_literal()轉義用於查詢 PostgreSQL 數據庫的文字。 它返回 PostgreSQL 格式的轉義文字。

避免在清理輸入和轉義數據時出錯的最簡單方法是使用 PHP 框架,如SymfonyNette等或該框架的一部分(模板引擎、數據庫層、ORM)。

默認情況下,像Twig或 Latte 這樣的模板引擎具有輸出轉義功能 - 如果您根據上下文(網頁的 HTML 或 Javascript 部分)正確轉義了輸出,則無需手動解決。

框架會自動清理輸入,您不應該直接使用 $_POST、$_GET 或 $_SESSION 變量,而是通過路由、會話處理等機制。

對於數據庫(模型)層,有像 Doctrine 這樣的 ORM 框架或像 Nette Database 這樣的 PDO 包裝器。

您可以在此處閱讀更多相關信息 - 什么是軟件框架?

只是想在輸出轉義的主題上添加這一點,如果您使用 php DOMDocument 使您的 html 輸出,它將在正確的上下文中自動轉義。 屬性 (value="") 和 <span> 的內部文本不相等。 為了安全抵御 XSS,請閱讀以下內容: OWASP XSS 預防備忘單

你從不清理輸入。

你總是清理輸出。

您應用於數據以使其安全地包含在 SQL 語句中的轉換與您申請包含在 HTML 中的轉換完全不同 與您申請包含在 Javascript 中的完全不同 與您申請包含在 LDIF 中的完全不同與您應用於包含在 CSS 中的那些完全不同 與您應用於包含在電子郵件中的那些完全不同....

無論如何驗證輸入- 決定您是否應該接受它以進行進一步處理或告訴用戶這是不可接受的。 但是在數據即將離開 PHP 領域之前不要對數據的表示進行任何更改。

很久以前,有人試圖發明一種萬能的轉義數據機制,我們最終得到了“ magic_quotes ”,它沒有正確轉義所有輸出目標的數據,導致不同的安裝需要不同的代碼才能工作。

使用此修剪空白並刪除不可打印的字符

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

PHP 過濾器擴展具有檢查外部用戶輸入所需的許多功能,它旨在使數據清理更容易、更快捷。

PHP 過濾器可以輕松地清理和驗證外部輸入。

有過濾器擴展( howto-linkmanual ),它適用於所有 GPC 變量。 不過,這不是萬能的,您仍然必須使用它。

永遠不要相信用戶數據。

function clean_input($data) {
  $data = trim($data);
  $data = stripslashes($data);
  $data = htmlspecialchars($data);
  return $data;
}

trim()函數從字符串的兩側刪除空格和其他預定義字符。

stripslashes()函數刪除反斜杠

htmlspecialchars()函數將一些預定義的字符轉換為 HTML 實體。

預定義的字符是:

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

我可以看到php過濾器清理特殊的特殊字符派上用場。

喜歡:

    $a=fliter_var($_POST['a'],FILTER_SANITIZE_SPECIAL_CHARS);

然而,通過股票,我認為它可能會更好,因為查看c代碼,它只過濾“'\\ <>&和\\ 0所以我可以看到這是一個很好的消毒方法。但是,更改源代碼包括像/ {} []。這樣的其他字符;`會在encode(enc [''])行上強化這個函數:

    void php_filter_special_chars(PHP_INPUT_FILTER_PARAM_DECL)
{
unsigned char enc[256] = {0};

php_filter_strip(value, flags);

/* encodes ' " < > & \0 to numerical entities */
enc['\''] = enc['"'] = enc['<'] = enc['>'] = enc['&'] = enc[0] = 1;

/* if strip low is not set, then we encode them as &#xx; */
memset(enc, 1, 32);

if (flags & FILTER_FLAG_ENCODE_HIGH) {
    memset(enc + 127, 1, sizeof(enc) - 127);
}

php_filter_encode_html(value, enc);
}
    function sanitizeSearchKeyword($keyword)
    {
        $keyword = preg_replace('/[\'&*()}{|_+-]/', ' ', $keyword); // Removes special chars.
        return quotemeta($keyword);
    }

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

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM