web-dev-qa-db-ja.com

PHPでユーザー入力をサニタイズするための最良の方法は何ですか?

SQLインジェクションやXSS攻撃に対するユーザー入力のサニタイズに有効なキャッチオール関数はどこかにありますが、それでも特定のタイプのhtmlタグを許可しますか?

1044
Brent

ユーザー入力をフィルタリングできることはよくある誤解です。 PHPにも、マジッククォートと呼ばれる(現在は廃止予定の) "機能"があり、これはこのアイデアの上に成り立っています。それはナンセンスです。フィルタリング(またはクリーニング、または人々がそれを呼び出すものは何でも)については忘れてください。

問題を避けるためにあなたがすべきことは非常に簡単です:あなたが外国語のコードの中に文字列を埋め込むときはいつでも、あなたはその言語の規則に従って、それをエスケープしなければなりません。たとえば、MySqlをターゲットにしたSQLに文字列を埋め込む場合は、この目的のためにMySqlの関数で文字列をエスケープする必要があります(mysqli_real_escape_string)。 (または、データベースの場合は、可能であれば、準備済みステートメントを使用することをお勧めします。)

もう1つの例はHTMLです。HTMLマークアップ内に文字列を埋め込む場合は、 htmlspecialchars でエスケープする必要があります。つまり、すべてのecho文またはprint文では、htmlspecialcharsを使用する必要があります。

3番目の例としてはシェルコマンドがあります。文字列(引数など)を外部コマンドに埋め込み、それらを exec で呼び出す場合は、 escapeshellcmd および escapeshellarg を使用する必要があります。

などなど.

データを積極的にフィルタリングする必要があるのは、 only の場合です。フォーマット済みの入力を受け入れる場合です。例えば。ユーザーにHTMLマークアップの投稿を許可している場合は、それをサイトに表示する予定です。ただし、これを回避するのは賢明であるべきです。それをどの程度うまくフィルタリングしても、それは常に潜在的なセキュリティホールになるでしょう。

1123
troelskn

入力データをサニタイズしてSQLインジェクションを防止しようとしないでください。

代わりに、 SQLコードの作成にデータを使用することを許可しない 。束縛変数を使用するプリペアドステートメントを使用する(すなわち、テンプレートクエリでパラメータを使用する)。 SQLインジェクションに対して保証される唯一の方法です。

SQLインジェクションの防止についての詳細は私のウェブサイト http://bobby-tables.com/ をご覧ください。

195
Andy Lester

いいえ。目的のコンテキストなしにデータを一般的にフィルタすることはできません。 SQLクエリを入力として使用したい場合や、HTMLを入力として使用したい場合があります。

ホワイトリストで入力をフィルタリングする必要があります - データがあなたが期待するものの仕様と一致することを確認してください。それを使用しているコンテキストに応じて、それを使用する前にそれをエスケープする必要があります。

SQLインジェクションを防ぐためのSQLのデータエスケープ処理は、XSSを防ぐための(X)HTMLのデータエスケープ処理とは大きく異なります。

75
Daniel Papasian

PHPには新しいNice 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サーバーには役に立ちません)。おそらくORMのような適切なアプリケーション層でこれを自動的に処理したいでしょう。また、上で述べたように、htmlに出力するには、htmlspecialcharsなどの他のphp専用関数を使用してください。

同じように削除されたクラスやタグを使用したHTML入力を本当に許可するには、専用のxss検証パッケージのいずれかに依存します。 HTMLを解析するための独自の正規表現を書かないでください。

47
SchizoDuckie

いいえ、ありません。

まず第一に、SQLインジェクションは入力フィルタリングの問題であり、XSSはそれを回避する出力です - したがって、コードライフサイクルの中でこれら2つの操作を同時に実行することすらありません。

基本的な経験則

  • SQLクエリの場合は、(PDOと同様に)パラメータをバインドするか、クエリ変数にドライバ固有のエスケープ関数(mysql_real_escape_string()など)を使用します。
  • 不要なHTMLを除外するためにstrip_tags()を使用してください
  • 他のすべての出力をhtmlspecialchars()でエスケープし、ここで2番目と3番目のパラメーターに注意してください。
