Web アプリケーションのセキュリティの向上
PHP アプリケーションのセキュリティには、リモート セキュリティとローカル セキュリティが含まれます。この記事では、PHP 開発者が両方の種類のセキュリティを備えた Web アプリケーションを実装する際に身につけるべき習慣を明らかにします。
?
セキュリティの問題に関しては、実際のプラットフォームとオペレーティング システムのセキュリティの問題に加えて、安全なアプリケーションを作成する必要があることに注意することが重要です。 PHP アプリケーションを作成するときは、次の 7 つの習慣を適用して、アプリケーションに可能な限り最高のセキュリティを確保してください:
- 入力を検証します
- ファイルシステムを保護
- データベースを保護
- セッションデータを保護
- クロスサイトスクリプティング (XSS) の脆弱性の保護
- 検査フォームの投稿
- クロスサイト リクエスト フォージェリ (CSRF) から保護
?
入力を検証します
データを検証することは、セキュリティに関して採用できる最も重要な習慣です。入力に関して言えば、それは非常に簡単です。ユーザーを信頼しないことです。ユーザーは優秀である可能性があり、そのほとんどがアプリケーションを期待どおりに使用している可能性があります。しかし、インプットの機会があるところでは、非常に悪いインプットが行われる可能性も高くなります。アプリケーション開発者は、アプリケーションが誤った入力を受け入れないようにする必要があります。ユーザー入力の場所と正しい値を慎重に考慮することで、堅牢で安全なアプリケーションを構築できます。
?
ファイル システムとデータベースの相互作用については後で説明しますが、さまざまな種類の検証に適用される一般的な検証のヒントをいくつか紹介します。
- ホワイトリストの値を使用
- 限られたオプションを常に再検証する
- 組み込みのエスケープ関数を使用する
- 正しいデータ型 (数値など) を確認してください
ホワイトリストに登録された値は、無効なブラックリストに登録された値とは対照的に、正しい値です。 2 つの違いは、通常、検証時に、可能な値のリストまたは範囲が無効な値のリストまたは範囲よりも小さく、その多くが不明な値または予期しない値である可能性があることです。
?
検証する場合、多くの場合、すべての未知の値から保護するよりも、アプリケーションで許可される値を設計して検証する方が簡単であることに注意してください。たとえば、フィールド値をすべての数値に制限するには、入力がすべて数値であることを保証するルーチンを作成する必要があります。数値以外の値を検索し、見つかった場合に無効としてマークするルーチンを作成しないでください。
?
ファイルシステムを保護
2000 年 7 月、Web サイトが Web サーバー上のファイルに保存されていた顧客データを暴露しました。 Web サイトの訪問者は、その URL を使用してデータを含むファイルを表示しました。ファイルは置き忘れられましたが、この例はファイル システムを攻撃者から保護することの重要性を強調しています。
PHP アプリケーションがファイルを操作し、ユーザーが入力できる可変データが含まれている場合は、ユーザー入力を注意深くチェックして、ユーザーがファイル システム上で不適切なアクションを実行できないことを確認してください。リスト 1 は、指定された名前のイメージをダウンロードする PHP サイトの例を示しています。
?
チェックリスト 1. ファイルをダウンロードします
<?php if ($_POST['submit'] == 'Download') { $file = $_POST['fileName']; header("Content-Type: application/x-octet-stream"); header("Content-Transfer-Encoding: binary"); header("Content-Disposition: attachment; filename=\"" . $file . "\";" ); $fh = fopen($file, 'r'); while (! feof($fh)) { echo(fread($fh, 1024)); } fclose($fh); } else { echo("<html><head><"); echo("title>Guard your filesystem</title></head>"); echo("<body><form id=\"myFrom\" action=\"" . $_SERVER['PHP_SELF'] . "\" method=\"post\">"); echo("<div><input type=\"text\" name=\"fileName\" value=\""); echo(isset($_REQUEST['fileName']) ? $_REQUEST['fileName'] : ''); echo("\" />"); echo("<input type=\"submit\" value=\"Download\" name=\"submit\" /></div>"); echo("</form></body></html>"); }
?
ご覧のとおり、リスト 1 のより危険なスクリプトは、セッション ディレクトリ内のファイル (「セッション データの保護」を参照) や一部のシステム ファイル (たとえば、 /etc/passwd
)。デモンストレーションの目的で、この例ではユーザーがファイル名を入力できるテキスト ボックスを使用しますが、ファイル名はクエリ文字列で簡単に指定できます。
?
ユーザー入力とファイル システム アクセスの同時構成は危険であるため、同時構成を避けるためにデータベースを使用し、生成されたファイル名を非表示にするようにアプリケーションを設計することが最善です。ただし、これが常に機能するとは限りません。リスト 2 は、ファイル名を検証するためのサンプル ルーチンを示しています。正規表現を使用してファイル名に有効な文字のみが使用されていることを確認し、特にドット文字 ..
をチェックします。
?
リスト 2. 有効なファイル名文字のチェック
function isValidFileName($file) { /* don't allow .. and allow any "word" character \ / */ return preg_match('/^(((?:\.)(?!\.))|\w)+$/', $file); }
?
データベースを保護
2008 年 4 月、米国の州矯正局は、クエリ文字列で SQL 列名を使用して機密データを漏洩しました。この侵害により、悪意のあるユーザーが表示する列を選択し、ページを送信し、データを取得することが可能になりました。この侵害は、アプリケーション開発者が予期できない方法でユーザーがどのように入力を実行できるかを示し、SQL インジェクション攻撃を防御する必要性を示しています。
?
リスト 3 は、SQL ステートメントを実行するサンプル スクリプトを示しています。この場合、SQL ステートメントは同じ攻撃を許可する動的ステートメントです。このフォームの所有者は、列名を選択リストに制限しているため、フォームが安全であると考えている可能性があります。ただし、このコードには、フォームの不正行為に関する最後のヒントが 1 つ抜けています。コードがオプションをドロップダウン ボックスに制限しているからといって、他のユーザーが必要なコンテンツ (アスタリスク [*
を含む) を含むフォームを投稿できないというわけではありません。 ])。
?
リスト 3. SQL ステートメントの実行
<html> <head> <title>SQL Injection Example</title> </head> <body> <form id="myFrom" action="<?php echo $_SERVER['PHP_SELF']; ?>" method="post"> <div><input type="text" name="account_number" value="<?php echo(isset($_POST['account_number']) ? $_POST['account_number'] : ''); ?>" /> <select name="col"> <option value="account_number">Account Number</option> <option value="name">Name</option> <option value="address">Address</option> </select> <input type="submit" value="Save" name="submit" /></div> </form> <?php if ($_POST['submit'] == 'Save') { /* do the form processing */ $link = mysql_connect('hostname', 'user', 'password') or die ('Could not connect' . mysql_error()); mysql_select_db('test', $link); $col = $_POST['col']; $select = "SELECT " . $col . " FROM account_data WHERE account_number = " . $_POST['account_number'] . ";" ; echo '<p>' . $select . '</p>'; $result = mysql_query($select) or die('<p>' . mysql_error() . '</p>'); echo '<table>'; while ($row = mysql_fetch_assoc($result)) { echo '<tr>'; echo '<td>' . $row[$col] . '</td>'; echo '</tr>'; } echo '</table>'; mysql_close($link); } ?> </body> </html>
?
因此,要形成保护数据库的习惯,请尽可能避免使用动态 SQL 代码。如果无法避免动态 SQL 代码,请不要对列直接使用输入。清单 4 显示了除使用静态列外,还可以向帐户编号字段添加简单验证例程以确保输入值不是非数字值。
?
清单 4. 通过验证和 mysql_real_escape_string()
提供保护
<html> <head> <title>SQL Injection Example</title> </head> <body> <form id="myFrom" action="<?php echo $_SERVER['PHP_SELF']; ?>" method="post"> <div><input type="text" name="account_number" value="<?php echo(isset($_POST['account_number']) ? $_POST['account_number'] : ''); ?>" /> <input type="submit" value="Save" name="submit" /></div> </form> <?php function isValidAccountNumber($number) { return is_numeric($number); } if ($_POST['submit'] == 'Save') { /* Remember habit #1--validate your data! */ if (isset($_POST['account_number']) && isValidAccountNumber($_POST['account_number'])) { /* do the form processing */ $link = mysql_connect('hostname', 'user', 'password') or die ('Could not connect' . mysql_error()); mysql_select_db('test', $link); $select = sprintf("SELECT account_number, name, address " . " FROM account_data WHERE account_number = %s;", mysql_real_escape_string($_POST['account_number'])); echo '<p>' . $select . '</p>'; $result = mysql_query($select) or die('<p>' . mysql_error() . '</p>'); echo '<table>'; while ($row = mysql_fetch_assoc($result)) { echo '<tr>'; echo '<td>' . $row['account_number'] . '</td>'; echo '<td>' . $row['name'] . '</td>'; echo '<td>' . $row['address'] . '</td>'; echo '</tr>'; } echo '</table>'; mysql_close($link); } else { echo "<span style=\"font-color:red\">" . "Please supply a valid account number!</span>"; } } ?> </body> </html>
?
本例还展示了 mysql_real_escape_string()
函数的用法。此函数将正确地过滤您的输入,因此它不包括无效字符。如果您一直依赖于 magic_quotes_gpc
,那么需要注意它已被弃用并且将在 PHP V6 中删除。从现在开始应避免使用它并在此情况下编写安全的 PHP 应用程序。此外,如果使用的是 ISP,则有可能您的 ISP 没有启用 magic_quotes_gpc
。
?
最 后,在改进的示例中,您可以看到该 SQL 语句和输出没有包括动态列选项。使用这种方法,如果把列添加到稍后含有不同信息的表中,则可以输出这些列。如果要使用框架以与数据库结合使用,则您的框架 可能已经为您执行了 SQL 验证。确保查阅文档以保证框架的安全性;如果仍然不确定,请进行验证以确保稳妥。即使使用框架进行数据库交互,仍然需要执行其他验证。
?
保护会话
默认情况下,PHP 中的会话信息将被写入临时目录。考虑清单 5 中的表单,该表单将显示如何存储会话中的用户 ID 和帐户编号。
?
清单 5. 存储会话中的数据
<?php session_start(); ?> <html> <head> <title>Storing session information</title> </head> <body> <?php if ($_POST['submit'] == 'Save') { $_SESSION['userName'] = $_POST['userName']; $_SESSION['accountNumber'] = $_POST['accountNumber']; } ?> <form id="myFrom" action="<?php echo $_SERVER['PHP_SELF']; ?>" method="post"> <div><input type="hidden" name="token" value="<?php echo $token; ?>" /> <input type="text" name="userName" value="<?php echo(isset($_POST['userName']) ? $_POST['userName'] : ''); ?>" /> <br /> <input type="text" name="accountNumber" value="<?php echo(isset($_POST['accountNumber']) ? $_POST['accountNumber'] : ''); ?>" /> <br /> <input type="submit" value="Save" name="submit" /></div> </form> </body> </html>
?
清单 6 显示了 /tmp 目录的内容。
?
清单 6. /tmp 目录中的会话文件
-rw------- 1 _www wheel 97 Aug 18 20:00 sess_9e4233f2cd7cae35866cd8b61d9fa42b
?
正如您所见,在输出时(参见清单 7),会话文件以非常易读的格式包含信息。由于该文件必须可由 Web 服务器用户读写,因此会话文件可能为共享服务器中的所有用户带来严重的问题。除您之外的某个人可以编写脚本来读取这些文件,因此可以尝试从会话中取出值。
?
清单 7. 会话文件的内容
userName|s:5:"ngood";accountNumber|s:9:"123456789";