编写安全 PHP 应用程序的七个习惯
PHP 应用程序中的安全性包括远程安全性和本地安全性。本文将揭示 PHP 开发人员在实现具有这两种安全性的 Web 应用程序时应该养成的习惯。 在提及安全性问题时,需要注意,除了实际的平台和操作系统安全性问题之外,您还需要确保编写安全的应用程序。在编写 PHP 应用程序时,请应用下面的七个习惯以确保应用程序具有最好的安全性:
# 验证输入
# 保护文件系统
# 保护数据库
# 保护会话数据
# 保护跨站点脚本(Cross-site scripting,XSS)漏洞
# 检验表单 post
# 针对跨站点请求伪造(Cross-Site Request Forgeries,CSRF)进行保护 验证输入 在提及安全性问题时,验证数据是您可能采用的最重要的习惯。而在提及输入时,十分简单:不要相信用户。您的用户可能十分优秀,并且大多数用户可能完全按照期望来使用应用程序。但是,只要提供了输入的机会,也就极有可能存在非常糟糕的输入。作为一名应用程序开发人员,您必须阻止应用程序接受错误的输入。仔细考虑用户输入的位置及正确值将使您可以构建一个健壮、安全的应用程序。 虽然后文将介绍文件系统与数据库交互,但是下面列出了适用于各种验证的一般验证提示:
# 使用白名单中的值
# 始终重新验证有限的选项
# 使用内置转义函数
# 验证正确的数据类型(如数字) 白名单中的值(White-listed value)是正确的值,与无效的黑名单值(Black-listed value)相对。两者之间的区别是,通常在进行验证时,可能值的列表或范围小于无效值的列表或范围,其中许多值可能是未知值或意外值。 在进行验证时,记住设计并验证应用程序允许使用的值通常比防止所有未知值更容易。例如,要把字段值限定为所有数字,需要编写一个确保输入全都是数字的例程。不要编写用于搜索非数字值并在找到非数字值时标记为无效的例程。
PHP 应用程序中的安全性包括远程安全性和本地安全性。本文将揭示 PHP 开发人员在实现具有这两种安全性的 Web 应用程序时应该养成的习惯。 在提及安全性问题时,需要注意,除了实际的平台和操作系统安全性问题之外,您还需要确保编写安全的应用程序。在编写 PHP 应用程序时,请应用下面的七个习惯以确保应用程序具有最好的安全性:
# 验证输入
# 保护文件系统
# 保护数据库
# 保护会话数据
# 保护跨站点脚本(Cross-site scripting,XSS)漏洞
# 检验表单 post
# 针对跨站点请求伪造(Cross-Site Request Forgeries,CSRF)进行保护 验证输入 在提及安全性问题时,验证数据是您可能采用的最重要的习惯。而在提及输入时,十分简单:不要相信用户。您的用户可能十分优秀,并且大多数用户可能完全按照期望来使用应用程序。但是,只要提供了输入的机会,也就极有可能存在非常糟糕的输入。作为一名应用程序开发人员,您必须阻止应用程序接受错误的输入。仔细考虑用户输入的位置及正确值将使您可以构建一个健壮、安全的应用程序。 虽然后文将介绍文件系统与数据库交互,但是下面列出了适用于各种验证的一般验证提示:
# 使用白名单中的值
# 始终重新验证有限的选项
# 使用内置转义函数
# 验证正确的数据类型(如数字) 白名单中的值(White-listed value)是正确的值,与无效的黑名单值(Black-listed value)相对。两者之间的区别是,通常在进行验证时,可能值的列表或范围小于无效值的列表或范围,其中许多值可能是未知值或意外值。 在进行验证时,记住设计并验证应用程序允许使用的值通常比防止所有未知值更容易。例如,要把字段值限定为所有数字,需要编写一个确保输入全都是数字的例程。不要编写用于搜索非数字值并在找到非数字值时标记为无效的例程。
2. if ($_POST['submit'] == 'Download') {
3. $file = $_POST['fileName'];
4. header("Content-Type: application/x-octet-stream");
5. header("Content-Transfer-Encoding: binary");
6. header("Content-Disposition: attachment; filename=\"" . $file . "\";" );
7. $fh = fopen($file, 'r');
8. while (! feof($fh))
9. {
10. echo(fread($fh, 1024));
11. }
12. fclose($fh);
13. } else {
14. echo("<html><head><");
15. echo("title>Guard your filesystem</title></head>");
16. echo("<body><form id=\"myFrom\" action=\"" . $_SERVER['PHP_SELF'] .
17. "\" method=\"post\">");
18. echo("<div><input type=\"text\" name=\"fileName\" value=\"");
19. echo(isset($_REQUEST['fileName']) ? $_REQUEST['fileName'] : '');
20. echo("\" />");
21. echo("<input type=\"submit\" value=\"Download\" name=\"submit\" /></div>");
22. echo("</form></body></html>");
23. }
24. 正如您所见,清单 1 中比较危险的脚本将处理 Web 服务器拥有读取权限的所有文件,包括会话目录中的文件(请参阅 “保护会话数据”),甚至还包括一些系统文件(例如 /etc/passwd)。为了进行演示,这个示例使用了一个可供用户键入文件名的文本框,但是可以在查询字符串中轻松地提供文件名。 同时配置用户输入和文件系统访问权十分危险,因此最好把应用程序设计为使用数据库和隐藏生成的文件名来避免同时配置。但是,这样做并不总是有效。清单 2 提供了验证文件名的示例例程。它将使用正则表达式以确保文件名中仅使用有效字符,并且特别检查圆点字符:..。 1. function isValidFileName($file) {
2. /* don't allow .. and allow any "word" character \ / */
3. return preg_match('/^(((?:\.)(?!\.))|\w)+$/', $file);
4. }
2. <head>
3. <title>SQL Injection Example</title>
4. </head>
5. <body>
6. <form id="myFrom" action="<?php echo $_SERVER['PHP_SELF']; ?>"
7. method="post">
8. <div><input type="text" name="account_number"
9. value="<?php echo(isset($_POST['account_number']) ?
10. $_POST['account_number'] : ''); ?>" />
11. <select name="col">
12. <option value="account_number">Account Number</option>
13. <option value="name">Name</option>
14. <option value="address">Address</option>
15. </select>
16. <input type="submit" value="Save" name="submit" /></div>
17. </form>
18. <?php
19. if ($_POST['submit'] == 'Save') {
20. /* do the form processing */
21. $link = mysql_connect('hostname', 'user', 'password') or
22. die ('Could not connect' . mysql_error());
23. mysql_select_db('test', $link);
24.
25. $col = $_POST['col'];
26.
27. $select = "SELECT " . $col . " FROM account_data WHERE account_number = "
28. . $_POST['account_number'] . ";" ;
29. echo '<p>' . $select . '</p>';
30.
31. $result = mysql_query($select) or die('<p>' . mysql_error() . '</p>');
32.
33. echo '<table>';
34. while ($row = mysql_fetch_assoc($result)) {
35. echo '<tr>';
36. echo '<td>' . $row[$col] . '</td>';
37. echo '</tr>';
38. }
39. echo '</table>';
40.
41. mysql_close($link);
42. }
43. ?>
44. </body>
45. </html>
46. 因此,要形成保护数据库的习惯,请尽可能避免使用动态 SQL 代码。如果无法避免动态 SQL 代码,请不要对列直接使用输入。清单 4 显示了除使用静态列外,还可以向帐户编号字段添加简单验证例程以确保输入值不是非数字值。 1. <html>
2. <head>
3. <title>SQL Injection Example</title>
4. </head>
5. <body>
6. <form id="myFrom" action="<?php echo $_SERVER['PHP_SELF']; ?>"
7. method="post">
8. <div><input type="text" name="account_number"
9. value="<?php echo(isset($_POST['account_number']) ?
10. $_POST['account_number'] : ''); ?>" /> <input type="submit"
11. value="Save" name="submit" /></div>
12. </form>
13. <?php
14. function isValidAccountNumber($number)
15. {
16. return is_numeric($number);
17. }
18.
19. if ($_POST['submit'] == 'Save') {
20.
21. /* Remember habit #1--validate your data! */
22. if (isset($_POST['account_number']) &
23. isValidAccountNumber($_POST['account_number'])) {
24.
25. /* do the form processing */
26. $link = mysql_connect('hostname', 'user', 'password') or
27. die ('Could not connect' . mysql_error());
28. mysql_select_db('test', $link);
29.
30. $select = sprintf("SELECT account_number, name, address " .
31. " FROM account_data WHERE account_number = %s;",
32. mysql_real_escape_string($_POST['account_number']));
33. echo '<p>' . $select . '</p>';
34.
35. $result = mysql_query($select) or die('<p>' . mysql_error() . '</p>');
36.
37. echo '<table>';
38. while ($row = mysql_fetch_assoc($result)) {
39. echo '<tr>';
40. echo '<td>' . $row['account_number'] . '</td>';
41. echo '<td>' . $row['name'] . '</td>';
42. echo '<td>' . $row['address'] . '</td>';
43. echo '</tr>';
44. }
45. echo '</table>';
46.
47. mysql_close($link);
48. } else {
49. echo "<span style=\"font-color:red\">" .
50. "Please supply a valid account number!</span>";
51.
52. }
53. }
54. ?>
55. </body>
56. </html>
57. 本例还展示了 mysql_real_escape_string() 函数的用法。此函数将正确地过滤您的输入,因此它不包括无效字符。如果您一直依赖于 magic_quotes_gpc,那么需要注意它已被弃用并且将在 PHP V6 中删除。从现在开始应避免使用它并在此情况下编写安全的 PHP 应用程序。此外,如果使用的是 ISP,则有可能您的 ISP 没有启用 magic_quotes_gpc。 最后,在改进的示例中,您可以看到该 SQL 语句和输出没有包括动态列选项。使用这种方法,如果把列添加到稍后含有不同信息的表中,则可以输出这些列。如果要使用框架以与数据库结合使用,则您的框架可能已经为您执行了 SQL 验证。确保查阅文档以保证框架的安全性;如果仍然不确定,请进行验证以确保稳妥。即使使用框架进行数据库交互,仍然需要执行其他验证。
2. session_start();
3. ?>
4. <html>
5. <head>
6. <title>Storing session information</title>
7. </head>
8. <body>
9. <?php
10. if ($_POST['submit'] == 'Save') {
11. $_SESSION['userName'] = $_POST['userName'];
12. $_SESSION['accountNumber'] = $_POST['accountNumber'];
13. }
14. ?>
15. <form id="myFrom" action="<?php echo $_SERVER['PHP_SELF']; ?>"
16. method="post">
17. <div><input type="hidden" name="token" value="<?php echo $token; ?>" />
18. <input type="text" name="userName"
19. value="<?php echo(isset($_POST['userName']) ? $_POST['userName'] : ''); ?>" />
20. <br />
21. <input type="text" name="accountNumber"
22. value="<?php echo(isset($_POST['accountNumber']) ?
23. $_POST['accountNumber'] : ''); ?>" />
24. <br />
25. <input type="submit" value="Save" name="submit" /></div>
26. </form>
27. </body>
28. </html>
29. 清单 6 显示了 /tmp 目录的内容。 1. -rw------- 1 _www wheel 97 Aug 18 20:00 sess_9e4233f2cd7cae35866cd8b61d9fa42b 正如您所见,在输出时(参见清单 7),会话文件以非常易读的格式包含信息。由于该文件必须可由 Web 服务器用户读写,因此会话文件可能为共享服务器中的所有用户带来严重的问题。除您之外的某个人可以编写脚本来读取这些文件,因此可以尝试从会话中取出值。 1. userName|s:5:"ngood";accountNumber|s:9:"123456789";
2. 您可以采取两项操作来保护会话数据。第一是把您放入会话中的所有内容加密。但是正因为加密数据并不意味着绝对安全,因此请慎重采用这种方法作为保护会话的惟一方式。备选方法是把会话数据存储在其他位置中,比方说数据库。您仍然必须确保锁定数据库,但是这种方法将解决两个问题:第一,它将把数据放到比共享文件系统更加安全的位置;第二,它将使您的应用程序可以更轻松地跨越多个 Web 服务器,同时共享会话可以跨越多个主机。 要实现自己的会话持久性,请参阅 PHP 中的 session_set_save_handler() 函数。使用它,您可以将会话信息存储在数据库中,也可以实现一个用于加密和解密所有数据的处理程序。清单 8 提供了实现的函数用法和函数骨架示例。您还可以在 参考资料 小节中查看如何使用数据库。