PHPエスケープ関数の比較一覧
※以前別の場所で書いた文章を備忘的に書き記しておきます。
【投稿年月日】2008-02-27 【ジャンル】PHP/MySQL
「PHPとMySQLのセキュリティー対策関数」の続き。クロスサイトスクリプティング対策関数のhtmlspecialchars()と、SQLインジェクション対策関数のaddslashes()、mysql_real_escape_string()について比較一覧表を作成してみました。黄色い部分が変換できない文字です。
●htmlspecialchars ※HTML表示時に使用
変換前 |
変換後 |
|
$str |
htmlspecialchars($str) |
htmlspecialchars($str, ENT_QUOTES) |
& |
& |
& |
< |
< |
< |
> |
> |
> |
" |
" |
" |
' |
' |
' |
●addslashes・mysql_real_escape_string ※MySQLのquery発行時に使用
変換前 |
変換後 |
|
$str |
addslashes($str) |
mysql_real_escape_string($str) |
\x00(*null) |
\0 |
\0 |
\ |
||
' |
\' |
\' |
" |
\" |
\" |
\n(*改行) |
(*改行) |
\n |
\r(*CR) |
(*CR) |
\r |
\x1a(*EOF) |
(*EOF) |
\Z |
- CR … カーソルを文頭へ戻す制御コード(Carriage Return)
- EOF … ファイル終端を示す制御コード(End Of File)
黄色い部分が全部で4箇所、すなわち、htmlspecialchars($str)で変換できない文字が1つ、addslashes($str)で変換できない文字が3つあることが分かります。より安全なプログラムを心がけるには、htmlspecialchars($str)ではなくhtmlspecialchars($str, ENT_QUOTES)を、addslashes($str)ではなくmysql_real_escape_string($str)を使う必要があります。
また、クロスサイトスクリプティング対策とSQLインジェクション対策では、変換すべき文字や、変換後の文字が異なることも一目瞭然かと。この2つは全く別物として考えるべきです。
と偉そうに書いていますが、以前は、addslashes()とhtmlspecialchars()を重ねて使用したり、addslashes()を使用しなければならない場面でhtmlspecialchars()を使用したりと、十分な理解もなく、誤ったセキュリティ対策をしていました。また、htmlspecialchars()を使用しても、htmlspecialchars($str, ENT_QUOTES)というように「ENT_QUOTES」を付けることは(面倒なので)やっていませんでした。反省しています。。。(ああぁぁ…)
現実的に考えると、mysql_real_escape_string()はPHP4.3以上でしか使えないし、htmlspecialchars($str, ENT_QUOTES)はあまりにも長すぎるなど、PHPは、セキュリティ対策を手軽に行うことが難しい言語なのかもしれません。
そういう訳なので、皆様にも「PHPとMySQLのセキュリティー対策関数」で取り上げた2つのセキュリティー対策関数を利用して手軽にサニタイズすることをお勧めします。
以下、具体的なサンプル。$_GETで受け取った変数の典型的なエスケープ方法です。
<?php
$db = 'MYDATABASE';
$dbconnect = @mysql_connect('localhost', 'USER', 'PASSWORD');
if(!$dbconnect) {
exit;
}
mysql_select_db($db) or die(SQL error was: " . mysql_error());
$text = $_GET['text']; // $_GETで受け取った時点ではサニタイズしない
$query = $_GET['query']; // $_GETで受け取った時点ではサニタイズしない
$line = "<h1>".h($text)."</h1>"; // HTMLを表示するので「h」でサニタイズ
$line .= "<ol>";
$my = mysql_query('SELECT q, t FROM d1 WHERE q = "'.q($query).'" and t = "'.q($text).'"'); // MySQLのqueryを発行するので「q」でサニタイズ
while($row = mysql_fetch_assoc($my)) {
$line .= "<li>".h($row2['q'])." : ".h($row2['t'])."</li>"; // HTMLを表示するので「h」でサニタイズ
}
$line .= "</ol>";
$line .= "<h2>".h($query)."</h2>"; // HTMLを表示するので「h」でサニタイズ
echo $line;
mysql_query("INSERT INTO d1 VALUES ('".q($query)."', '".q($text)."')"); // MySQLのqueryを発行するので「q」でサニタイズ
mysql_close($dbconnect);
// XSS
function h($str='') {
if(is_array($str)) {
$h = function_exists("h") ? "h" : array(&$this, "h");
return array_map($h, $str);
}else {
if(!is_numeric($str)) {
$str = htmlspecialchars($str, ENT_QUOTES, "UTF-8"); // 文字コードは適宜変更
}
return $str;
}
}
// SQL Injection
function q($str='') {
if(is_array($str)) {
$q = function_exists("q") ? "q" : array(&$this, "q");
return array_map($q, $str);
}else {
if(get_magic_quotes_gpc()) {
$str = stripslashes($str);
}
if(!is_numeric($str)) {
$ver = explode('.', phpversion());
if(intval($ver[0].$ver[1])>=43) {
$str = mysql_real_escape_string($str);
}else {
$str = addslashes($str);
$pre = array('/\n/m', '/\r/m', '/\x1a/m');
$after = array('\\\n', '\\\r', '\Z');
$str = preg_replace($pre, $after, $str);
}
}
return $str;
}
}
?>
$db = 'MYDATABASE';
$dbconnect = @mysql_connect('localhost', 'USER', 'PASSWORD');
if(!$dbconnect) {
exit;
}
mysql_select_db($db) or die(SQL error was: " . mysql_error());
$text = $_GET['text']; // $_GETで受け取った時点ではサニタイズしない
$query = $_GET['query']; // $_GETで受け取った時点ではサニタイズしない
$line = "<h1>".h($text)."</h1>"; // HTMLを表示するので「h」でサニタイズ
$line .= "<ol>";
$my = mysql_query('SELECT q, t FROM d1 WHERE q = "'.q($query).'" and t = "'.q($text).'"'); // MySQLのqueryを発行するので「q」でサニタイズ
while($row = mysql_fetch_assoc($my)) {
$line .= "<li>".h($row2['q'])." : ".h($row2['t'])."</li>"; // HTMLを表示するので「h」でサニタイズ
}
$line .= "</ol>";
$line .= "<h2>".h($query)."</h2>"; // HTMLを表示するので「h」でサニタイズ
echo $line;
mysql_query("INSERT INTO d1 VALUES ('".q($query)."', '".q($text)."')"); // MySQLのqueryを発行するので「q」でサニタイズ
mysql_close($dbconnect);
// XSS
function h($str='') {
if(is_array($str)) {
$h = function_exists("h") ? "h" : array(&$this, "h");
return array_map($h, $str);
}else {
if(!is_numeric($str)) {
$str = htmlspecialchars($str, ENT_QUOTES, "UTF-8"); // 文字コードは適宜変更
}
return $str;
}
}
// SQL Injection
function q($str='') {
if(is_array($str)) {
$q = function_exists("q") ? "q" : array(&$this, "q");
return array_map($q, $str);
}else {
if(get_magic_quotes_gpc()) {
$str = stripslashes($str);
}
if(!is_numeric($str)) {
$ver = explode('.', phpversion());
if(intval($ver[0].$ver[1])>=43) {
$str = mysql_real_escape_string($str);
}else {
$str = addslashes($str);
$pre = array('/\n/m', '/\r/m', '/\x1a/m');
$after = array('\\\n', '\\\r', '\Z');
$str = preg_replace($pre, $after, $str);
}
}
return $str;
}
}
?>
$_GETを受け取った時点では何もしません。変数をHTML表示とMySQLのquery発行時の両方で使う可能性があるからです。上記比較一覧を見れば分かるように、使い方を誤ると意図しない文字変換をしてしまいます。よって、原則的にサニタイズはHTML表示時かMySQLのquery発行時に、面倒くさがらずその都度行なうべきです。
HTML表示で使う場合は「h($str)」、MySQLのquery発行時で使う場合は「q($str)」としてサニタイズします。なお、上記サンプルにはありませんが、数値の場合は「intval($str)」とします。
以上がクロスサイトスクリプティング対策とSQLインジェクション対策の基本です。
なお、$_GETや$_POST、$_SERVERなどのスーパーグローバル変数だけでなく、グローバル変数やユーザー定義関数(function)についても、汚染されている可能性があるので安全性を疑った方が無難です。安全性が担保されているもの以外は、多少の面倒くささを我慢してサニタイズした方がいいかと思われます。
対策の一つとしては、ユーザー定義関数(function)の名前付けを工夫することが挙げられます。名前の先頭に、クロスサイトスクリプティング対策済みの場合は「h_」を、SQLインジェクション対策済みの場合は「q_」を、両方とも完了している場合は「hq_」をつけると、プログラムの見通しがよくなります。
例えば、h_CloseHtml()ならばクロスサイトスクリプティング対策済みの関数、q_CountTable()ならばSQLインジェクション対策済みの関数だと一目で分かるので、作業効率が高まるものと思われます。
また、returnで終わるものに「r_」を、echoやprintで終わるものに「p_」を、MySQLのquery発行で終わるものに「m_」などとつけることにより、ユーザー定義関数の性格がより明確になります。
安全性を考慮すれば、グローバル変数ではなく、安全性が担保されたユーザー定義関数(※名前の先頭が「h_」や「q_」)を多用した方がよさそうです。
【参考】
EDIUNET | PHP/MySQL | 独り言 | 提供サービス | JavaScript