42
Peter Bailey

XSSの問題に対処するには、 HTML Purifier をご覧ください。それはかなり設定可能で、まともな実績があります。

SQLインジェクション攻撃の場合は、必ずユーザー入力を確認してから、mysql_real_escape_string()を介して実行してください。ただし、この関数はすべてのインジェクション攻撃を無効にするわけではありません。したがって、クエリ文字列にダンプする前にデータを確認することが重要です。

より良い解決策は、準備済みステートメントを使用することです。 PDOライブラリ とmysqli拡張モジュールはこれらをサポートします。

22
jasonbar

PHP 5.2では filter_var 関数が導入されました。

大量のSANITIZE、VALIDATEフィルターをサポートしています。

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

20
dangel

/mypage?id=53のようなページがあり、WHERE句でidを使用するという特定の状況で役立つことができる1つのトリックは、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つの特定の攻撃を切り取るだけなので、他のすべての答えを読んでください。 (そして、はい、私は上記のコードが素晴らしいことではないことを知っています、しかしそれは特定の防御を示しています。)

16
Hamish Downer

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 =新しい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( 'utf8')
      [PHP 5.5.0で非推奨、PHP 7.0.0で削除。
  • 安全な文字セットを使う:

    • Utf8、latin1、ascii ..を選択し、脆弱な文字セットbig5、cp932、gb2312、gbk、sjisを使用しないでください。
  • 空間化関数を使う:

    • MySQLiはステートメントを準備しました:
      $ stmt = $ mysqli  - > prepare( 'SELECT *テストから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/*");リテラルをエスケープするだけでなく、引用符で囲みます(一重引用符で囲みます)。 $ 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);明示的に文字セットを設定する
      $ pdo-> setAttribute(PDO :: ATTR_EMULATE_PREPARES、false);mySQLがネイティブに準備できないステートメントのエミュレーションへのフォールバックを防ぐために(インジェクションを防ぐために)準備済みステートメントのエミュレーションを無効にします。 $ stmt = $ pdo-> prepare( 'SELECT *テストから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ステートメントで使用するための文字列内の特殊文字をエスケープします。しかし、プリペアドステートメントは単にエスケープされた文字列ではないため、プリペアドステートメントを使用することをお勧めします。ステートメントは、使用するテーブルとインデックスを含む完全なクエリ実行プランを作成します。これは最適な方法です。
    • クエリ内では、変数を一重引用符( '')で囲みます。
  • 変数に、期待しているものが含まれていることを確認してください。

    • 整数を期待しているのなら、:を使ってください。
      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() - 正規表現の一致を実行します。
    • あなた自身の検証関数を書いてください。
11
Mark Martin

ここで説明しているのは、2つの別々の問題です。

  1. ユーザー入力データのサニタイズ/フィルタリング。
  2. 出力をエスケープします。

1)ユーザー入力は常に悪いと想定されるべきです。

プリペアドステートメントを使用するか、mysql_real_escape_stringを使用してフィルタリングすることは間違いなく必須です。 PHPにはfilter_inputも組み込まれています。ここから始めるのが良いでしょう。

2)これは大きなトピックであり、出力されるデータのコンテキストによって異なります。 HTMLには、htmlpurifierなどの解決策があります。経験則として、出力したものは常にエスケープしてください。

どちらの問題も1つの投稿に入るには大きすぎますが、より詳細に説明する投稿がたくさんあります。

メソッドPHP output

より安全なPHPの出力

11
Andrew

PostgreSQLを使用している場合、PHPからの入力はpg_escape_string()でエスケープできます。

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

ドキュメントから( http://php.net/manual/es/function.pg-escape-string.php ):

pg_escape_string()はデータベースを問い合わせるために文字列をエスケープします。引用符なしでPostgreSQL形式のエスケープ文字列を返します。

7
Alejandro Silva

入力をサニタイズしてデータをエスケープする際のミスを避ける最も簡単な方法は SymfonyNette などのPHP frameworkまたはそのフレームワークの一部(テンプレートエンジン、データベースレイヤー)を使うことです。 、ORM)。

Twig またはLatteのようなテンプレートエンジンは、デフォルトではエスケープ出力をオンにしています - コンテキスト(WebページのHTMLまたはJavascript部分)に応じて出力を適切にエスケープしている場合は手動で解決する必要はありません。

フレームワークは自動的に入力をサニタイズするので、$ _POST、$ _ GETまたは$ _ SESSION変数を直接使用するべきではありませんが、ルーティング、セッション処理などのメカニズムを通して.

そしてデータベース(モデル)層にはDoctrineのようなORMフレームワークやNette DatabaseのようなPDOのラッパーがあります。

あなたはここでそれについてもっと読むことができます - ソフトウェアフレームワークとは何ですか?

6
Ondřej Šotek

対処すべき問題が複数あるため、包括的な機能はありません。

  1. SQLインジェクション - 今日、一般的に、すべてのPHPプロジェクトは_ -PHPデータオブジェクト(PDO)を介して プリペアドステートメントを使用するべきですインジェクションに対する迷惑な見積もりおよびフル機能の解決策からのエラーを防止する。それはまたあなたのデータベースにアクセスするための最も柔軟で安全な方法です。

    (唯一の適切な)PDOチュートリアル をチェックしてください。PDOについて知っておくべきことはほとんどすべてです。 (この件に関するこの素晴らしいリソースを提供してくれたtop SO投稿者@YourCommonSenseに心から感謝します。)

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

  3. XSS - データを消去する途中でサニタイズする... データベースに追加する前にデータが適切にサニタイズされていることが保証されない限り、ユーザーに表示する前にサニタイズする必要があります。これらの便利なPHP関数:

    • ユーザー指定の値を表示するためにechoまたはprintを呼び出すときは、データが正しくサニタイズされ、HTMLを表示することが許可されている場合を除き、 htmlspecialchars を使用してください。
    • json_encode はPHPからJavascriptにユーザー指定の値を提供する安全な方法です。
  4. exec() または system() functionsを使用して、または backtick 演算子を使用して外部シェルコマンドを呼び出しますか。 そうだとすれば、SQLインジェクションとXSSに加えて、サーバー上で悪意のあるコマンドを実行しているユーザーに対処することにさらなる懸念があるかもしれません。個々の引数をエスケープするには、コマンド全体[OR escapeshellcmd をエスケープする場合は escapeshellarg を使用する必要があります。

4
webaholik

あなたがあなたのHTML出力を作るためにphp DOMDocumentを使うならば、それは自動的に正しい文脈でエスケープするでしょう、出力エスケープの主題にそれを加えたいだけでした。属性(value = "")と<span>の内部テキストが等しくありません。 XSSに対して安全であるためには、これを読んでください: OWASP XSS防止チートシート

3
user138720

ユーザーデータを信頼しないでください。

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;
2
Erik Thiart

入力をサニタイズすることはありません。

あなたはいつも出力をサニタイズします。

SQLステートメントへの包含を安全にするためにデータに適用する変換は、HTMLへの包含に適用するものとは完全に異なります。Javascriptへの包含に適用するものとは完全に異なります。あなたがCSSに含めるために適用するものとはまったく異なるあなたが電子メールに含めるために適用するものとは完全に異なります....

どうしても 入力を検証する - あなたはそれをさらなる処理のために受け入れるべきかどうかを決定するか、あるいはそれを受け入れられないことをユーザーに告げる。しかし、PHP landから出ようとするまで、データの表現に変更を加えないでください。

ずいぶん昔、誰かがデータをエスケープするための万能のメカニズムを発明しようとしました、そして我々は " magic_quotes "で終わりました。動作するようにコード。

2
symcbean

私は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);
}
0
drtechno

フィルタ拡張( how-to-linkmanual )があり、これはすべてのGPC変数でうまく機能します。それは魔法のようなことではありません、あなたはまだそれを使わなければならないでしょう。

0
Till