深入理解ajax系列第八篇——表单提交

简介

要提供互联网服务,当你在开发代码的时候必须时刻保持安全意识。可能大部分
PHP 脚本都对安全问题都不在意,这很大程度上是因为有大量的无经验程序员在使用这门语言。但是,没有理由让你因为对你的代码的不确定性而导致不一致的安全策略。当你在服务器上放任何涉及到钱的东西时,就有可能会有人尝试破解它。创建一个论坛程序或者任何形式的购物车,被攻击的可能性就上升到了无穷大。

澳门新浦京电子游戏 1

前面的话

  在以前,网站的用户与后端交互的主要方式是通过HTML表单的使用。表单的引入在1993年,由于其简单性和易用性,直到电子商务出现之前一直保持着重要位置。理解表单提交,对于更深入地理解ajax是有好处的。下面将详细介绍表单形式的交互

 

澳门新浦京电子游戏,好文章要让更多的人知道!!本文转载自:开源社区原文链接:

背景

为了确保你的 web 内容安全,这里有一些常规的安全准则:

建立表单

  表单处理是一个多线程。首先创建一个表单,以供用户输入详细的请求信息。接着,输入的数据被发送到网页服务器,在服务器里这些数据得到编译和错误检测。如果PHP代码标识出一个或多个需要重要输入的字段,则带有相关错误信息的表单会重新显示。当精确的输入信息满足代码的需要时,代码会采取一些调用数据库的行为,如输入购物的细节

  [注意]关于HTML表单元素的详细信息移步至此

  要建立一个表单,至少需要以下几个元素:一个form元素、一个指定GET或POST方法的提交类型、一个或多个输入字段,以及表单数据提交的目的地址URL

<form action="http://www.w3school.com.cn/demo/welcome.php">
    Name:
    <input name="name"><br>
    Email:
    <input name="email"><br>
    <input type="submit">
</form>

 

目标

别相信表单

攻击表单很简单。通过使用一个简单的 JavaScript
技巧,你可以限制你的表单只允许在评分域中填写 1 到 5
的数字。如果有人关闭了他们浏览器的 JavaScript
功能或者提交自定义的表单数据,你客户端的验证就失败了。

用户主要通过表单参数和你的脚本交互,因此他们是最大的安全风险。你应该学到什么呢?在
PHP 脚本中,总是要验证 传递给任何 PHP
脚本的数据。在本文中,我们向你演示了如何分析和防范跨站脚本(XSS)攻击,它可能会劫持用户凭据(甚至更严重)。你也会看到如何防止会玷污或毁坏你数据的
MySQL 注入攻击。

表单处理

  PHP 超全局变量 $_GET 和 $_POST 用于收集表单数据(form-data)

  GET 和 POST 都创建数组(例如,array( key => value, key2 =>
value2, key3 => value3,
…))。此数组包含键/值对,其中的键是表单控件的名称,而值是来自用户的输入数据。

  GET 和 POST 被视作 $_GET 和
$_POST。它们是超全局变量,这意味着对它们的访问无需考虑作用域,即无需任何特殊代码,能够从任何函数、类或文件访问它们

  $_GET 是通过 URL
参数传递到当前脚本的变量数组

  $_POST 是通过 HTTP POST
传递到当前脚本的变量数组

  通过 GET
方法从表单发送的信息对任何人都是可见的(所有变量名和值都显示在 URL
中)。GET对所发送信息的数量也有限制。限制在大于2000个字符。不过,由于变量显示在
URL 中,把页面添加到书签中也更为方便

  通过 POST 方法从表单发送的信息对其他人是不可见的(所有名称/值会被嵌入
HTTP 请求的主体中),并且对所发送信息的数量也无限制。此外 POST
支持高阶功能,比如在向服务器上传文件时进行 multi-part
二进制输入。不过,由于变量未显示在 URL
中,也就无法将页面添加到书签。一般地,使用 POST 来发送表单数据

【post】

<!-- 提交页 -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
</head>
<body>
<form method="post" action="http://www.w3school.com.cn/demo/welcome.php">
    Name:
    <input name="name"><br>
    Email:
    <input name="email"><br>
    <input type="submit">
</form>
</body>
</html>

<!-- 响应页 -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
</head>
<body>
    Welcome <?php echo $_POST["name"]; ?><br>
    Your email address is: <?php echo $_POST["email"]; ?>
</body>
</html>

【get】

  如果不设置form元素的method属性,则默认为get方法

<!-- 提交页 -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
</head>
<body>
<form action="http://www.w3school.com.cn/demo/welcome_get.php">
    Name:
    <input name="name"><br>
    Email:
    <input name="email"><br>
    <input type="submit">
</form>
</body>
</html>

<!-- 响应页 -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
</head>
<body>
    Welcome <?php echo $_GET["name"]; ?><br>
    Your email address is: <?php echo $_GET["email"]; ?>
</body>
</html>

 

本教程讲解如何防御最常见的安全威胁:SQL 注入、操纵 GET 和 POST
变量、缓冲区溢出攻击、跨站点脚本攻击、浏览器内的数据操纵和远程表单提交。

别相信用户

假定你网站获取的每一份数据都充满了有害的代码。清理每一部分,即便你相信没有人会尝试攻击你的站点。

表单安全

  上面的代码很简单。不过,最重要的内容被漏掉了。需要对表单数据进行验证,以防止脚本出现漏洞

  对 HTML 表单数据进行适当的验证对于防范黑客和垃圾邮件很重要

字段           验证规则
Name           必需。必须包含字母和空格。
E-mail         必需。必须包含有效的电子邮件地址(包含 @ 和 .)
Website        可选。如果选填,则必须包含有效的 URL。
Comment        可选。多行输入字段(文本框)
Gender         必需。必须选择一项。

<form method="post" action="<?php echo htmlspecialchars($_SERVER["PHP_SELF"]);?>">

姓名:
<input type="text" name="name" value="">
* 
<br><br>
电邮:
<input type="text" name="email" value="">
* 
<br><br>
网址:
<input type="text" name="website" value="">

<br><br>
<label>
评论:
<textarea name="comment" rows="5" cols="40"></textarea>
<br><br>
性别:
<input type="radio" name="gender" value="female">女性
<input type="radio" name="gender" value="male">男性
* 
<br><br>
<input type="submit" name="submit" value="提交"> 
</form>

【$_SERVER[“PHP_SELF”]】

  $_SERVER[“PHP_SELF”]
是一种超全局变量,它返回当前执行脚本的文件名。因此,$_SERVER[“PHP_SELF”]
将表单数据发送到页面本身,而不是跳转到另一张页面。这样,用户就能够在表单页面获得错误提示信息

【XSS】

  $_SERVER[“PHP_SELF”]
变量能够被黑客利用。如果页面使用了PHP_SELF,用户能够输入下划线然后执行跨站点脚本(XSS)

  跨站点脚本(Cross-site
scripting,XSS)是一种计算机安全漏洞类型,常见于Web应用程序。XSS能够使攻击者向其他用户浏览的网页中输入客户端脚本

  假设”test_form.php” 的页面中有如下表单

<form method="post" action="<?php echo $_SERVER["PHP_SELF"];?>">

  现在,如果用户进入的是地址栏中正常的URL:”

<form method="post" action="test_form.php">

  不过,如果用户在地址栏中键入了如下 URL:

http://www.example.com/test_form.php/%22%3E%3Cscript%3Ealert('hacked')%3C/script%3E

  在这种情况下,上面的代码会转换为:

<form method="post" action="test_form.php"/><script>alert('hacked')</script>

  这段代码加入了一段脚本和一个提示命令。并且当此页面加载后,就会执行JavaScript代码(用户会看到一个提示框)。这仅仅是一个关于
PHP_SELF 变量如何被利用的简单无害案例

  <script>标签内能够添加任何JavaScript代码,黑客能够把用户重定向到另一台服务器上的某个文件,该文件中的恶意代码能够更改全局变量或将表单提交到其他地址以保存用户数据等

【htmlspecialchars()】

  如果避免$_SERVER[“PHP_SELF”]被利用?通过使用
htmlspecialchars() 函数能够避免$_SERVER[“PHP_SELF”]被利用

  htmlspecialchars()函数把特殊字符转换为 HTML 实体。这意味着 < 和
> 之类的HTML字符会被替换为 < 和
>。这样可防止攻击者通过在表单中注入HTML或JavaScript代码(跨站点脚本攻击)对代码进行利用

  表单代码是这样的:

<form method="post" action="<?php echo htmlspecialchars($_SERVER["PHP_SELF"]);?>">

  htmlspecialchars()把特殊字符转换为HTML实体。现在,如果用户试图利用PHP_SELF变量,会导致如下输出

<form method="post" action="test_form.php/"><script>alert('hacked')</script>">

  所以,验证表单要做的第一件事是通过PHP的htmlspecialchars()函数传递所有变量。在使用htmlspecialchars()函数后,如果用户试图在文本字段中提交以下内容:

<script>location.href('http://www.hacked.com')</script>

  代码不会执行,因为会被保存为转义代码,就像这样:

&lt;script&gt;location.href('http://www.hacked.com')&lt;/script&gt;

  现在这条代码显示在页面上或e-mail中是安全的

  在用户提交该表单时,我们还要做两件事:1、通过PHP的trim()函数去除用户输入数据中不必要的字符(多余的空格、制表符、换行);2、通过PHP的stripslashes()函数删除用户输入数据中的反斜杠()

  接下来我创建一个检查函数,命名为
test_input(),通过test_input()函数检查每个$_POST变量

<?php
// 定义变量并设置为空值
$name = $email = $gender = $comment = $website = "";

if ($_SERVER["REQUEST_METHOD"] == "POST") {
$name = test_input($_POST["name"]);
$email = test_input($_POST["email"]);
$website = test_input($_POST["website"]);
$comment = test_input($_POST["comment"]);
$gender = test_input($_POST["gender"]);
}

function test_input($data) {
$data = trim($data);
$data = stripslashes($data);
$data = htmlspecialchars($data);
return $data;
}
?>

 

前提条件

关闭全局变量

你可能会有的最大安全漏洞是启用了 register_globals
配置参数。幸运的是,PHP 4.2 及以后版本默认关闭了这个配置。如果打开了
register_globals,你可以在你的 php.ini 文件中通过改变
register_globals 变量为 Off 关闭该功能:

register_globals = Off

新手程序员觉得注册全局变量很方便,但他们不会意识到这个设置有多么危险。一个启用了全局变量的服务器会自动为全局变量赋任何形式的参数。为了了解它如何工作以及为什么有危险,让我们来看一个例子。

假设你有一个称为 process.php
的脚本,它会向你的数据库插入表单数据。初始的表单像下面这样:

<input name="username" type="text" size="15" maxlength="64">

运行 process.php 的时候,启用了注册全局变量的 PHP 会将该参数赋值到
$username 变量。这会比通过 $_POST[‘username’]
$_GET[‘username’]
访问它节省击键次数。不幸的是,这也会给你留下安全问题,因为 PHP
会设置该变量的值为通过 GET 或 POST
的参数发送到脚本的任何值,如果你没有显示地初始化该变量并且你不希望任何人去操作它,这就会有一个大问题。

看下面的脚本,假如 $authorized 变量的值为
true,它会给用户显示通过验证的数据。正常情况下,只有当用户正确通过了这个假想的
authenticated_user() 函数验证,$authorized
变量的值才会被设置为真。但是如果你启用了
register_globals,任何人都可以发送一个 GET 参数,例如 authorized=1
去覆盖它:

<?php
// Define $authorized = true only if user is authenticated
if (authenticated_user()) {
    $authorized = true;
}
?>

这个故事的寓意是,你应该从预定义的服务器变量中获取表单数据。所有通过
post 表单传递到你 web 页面的数据都会自动保存到一个称为 $_POST
的大数组中,所有的 GET 数据都保存在 $_GET
大数组中。文件上传信息保存在一个称为 $_FILES
的特殊数据中。另外,还有一个称为 $_REQUEST 的复合变量。

要从一个 POST 方法表单中访问 username 字段,可以使用
$_POST[‘username’]。如果 username 在 URL 中就使用
$_GET[‘username’]。如果你不确定值来自哪里,用
$_REQUEST[‘username’]

<?php
$post_value = $_POST['post_value'];
$get_value = $_GET['get_value'];
$some_variable = $_REQUEST['some_value']; 
?>

$_REQUEST 是 $_GET、$_POST、和 $_COOKIE
数组的结合。如果你有两个或多个值有相同的参数名称,注意 PHP
会使用哪个。默认的顺序是 cookie、POST、然后是 GET。

错误信息

  在下面的代码中增加了一些新变量:$nameErr、$emailErr、$genderErr以及$websiteErr。这些错误变量会保存被请求字段的错误消息。还为每个$_POST变量添加了一个if
else语句。这条语句通过PHP的empty()函数检查$_POST变量是否为空。如果为空,则错误消息会存储于不同的错误变量中。如果不为空,则通过test_input()函数发送用户输入数据

<?php
// 定义变量并设置为空值
$nameErr = $emailErr = $genderErr = $websiteErr = "";
$name = $email = $gender = $comment = $website = "";

if ($_SERVER["REQUEST_METHOD"] == "POST") {
  if (empty($_POST["name"])) {
    $nameErr = "Name is required";
  } else {
    $name = test_input($_POST["name"]);
  }

  if (empty($_POST["email"])) {
    $emailErr = "Email is required";
  } else {
    $email = test_input($_POST["email"]);
  }

  if (empty($_POST["website"])) {
    $website = "";
  } else {
    $website = test_input($_POST["website"]);
  }

  if (empty($_POST["comment"])) {
    $comment = "";
  } else {
    $comment = test_input($_POST["comment"]);
  }

  if (empty($_POST["gender"])) {
    $genderErr = "Gender is required";
  } else {
    $gender = test_input($_POST["gender"]);
  }
}
?>

  在 HTML
表单中,在每个被请求字段后面增加了一点脚本。如果需要,会生成恰当的错误消息(如果用户未填写必填字段就试图提交表单)

<form method="post" action="<?php echo htmlspecialchars($_SERVER["PHP_SELF"]);?>">

Name: <input type="text" name="name">
* <?php echo $nameErr;?>
<br><br>
E-mail:
<input type="text" name="email">
* <?php echo $emailErr;?>
<br><br>
Website:
<input type="text" name="website">
<?php echo $websiteErr;?>
<br><br>
<label>Comment: <textarea name="comment" rows="5" cols="40"></textarea>
<br><br>
Gender:
<input type="radio" name="gender" value="female">Female
<input type="radio" name="gender" value="male">Male
* <?php echo $genderErr;?>
<br><br>
<input type="submit" name="submit" value="Submit"> 

</form>

 

本教程是为至少有一年编程经验的 PHP 开发人员编写的。您应该了解 PHP
的语法和约定;这里不解释这些内容。有使用其他语言(比如 Ruby、Python 和
Perl)的经验的开发人员也能够从本教程中受益,因为这里讨论的许多规则也适用于其他语言和环境。

推荐安全配置选项

这里有几个会影响安全功能的 PHP
配置设置。下面是一些显然应该用于生产服务器的:

  • register_globals 设置为 off
  • safe_mode 设置为 off
  • error_reporting 设置为
    off。如果出现错误了,这会向用户浏览器发送可见的错误报告信息。对于生产服务器,使用错误日志代替。开发服务器如果在防火墙后面就可以启用错误日志。(LCTT
    译注:此处据原文逻辑和常识,应该是“开发服务器如果在防火墙后面就可以启用错误报告,即
    on。”)
  • 停用这些函数:system()、exec()、passthru()、shell_exec()、proc_open()、和
    popen()。
  • open_basedir 为 /tmp(以便保存会话信息)目录和 web
    根目录,以便脚本不能访问这些选定区域外的文件。
  • expose_php 设置为 off。该功能会向 Apache 头添加包含版本号的 PHP
    签名。
  • allow_url_fopen 设置为
    off。如果你能够注意你代码中访问文件的方式-也就是你验证所有输入参数,这并不严格需要。
  • allow_url_include 设置为
    off。对于任何人来说,实在没有明智的理由会想要访问通过 HTTP
    包含的文件。

一般来说,如果你发现想要使用这些功能的代码,你就不应该相信它。尤其要小心会使用类似
system() 函数的代码-它几乎肯定有缺陷。

启用了这些设置后,让我们来看看一些特定的攻击以及能帮助你保护你服务器的方法。

表单验证

  验证规则中,”Name”, “E-mail” 以及 “Gender”
字段是必需的。这些字段不能为空且必须在 HTML 表单中填写

【验证名字】

  以下代码检查name字段是否包含字母和空格。如果name字段无效,则存储一条错误消息

$name = test_input($_POST["name"]);
if (!preg_match("/^[a-zA-Z ]*$/",$name)) {
  $nameErr = "只允许字母和空格!"; 
}

【验证 E-mail】

  以下代码展检查e-mail地址语法是否有效。如果无效则存储一条错误消息

$email = test_input($_POST["email"]);
if (!preg_match("/([w-]+@[w-]+.[w-]+)/",$email)) {
  $emailErr = "无效的 email 格式!"; 
}

【验证 URL】

  以下代码检查URL地址语法是否有效。如果 URL
地址语法无效,则存储一条错误消息

$website = test_input($_POST["website"]);
if (!preg_match("/b(?:(?:https?|ftp)://|www.)[-a-z0-9+&@#/%?=~_|!:,.;]*[-a-z0-9+&@#/%
=~_|]/i",$website)) {
  $websiteErr = "无效的 URL"; 
}

 

安全性快速简介

SQL 注入攻击

由于 PHP 传递到 MySQL 数据库的查询语句是用强大的 SQL
编程语言编写的,就有了某些人通过在 web 查询参数中使用 MySQL 语句尝试 SQL
注入攻击的风险。通过在参数中插入有害的 SQL
代码片段,攻击者会尝试进入(或破坏)你的服务器。

假如说你有一个最终会放入变量 $product 的表单参数,你使用了类似下面的 SQL
语句:

$sql = "select * from pinfo where product = '$product'";

如果参数是直接从表单中获得的,应该使用 PHP
自带的数据库特定转义函数,类似:

$sql = 'Select * from pinfo where product = '"' 
       mysql_real_escape_string($product) . '"';

如果不这样做的话,有人也许会把下面的代码段放到表单参数中:

39'; DROP pinfo; SELECT 'FOO

那么 $sql 的结果就是:

select product from pinfo where product = '39'; DROP pinfo; SELECT 'FOO'

由于分号是 MySQL 的语句分隔符,数据库会运行下面三条语句:

select * from pinfo where product = '39'
DROP pinfo
SELECT 'FOO'

好了,你丢失了你的表。

注意实际上 PHP 和 MySQL 不会运行这种特殊语法,因为 mysql_query()
函数只允许每个请求处理一个语句。但是,一个子查询仍然会生效。

要防止 SQL 注入攻击,做这两件事:

  • 总是验证所有参数。例如,如果需要一个数字,就要确保它是一个数字。
  • 总是对数据使用 mysql_real_escape_string()
    函数转义数据中的任何引号和双引号。

注意:要自动转义任何表单数据,可以启用魔术引号(Magic
Quotes)。

一些 MySQL 破坏可以通过限制 MySQL 用户权限避免。任何 MySQL
账户可以限制为只允许对选定的表进行特定类型的查询。例如,你可以创建只能选择行的
MySQL
用户。但是,这对于动态数据并不十分有用,另外,如果你有敏感的用户信息,可能某些人能访问其中一些数据,但你并不希望如此。例如,一个访问账户数据的用户可能会尝试注入访问另一个人的账户号码的代码,而不是为当前会话指定的号码。

保留值

  如果需要在用户点击提交按钮后在输入字段中显示值,我们在以下输入字段的value属性中增加了一小段
PHP 脚本:name、email 以及 website。在 comment
文本框字段中,把脚本放到了 <textarea> 与 </textarea>
之间。这些脚本输出$name、$email、$website 和 $comment 变量的值

  然后,还需要显示选中了哪个单选按钮。对此,必须操作 checked
属性(而非单选按钮的 value 属性)

Name: <input type="text" name="name" value="<?php echo $name;?>">

E-mail: <input type="text" name="email" value="<?php echo $email;?>">

Website: <input type="text" name="website" value="<?php echo $website;?>">

Comment: <textarea name="comment" rows="5" cols="40"><?php echo $comment;?></textarea>

Gender:

<input type="radio" name="gender"
<?php if (isset($gender) && $gender=="female") echo "checked";?>
value="female">Female
<input type="radio" name="gender"
<?php if (isset($gender) && $gender=="male") echo "checked";?>
value="male">Male

 

Web
应用程序最重要的部分是什么?根据回答问题的人不同,对这个问题的答案可能是五花八门。业务人员需要可靠性和可伸缩性。IT
支持团队需要健壮的可维护的代码。最终用户需要漂亮的用户界面和执行任务时的高性能。但是,如果回答
“安全性”,那么每个人都会同意这对 Web 应用程序很重要。

防止基本的 XSS 攻击

XSS 表示跨站脚本。不像大部分攻击,该漏洞发生在客户端。XSS
最常见的基本形式是在用户提交的内容中放入 JavaScript 以便偷取用户 cookie
中的数据。由于大部分站点使用 cookie 和 session
验证访客,偷取的数据可用于模拟该用户-如果是一个常见的用户账户就会深受麻烦,如果是管理员账户甚至是彻底的惨败。如果你不在站点中使用
cookie 和 session
ID,你的用户就不容易被攻击,但你仍然应该明白这种攻击是如何工作的。

不像 MySQL 注入攻击,XSS 攻击很难预防。Yahoo、eBay、Apple、以及
Microsoft 都曾经受 XSS 影响。尽管攻击不包含 PHP,但你可以使用 PHP
来剥离用户数据以防止攻击。为了防止 XSS
攻击,你应该限制和过滤用户提交给你站点的数据。正是因为这个原因,大部分在线公告板都不允许在提交的数据中使用
HTML 标签,而是用自定义的标签格式代替,例如 [b]
[linkto]

让我们来看一个如何防止这类攻击的简单脚本。对于更完善的解决办法,可以使用
SafeHTML,本文的后面部分会讨论到。

function transform_HTML($string, $length = null) {
// Helps prevent XSS attacks
    // Remove dead space.
    $string = trim($string);
    // Prevent potential Unicode codec problems.
    $string = utf8_decode($string);
    // HTMLize HTML-specific characters.
    $string = htmlentities($string, ENT_NOQUOTES);
    $string = str_replace("#", "&#35;", $string);
    $string = str_replace("%", "&#37;", $string);
    $length = intval($length);
    if ($length > 0) {
        $string = substr($string, 0, $length);
    }
    return $string;
}

这个函数将 HTML 特定的字符转换为 HTML
字面字符。一个浏览器对任何通过这个脚本的 HTML
以非标记的文本呈现。例如,考虑下面的 HTML 字符串:

<STRONG>Bold Text</STRONG>

一般情况下,HTML 会显示为:Bold Text

但是,通过 transform_HTML()
后,它就像原始输入一样呈现。原因是处理的字符串中的标签字符串转换为 HTML
实体。transform_HTML() 的结果字符串的纯文本看起来像下面这样:

<STRONG>Bold Text</STRONG>

该函数的实质是 htmlentities() 函数调用,它会将 <、>、和 & 转换为
<>、和
&。尽管这会处理大部分的普通攻击,但有经验的 XSS
攻击者有另一种把戏:用十六进制或 UTF-8 编码恶意脚本,而不是采用普通的
ASCII 文本,从而希望能绕过你的过滤器。他们可以在 URL 的 GET
变量中发送代码,告诉浏览器,“这是十六进制代码,你能帮我运行吗?”
一个十六进制例子看起来像这样:

<a href="http://host/a.php?variable=%22%3e%20%3c%53%43%52%49%50%54%3e%44%6f%73%6f%6d%65%74%68%69%6e%67%6d%61%6c%69%63%69%6f%75%73%3c%2f%53%43%52%49%50%54%3e">

浏览器渲染这个信息的时候,结果就是:

<a href="http://host/a.php?variable="> <SCRIPT>Dosomethingmalicious</SCRIPT>

为了防止这种情况,transform_HTML() 采用额外的步骤把 # 和 %
符号转换为它们的实体,从而避免十六进制攻击,并转换 UTF-8 编码的数据。

最后,为了防止某些人用很长的输入超载字符串从而导致某些东西崩溃,你可以添加一个可选的
$length 参数来截取你指定最大长度的字符串。

表单发送

  HTML网页的<form>元素能够以四种格式,向服务器发送数据

  使用POST方法,将enctype属性设为application/x-www-form-urlencoded,这是默认方法

<form action="register.php" method="post" onsubmit="AJAXSubmit(this); return false;"></form>

  使用POST方法,将enctype属性设为text/plain

<form action="register.php" method="post" enctype="text/plain" onsubmit="AJAXSubmit(this); return false;"></form>

  使用POST方法,将enctype属性设为multipart/form-data

<form action="register.php" method="post" enctype="multipart/form-data" onsubmit="AJAXSubmit(this); return false;"></form>

  使用GET方法,enctype属性将被忽略

<form action="register.php" method="get" onsubmit="AJAXSubmit(this); return false;"></form>

  某个表单有两个字段,分别是foo和baz,其中foo字段的值等于bar,baz字段的值是一个分为两行的字符串。上面四种方法,都可以将这个表单发送到服务器

  第一种方法是默认方法,POST发送,Encoding
type为application/x-www-form-urlencoded

Content-Type: application/x-www-form-urlencoded
foo=bar&baz=The+first+line.&#37;0D%0AThe+second+line.%0D%0A

  第二种方法是POST发送,Encoding type为text/plain

Content-Type: text/plain

foo=bar
baz=The first line.
The second line.

  第三种方法是POST发送,Encoding type为multipart/form-data

Content-Type: multipart/form-data; boundary=---------------------------314911788813839

-----------------------------314911788813839
Content-Disposition: form-data; name="foo"

bar
-----------------------------314911788813839
Content-Disposition: form-data; name="baz"

The first line.
The second line.

-----------------------------314911788813839--

  第四种方法是GET请求

?foo=bar&baz=The%20first%20line.%0AThe%20second%20line

 

但是,大多数讨论到此就打住了。尽管安全性在项目的检查表中,但是往往到了项目交付之前才开始考虑解决安全性问题。采用这种方式的
Web
应用程序项目的数量多得惊人。开发人员工作几个月,只在最后才添加安全特性,从而让
Web 应用程序能够向公众开放。

使用 SafeHTML

之前脚本的问题比较简单,它不允许任何类型的用户标记。不幸的是,这里有上百种方法能使
JavaScript 跳过用户的过滤器,并且要从用户输入中剥离全部
HTML,还没有方法可以防止这种情况。

当前,没有任何一个脚本能保证无法被破解,尽管有一些确实比大部分要好。有白名单和黑名单两种方法加固安全,白名单比较简单而且更加有效。

一个白名单解决方案是 PixelApes 的 SafeHTML 反跨站脚本解析器。

SafeHTML 能识别有效 HTML,能追踪并剥离任何危险标签。它用另一个称为
HTMLSax 的软件包进行解析。

按照下面步骤安装和使用 SafeHTML:

  1. 到 下载最新版本的
    SafeHTML。
  2. 把文件放到你服务器的类文件夹。该文件夹包括 SafeHTML 和 HTMLSax
    功能所需的所有东西。
  3. 在脚本中 include SafeHTML 类文件(safehtml.php)。
  4. 创建一个名为 $safehtml 的新 SafeHTML 对象。
  5. 用 $safehtml->parse() 方法清理你的数据。

这是一个完整的例子:

<?php
/* If you're storing the HTMLSax3.php in the /classes directory, along
   with the safehtml.php script, define XML_HTMLSAX3 as a null string. */
define(XML_HTMLSAX3, '');
// Include the class file.
require_once('classes/safehtml.php');
// Define some sample bad code.
$data = "This data would raise an alert <script>alert('XSS Attack')</script>";
// Create a safehtml object.
$safehtml = new safehtml();
// Parse and sanitize the data.
$safe_data = $safehtml->parse($data);
// Display result.
echo 'The sanitized data is <br />' . $safe_data;
?>

如果你想清理脚本中的任何其它数据,你不需要创建一个新的对象;在你的整个脚本中只需要使用
$safehtml->parse() 方法。

完整代码

<!DOCTYPE HTML> 
<html>
<head>
<style>
.error {color: #FF0000;}
</style>
</head>
<body> 

<?php
// 定义变量并设置为空值
$nameErr = $emailErr = $genderErr = $websiteErr = "";
$name = $email = $gender = $comment = $website = "";

if ($_SERVER["REQUEST_METHOD"] == "POST") {
   if (empty($_POST["name"])) {
     $nameErr = "姓名是必填的";
   } else {
     $name = test_input($_POST["name"]);
     // 检查姓名是否包含字母和空白字符
     if (!preg_match("/^[a-zA-Z ]*$/",$name)) {
       $nameErr = "只允许字母和空格"; 
     }
   }

   if (empty($_POST["email"])) {
     $emailErr = "电邮是必填的";
   } else {
     $email = test_input($_POST["email"]);
     // 检查电子邮件地址语法是否有效
     if (!preg_match("/([w-]+@[w-]+.[w-]+)/",$email)) {
       $emailErr = "无效的 email 格式"; 
     }
   }

   if (empty($_POST["website"])) {
     $website = "";
   } else {
     $website = test_input($_POST["website"]);
     // 检查 URL 地址语法是否有效(正则表达式也允许 URL 中的斜杠)
     if (!preg_match("/b(?:(?:https?|ftp)://|www.)[-a-z0-9+&@#/%?=~_|!:,.;]*[-a-z0-9+&@#/%=~_|]/i",$website)) {
       $websiteErr = "无效的 URL"; 
     }
   }

   if (empty($_POST["comment"])) {
     $comment = "";
   } else {
     $comment = test_input($_POST["comment"]);
   }

   if (empty($_POST["gender"])) {
     $genderErr = "性别是必选的";
   } else {
     $gender = test_input($_POST["gender"]);
   }
}

function test_input($data) {
   $data = trim($data);
   $data = stripslashes($data);
   $data = htmlspecialchars($data);
   return $data;
}
?>

<p>* 必需的字段</p>
<form method="post" action="<?php echo htmlspecialchars($_SERVER["PHP_SELF"]);?>"> 
   姓名:<input type="text" name="name" value="<?php echo $name;?>">
   * <?php echo $nameErr;?>
   <br><br>
   电邮:<input type="text" name="email" value="<?php echo $email;?>">
   * <?php echo $emailErr;?>
   <br><br>
   网址:<input type="text" name="website" value="<?php echo $website;?>">
   <?php echo $websiteErr;?>
   <br><br>
   评论:<textarea name="comment" rows="5" cols="40"><?php echo $comment;?></textarea>
   <br><br>
   性别:
    <input type="radio" name="gender"
    <?php if (isset($gender) && $gender=="female") echo "checked";?>
    value="female">女性
    <input type="radio" name="gender"
    <?php if (isset($gender) && $gender=="male") echo "checked";?>
    value="male">男性

   * <?php echo $genderErr;?>
   <br><br>
   <input type="submit" name="submit" value="提交"> 
</form>
</body>
</html>

结果往往是一片混乱,甚至需要返工,因为代码已经经过检验、单元测试并集成为更大的框架,之后才在其中添加安全特性。添加安全性之后,主要组件可能会停止工作。安全性的集成使得原本顺畅的过程增加额外负担或步骤。

什么可能会出现问题?

你可能犯的最大错误是假设这个类能完全避免 XSS 攻击。SafeHTML
是一个相当复杂的脚本,几乎能检查所有事情,但没有什么是能保证的。你仍然需要对你的站点做参数验证。例如,该类不能检查给定变量的长度以确保能适应数据库的字段。它也不检查缓冲溢出问题。

XSS
攻击者很有创造力,他们使用各种各样的方法来尝试达到他们的目标。可以阅读
RSnake 的 XSS 教程
,看一下这里有多少种方法尝试使代码跳过过滤器。SafeHTML
项目有很好的程序员一直在尝试阻止 XSS
攻击,但无法保证某些人不会想起一些奇怪和新奇的方法来跳过过滤器。

注意:XSS 攻击严重影响的一个例子
,其中显示了如何一步一步创建一个让
MySpace 服务器过载的 JavaScript XSS 蠕虫。

本教程提供一种将安全性集成到 PHP Web
应用程序中的好方法。它讨论几个一般性安全主题,然后深入讨论主要的安全漏洞以及如何堵住它们。在学完本教程之后,您会对安全性有更好的理解。

用单向哈希保护数据

该脚本对输入的数据进行单向转换,换句话说,它能对某人的密码产生哈希签名,但不能解码获得原始密码。为什么你希望这样呢?应用程序会存储密码。一个管理员不需要知道用户的密码,事实上,只有用户知道他/她自己的密码是个好主意。系统(也仅有系统)应该能识别一个正确的密码;这是
Unix 多年来的密码安全模型。单向密码安全按照下面的方式工作:

  1. 当一个用户或管理员创建或更改一个账户密码时,系统对密码进行哈希并保存结果。主机系统会丢弃明文密码。
  2. 当用户通过任何方式登录到系统时,再次对输入的密码进行哈希。
  3. 主机系统丢弃输入的明文密码。
  4. 当前新哈希的密码和之前保存的哈希相比较。
  5. 如果哈希的密码相匹配,系统就会授予访问权限。

主机系统完成这些并不需要知道原始密码;事实上,原始密码完全无所谓。一个副作用是,如果某人侵入系统并盗取了密码数据库,入侵者会获得很多哈希后的密码,但无法把它们反向转换为原始密码。当然,给足够时间、计算能力,以及弱用户密码,一个攻击者还是有可能采用字典攻击找出密码。因此,别轻易让人碰你的密码数据库,如果确实有人这样做了,让每个用户更改他们的密码。

主题包括:

加密 Vs 哈希

技术上来来说,哈希过程并不是加密。哈希和加密是不同的,这有两个理由:

不像加密,哈希数据不能被解密。

是有可能(但非常罕见)两个不同的字符串会产生相同的哈希。并不能保证哈希是唯一的,因此别像数据库中的唯一键那样使用哈希。

function hash_ish($string) {
    return md5($string);
}

上面的 md5() 函数基于 RSA 数据安全公司的消息摘要算法(即 MD5)返回一个由
32 个字符组成的十六进制串。然后你可以将那个 32
位字符串插入到数据库中和另一个 md5 字符串相比较,或者直接用这 32
个字符。

SQL 注入攻击

破解脚本

几乎不可能解密 MD5
数据。或者说很难。但是,你仍然需要好的密码,因为用一整个字典生成哈希数据库仍然很简单。有一些在线
MD5 字典,当你输入 06d80eb0c50b49a509b49f2424e8c805 后会得到结果
“dog”。因此,尽管技术上 MD5
不能被解密,这里仍然有漏洞,如果某人获得了你的密码数据库,你可以肯定他们肯定会使用
MD5 字典破译。因此,当你创建基于密码的系统的时候尤其要注意密码长度(最小
6 个字符,8 个或许会更好)和包括字母和数字。并确保这个密码不在字典中。

操纵 GET 字符串

用 Mcrypt 加密数据

如果你不需要以可阅读形式查看密码,采用 MD5
就足够了。不幸的是,这里并不总是有可选项,如果你提供以加密形式存储某人的信用卡信息,你可能需要在后面的某个地方进行解密。

最早的一个解决方案是 Mcrypt 模块,这是一个用于允许 PHP
高速加密的插件。Mcrypt 库提供了超过 30
种用于加密的计算方法,并且提供口令确保只有你(或者你的用户)可以解密数据。

让我们来看看使用方法。下面的脚本包含了使用 Mcrypt 加密和解密数据的函数:

<?php
$data = "Stuff you want encrypted";
$key = "Secret passphrase used to encrypt your data";
$cipher = "MCRYPT_SERPENT_256";
$mode = "MCRYPT_MODE_CBC";
function encrypt($data, $key, $cipher, $mode) {
// Encrypt data
return (string)
            base64_encode
                (
                mcrypt_encrypt
                    (
                    $cipher,
                    substr(md5($key),0,mcrypt_get_key_size($cipher, $mode)),
                    $data,
                    $mode,
                    substr(md5($key),0,mcrypt_get_block_size($cipher, $mode))
                    )
                );
}
function decrypt($data, $key, $cipher, $mode) {
// Decrypt data
    return (string)
            mcrypt_decrypt
                (
                $cipher,
                substr(md5($key),0,mcrypt_get_key_size($cipher, $mode)),
                base64_decode($data),
                $mode,
                substr(md5($key),0,mcrypt_get_block_size($cipher, $mode))
                );
}
?>

mcrypt() 函数需要几个信息:

  • 需要加密的数据
  • 用于加密和解锁数据的口令,也称为键。
  • 用于加密数据的计算方法,也就是用于加密数据的算法。该脚本使用了
    MCRYPT_SERPENT_256,但你可以从很多算法中选择,包括
    MCRYPT_TWOFISH192MCRYPT_RC2MCRYPT_DES、和
    MCRYPT_LOKI97
  • 加密数据的模式。这里有几个你可以使用的模式,包括电子密码本(Electronic
    Codebook) 和加密反馈(Cipher Feedback)。该脚本使用
    MCRYPT_MODE_CBC 密码块链接。
  • 一个 初始化向量-也称为 IV
    或者种子,用于为加密算法设置种子的额外二进制位。也就是使算法更难于破解的额外信息。
  • 键和 IV 字符串的长度,这可能随着加密和块而不同。使用
    mcrypt_get_key_size()mcrypt_get_block_size()
    函数获取合适的长度;然后用 substr()
    函数将键的值截取为合适的长度。(如果键的长度比要求的短,别担心,Mcrypt
    会用 0 填充。)

如果有人窃取了你的数据和短语,他们只能一个个尝试加密算法直到找到正确的那一个。因此,在使用它之前我们通过对键使用
md5()
函数增加安全,就算他们获取了数据和短语,入侵者也不能获得想要的东西。

入侵者同时需要函数,数据和口令,如果真是如此,他们可能获得了对你服务器的完整访问,你只能大清洗了。

这里还有一个数据存储格式的小问题。Mcrypt
以难懂的二进制形式返回加密后的数据,这使得当你将其存储到 MySQL
字段的时候可能出现可怕错误。因此,我们使用 base64encode()
base64decode() 函数转换为和 SQL 兼容的字母格式和可检索行。

缓冲区溢出攻击

破解脚本

除了实验多种加密方法,你还可以在脚本中添加一些便利。例如,不用每次都提供键和模式,而是在包含的文件中声明为全局常量。

跨站点脚本攻击

生成随机密码

随机(但难以猜测)字符串在用户安全中很重要。例如,如果某人丢失了密码并且你使用
MD5
哈希,你不可能,也不希望查找回来。而是应该生成一个安全的随机密码并发送给用户。为了访问你站点的服务,另外一个用于生成随机数字的应用程序会创建有效链接。下面是创建密码的一个函数:

<?php
 function make_password($num_chars) {
    if ((is_numeric($num_chars)) &&
        ($num_chars > 0) &&
        (! is_null($num_chars))) {
        $password = '';
        $accepted_chars = 'abcdefghijklmnopqrstuvwxyz1234567890';
        // Seed the generator if necessary.
        srand(((int)((double)microtime()*1000003)) );
        for ($i=0; $i<=$num_chars; $i++) {
            $random_number = rand(0, (strlen($accepted_chars) -1));
            $password .= $accepted_chars[$random_number] ;
        }
        return $password;
     }
}
?>

浏览器内的数据操纵

使用脚本

make_password()
函数返回一个字符串,因此你需要做的就是提供字符串的长度作为参数:

<?php
$fifteen_character_password = make_password(15);
?>

函数按照下面步骤工作:

  • 函数确保 $num_chars 是非零的正整数。
  • 函数初始化 $accepted_chars
    变量为密码可能包含的字符列表。该脚本使用所有小写字母和数字 0 到
    9,但你可以使用你喜欢的任何字符集合。(LCTT
    译注:有时候为了便于肉眼识别,你可以将其中的 0 和 O,1 和 l
    之类的都去掉。)
  • 随机数生成器需要一个种子,从而获得一系列类随机值(PHP 4.2
    及之后版本中并不需要,会自动播种)。
  • 函数循环 $num_chars 次,每次迭代生成密码中的一个字符。
  • 对于每个新字符,脚本查看 $accepted_chars 的长度,选择 0
    和长度之间的一个数字,然后添加 $accepted_chars
    中该数字为索引值的字符到 $password。
  • 循环结束后,函数返回 $password

远程表单提交

许可证

本篇文章,包括相关的源代码和文件,都是在 The Code Project Open License
(CPOL) 协议下发布。

Web 安全性 101

在讨论实现安全性的细节之前,最好从比较高的角度讨论 Web
应用程序安全性。本节介绍安全哲学的一些基本信条,无论正在创建何种 Web
应用程序,都应该牢记这些信条。这些思想的一部分来自 Chris
Shiflett(他关于 PHP 安全性的书是无价的宝库),一些来自 Simson
Garfinkel,还有一些来自多年积累的知识。

规则 1:绝不要信任外部数据或输入

关于 Web
应用程序安全性,必须认识到的第一件事是不应该信任外部数据。外部数据(outside
data) 包括不是由程序员在 PHP
代码中直接输入的任何数据。在采取措施确保安全之前,来自任何其他来源(比如
GET 变量、表单 POST、数据库、配置文件、会话变量或
cookie)的任何数据都是不可信任的。

例如,下面的数据元素可以被认为是安全的,因为它们是在 PHP 中设置的。

清单 1. 安全无暇的代码

$myUsername = ‘tmyer’;

$arrayUsers =array(’tmyer’, ‘tom’, ‘tommy’);

define(”GREETING”, ‘hello there’ .$myUsername);

但是,下面的数据元素都是有瑕疵的。

清单 2. 不安全、有瑕疵的代码

$myUsername =$_POST[‘username’];//tainted!

$arrayUsers =array($myUsername, ‘tom’, ‘tommy’);//tainted!

define(”GREETING”, ‘hello there’ .$myUsername);//tainted!

为什么第一个变量 $myUsername 是有瑕疵的?因为它直接来自表单
POST。用户可以在这个输入域中输入任何字符串,包括用来清除文件或运行以前上传的文件的恶意命令。您可能会问,“难道不能使用只接受字母
A-Z
的客户端(JavaScript)表单检验脚本来避免这种危险吗?”是的,这总是一个有好处的步骤,但是正如在后面会看到的,任何人都可以将任何表单下载到自己的机器上,修改它,然后重新提交他们需要的任何内容。

解决方案很简单:必须对 $_POST[‘username’]
运行清理代码。如果不这么做,那么在使用 $myUsername
的任何其他时候(比如在数组或常量中),就可能污染这些对象。

对用户输入进行清理的一个简单方法是,使用正则表达式来处理它。在这个示例中,只希望接受字母。将字符串限制为特定数量的字符,或者要求所有字母都是小写的,这可能也是个好主意。

清单 3. 使用户输入变得安全

$myUsername = cleanInput($_POST[‘username’]);//clean!

$arrayUsers =array($myUsername, ‘tom’, ‘tommy’);//clean!

define(”GREETING”, ‘hello there’ .$myUsername);//clean!

function cleanInput{

$clean =strtolower;

$clean = preg_replace(”/[^a-z]/”, “”,$clean);

$clean =substr($clean,0,12);

return$clean;

}

规则 2:禁用那些使安全性难以实施的 PHP 设置

已经知道了不能信任用户输入,还应该知道不应该信任机器上配置 PHP
的方式。例如,要确保禁用 register_globals。如果启用了
register_globals,就可能做一些粗心的事情,比如使用 $variable 替换同名的
GET 或 POST 字符串。通过禁用这个设置,PHP
强迫您在正确的名称空间中引用正确的变量。要使用来自表单 POST
的变量,应该引用 $_POST[‘variable’]。这样就不会将这个特定变量误会成
cookie、会话或 GET 变量。

要检查的第二个设置是错误报告级别。在开发期间,希望获得尽可能多的错误报告,但是在交付项目时,希望将错误记录到日志文件中,而不是显示在屏幕上。为什么呢?因为恶意的黑客会使用错误报告信息(比如
SQL
错误)来猜测应用程序正在做什么。这种侦察可以帮助黑客突破应用程序。为了堵住这个漏洞,需要编辑php.ini
文件,为 error_log 条目提供合适的目的地,并将 display_errors 设置为
Off。

规则 3:如果不能理解它,就不能保护它

一些开发人员使用奇怪的语法,或者将语句组织得很紧凑,形成简短但是含义模糊的代码。这种方式可能效率高,但是如果您不理解代码正在做什么,那么就无法决定如何保护它。

例如,您喜欢下面两段代码中的哪一段?

清单 4. 使代码容易得到保护

//obfuscated code

$input = (isset($_POST[‘username’]) ?$_POST[‘username’]:”);

//unobfuscated code

$input = ”;

if (isset($_POST[‘username’])){

$input =$_POST[‘username’];

}else{

$input = ”;

}

在第二个比较清晰的代码段中,很容易看出 $input
是有瑕疵的,需要进行清理,然后才能安全地处理。

规则 4:“纵深防御” 是新的法宝

本教程将用示例来说明如何保护在线表单,同时在处理表单的 PHP
代码中采用必要的措施。同样,即使使用 PHP regex 来确保 GET
变量完全是数字的,仍然可以采取措施确保 SQL 查询使用转义的用户输入。

纵深防御不只是一种好思想,它可以确保您不会陷入严重的麻烦。

既然已经讨论了基本规则,现在就来研究第一种威胁:SQL 注入攻击。

防止 SQL 注入攻击

在 SQL 注入攻击 中,用户通过操纵表单或 GET
查询字符串,将信息添加到数据库查询中。例如,假设有一个简单的登录数据库。这个数据库中的每个记录都有一个用户名字段和一个密码字段。构建一个登录表单,让用户能够登录。

清单 5. 简单的登录表单

Login

Username

Password

这个表单接受用户输入的用户名和密码,并将用户输入提交给名为
verify.php的文件。在这个文件中,PHP 处理来自登录表单的数据,如下所示:

清单 6. 不安全的 PHP 表单处理代码

$okay = 0;

$username =$_POST[‘user’];

$pw =$_POST[‘pw’];

$sql = “selectcountas ctr from users where

username=’”.$username.”‘and password=’”.$pw.”‘ limit 1″;

$result = mysql_query;

while ($data = mysql_fetch_object{

if ($data->ctr == 1){

//they’re okay to enter the application!

$okay = 1;

}

}

if {

$_SESSION[‘loginokay’] = true;

header(”index.php”);

}else{

header(”login.php”);

}

这段代码看起来没问题,对吗?世界各地成百的 PHP/MySQL
站点都在使用这样的代码。它错在哪里?好,记住
“不能信任用户输入”。这里没有对来自用户的任何信息进行转义,因此使应用程序容易受到攻击。具体来说,可能会出现任何类型的
SQL 注入攻击。

例如,如果用户输入 foo 作为用户名,输入 ‘ or ‘1′=’1
作为密码,那么实际上会将以下字符串传递给 PHP,然后将查询传递给 MySQL:

$sql = “select count as ctr from users where

username=’foo’ and password=” or ‘1′=’1′ limit 1″;

这个查询总是返回计数值 1,因此 PHP
会允许进行访问。通过在密码字符串的末尾注入某些恶意
SQL,黑客就能装扮成合法的用户。

解决这个问题的办法是,将 PHP 的内置 mysql_real_escape_string()
函数用作任何用户输入的包装器。这个函数对字符串中的字符进行转义,使字符串不可能传递撇号等特殊字符并让
MySQL 根据特殊字符进行操作。清单 7 展示了带转义处理的代码。

清单 7. 安全的 PHP 表单处理代码

$okay = 0;

$username =$_POST[‘user’];

$pw =$_POST[‘pw’];

$sql = “selectcountas ctr from users where

username=’”.mysql_real_escape_string($username).”‘

and password=’”. mysql_real_escape_string.”‘ limit 1″;

$result = mysql_query;

while ($data = mysql_fetch_object{

if ($data->ctr == 1){

//they’re okay to enter the application!

$okay = 1;

}

}

if {

$_SESSION[‘loginokay’] = true;

header(”index.php”);

}else{

header(”login.php”);

}

使用 mysql_real_escape_string()
作为用户输入的包装器,就可以避免用户输入中的任何恶意 SQL
注入。如果用户尝试通过 SQL
注入传递畸形的密码,那么会将以下查询传递给数据库:

select count as ctr from users where

username=’foo’ and password=’’ or ’1’=’1′ limit 1″

数据库中没有任何东西与这样的密码匹配。仅仅采用一个简单的步骤,就堵住了
Web 应用程序中的一个大漏洞。这里得出的经验是,总是应该对 SQL
查询的用户输入进行转义。

但是,还有几个安全漏洞需要堵住。下一项是操纵 GET 变量。

防止用户操纵 变量

在前一节中,防止了用户使用畸形的密码进行登录。如果您很聪明,应该应用您学到的方法,确保对
SQL 语句的所有用户输入进行转义。

但是,用户现在已经安全地登录了。用户拥有有效的密码,并不意味着他将按照规则行事
——
他有很多机会能够造成损害。例如,应用程序可能允许用户查看特殊的内容。所有链接指向
template.php?pid=33 或 template.php?pid=321 这样的位置。URL
中问号后面的部分称为查询字符串。因为查询字符串直接放在 URL
中,所以也称为 GET 查询字符串。

在 PHP 中,如果禁用了 register_globals,那么可以用 $_GET[‘pid’]
访问这个字符串。在 template.php 页面中,可能会执行与清单 8 相似的操作。

清单 8. 示例 template.php

$pid =$_GET[‘pid’];

//we create an object of a fictional class Page

$obj =new Page;

$content =$obj->fetchPage;

//and now we have a bunch of PHP that displays the page

//……

//……

这里有什么错吗?首先,这里隐含地相信来自浏览器的 GET 变量 pid
是安全的。这会怎么样呢?大多数用户没那么聪明,无法构造出语义攻击。但是,如果他们注意到浏览器的
URL 位置域中的
pid=33,就可能开始捣乱。如果他们输入另一个数字,那么可能没问题;但是如果输入别的东西,比如输入
SQL 命令或某个文件的名称(比如
/etc/passwd),或者搞别的恶作剧,比如输入长达 3,000
个字符的数值,那么会发生什么呢?

在这种情况下,要记住基本规则,不要信任用户输入。应用程序开发人员知道
template.php 接受的个人标识符应该是数字,所以可以使用 PHP 的
is_numeric() 函数确保不接受非数字的 PID,如下所示:

清单 9. 使用 is_numeric() 来限制 GET 变量

$pid =$_GET[‘pid’];

if (is_numeric{

//we create an object of a fictional class Page

$obj =new Page;

$content =$obj->fetchPage;

//and now we have a bunch of PHP that displays the page

//……

//……

}else{

//didn’t pass the is_numeric() test, do something else!

}

这个方法似乎是有效的,但是以下这些输入都能够轻松地通过 is_numeric()
的检查:

100

100.1

+0123.45e6 (科学计数法 —— 不好)

0xff33669f (十六进制 —— 危险!危险!)

那么,有安全意识的 PHP
开发人员应该怎么做呢?多年的经验表明,最好的做法是使用正则表达式来确保整个
GET 变量由数字组成,如下所示:

清单 10. 使用正则表达式限制 GET 变量

$pid =$_GET[‘pid’];

if (strlen{

if (!ereg(”^[0-9]+$”,$pid)){

//do something appropriate, like maybe logging

them outor sending them back to home page

}

}else{

//empty $pid, so send them back to the home page

}

//we create an object of a fictional class Page, which is now

//moderately protected from evil user input

$obj =new Page;

$content =$obj->fetchPage;

//and now we have a bunch of PHP that displays the page

//……

//……

需要做的只是使用 strlen()
检查变量的长度是否非零;如果是,就使用一个全数字正则表达式来确保数据元素是有效的。如果
PID
包含字母、斜线、点号或任何与十六进制相似的内容,那么这个例程捕获它并将页面从用户活动中屏蔽。如果看一下
Page 类幕后的情况,就会看到有安全意识的 PHP 开发人员已经对用户输入 $pid
进行了转义,从而保护了 fetchPage() 方法,如下所示:

清单 11. 对 fetchPage() 方法进行转义

class Page{

function fetchPage{

$sql = “select pid,title,desc,kw,content,

status from page where pid=’

”.mysql_real_escape_string.”‘”;

//etc, etc….

}

}

您可能会问,“既然已经确保 PID 是数字,那么为什么还要进行转义?”
因为不知道在多少不同的上下文和情况中会使用 fetchPage()
方法。必须在调用这个方法的所有地方进行保护,而方法中的转义体现了纵深防御的意义。

如果用户尝试输入非常长的数值,比如长达 1000
个字符,试图发起缓冲区溢出攻击,那么会发生什么呢?下一节更详细地讨论这个问题,但是目前可以添加另一个检查,确保输入的
PID 具有正确的长度。您知道数据库的 pid 字段的最大长度是 5
位,所以可以添加下面的检查。

清单 12. 使用正则表达式和长度检查来限制 GET 变量

$pid =$_GET[‘pid’];

if (strlen{

if (!ereg(”^[0-9]+$”,$pid) &&strlen > 5){

//do something appropriate, like maybe logging

them outor sending them back to home page

}

}else{

//empty $pid, so send them back to the home page

}

//we create an object of a fictional class Page, which is now

//even more protected from evil user input

$obj =new Page;

$content =$obj->fetchPage;

//and now we have a bunch of PHP that displays the page

//……

//……

现在,任何人都无法在数据库应用程序中塞进一个 5,000 位的数值 ——
至少在涉及 GET
字符串的地方不会有这种情况。想像一下黑客在试图突破您的应用程序而遭到挫折时咬牙切齿的样子吧!而且因为关闭了错误报告,黑客更难进行侦察。

缓冲区溢出攻击

缓冲区溢出攻击 试图使 PHP 应用程序中(或者更精确地说,在 Apache
或底层操作系统中)的内存分配缓冲区发生溢出。请记住,您可能是使用 PHP
这样的高级语言来编写 Web 应用程序,但是最终还是要调用 C(在 Apache
的情况下)。与大多数低级语言一样,C 对于内存分配有严格的规则。

缓冲区溢出攻击向缓冲区发送大量数据,使部分数据溢出到相邻的内存缓冲区,从而破坏缓冲区或者重写逻辑。这样就能够造成拒绝服务、破坏数据或者在远程服务器上执行恶意代码。

防止缓冲区溢出攻击的惟一方法是检查所有用户输入的长度。例如,如果有一个表单元素要求输入用户的名字,那么在这个域上添加值为
40 的 maxlength 属性,并在后端使用 substr() 进行检查。清单 13 给出表单和
PHP 代码的简短示例。

清单 13. 检查用户输入的长度

if ($_POST[‘submit’] == “go”){

$name =substr($_POST[‘name’],0,40);

//continue processing….

}

;

Name

“name” id=”name” size=”20″ maxlength=”40″/>

为什么既提供 maxlength 属性,又在后端进行 substr()
检查?因为纵深防御总是好的。浏览器防止用户输入 PHP 或 MySQL
不能安全地处理的超长字符串(想像一下有人试图输入长达 1,000
个字符的名称),而后端 PHP
检查会确保没有人远程地或者在浏览器中操纵表单数据。

正如您看到的,这种方式与前一节中使用 strlen() 检查 GET 变量 pid
的长度相似。在这个示例中,忽略长度超过 5
位的任何输入值,但是也可以很容易地将值截短到适当的长度,如下所示:

清单 14. 改变输入的 GET 变量的长度

$pid =$_GET[‘pid’];

if (strlen{

if (!ereg(”^[0-9]+$”,$pid)){

//if non numeric $pid, send them back to home page

}

}else{

//empty $pid, so send them back to the home page

}

//we have a numeric pid, but it may be too long, so let’s check

if (strlen>5){

$pid =substr;

}

//we create an object of a fictional class Page, which is now

//even more protected from evil user input

$obj =new Page;

$content =$obj->fetchPage;

//and now we have a bunch of PHP that displays the page

//……

//……

注意,缓冲区溢出攻击并不限于长的数字串或字母串。也可能会看到长的十六进制字符串(往往看起来像
xA3 或
xFF)。记住,任何缓冲区溢出攻击的目的都是淹没特定的缓冲区,并将恶意代码或指令放到下一个缓冲区中,从而破坏数据或执行恶意代码。对付十六进制缓冲区溢出最简单的方法也是不允许输入超过特定的长度。

如果您处理的是允许在数据库中输入较长条目的表单文本区,那么无法在客户端轻松地限制数据的长度。在数据到达
PHP 之后,可以使用正则表达式清除任何像十六进制的字符串。

清单 15. 防止十六进制字符串

if ($_POST[‘submit’] == “go”){

$name =substr($_POST[‘name’],0,40);

//clean out any potential hexadecimal characters

$name = cleanHex;

//continue processing….

}

function cleanHex{

$clean = preg_replace(”![][xX]([A-Fa-f0-9]{1,3})!”, “”,$input);

return$clean;

}

” method=”post”

Name

您可能会发现这一系列操作有点儿太严格了。毕竟,十六进制串有合法的用途,比如输出外语中的字符。如何部署十六进制
regex

由您自己决定。比较好的策略是,只有在一行中包含过多十六进制串时,或者字符串的字符超过特定数量(比如
128 或 255)时,才删除十六进制串。

跨站点脚本攻击

在跨站点脚本攻击中,往往有一个恶意用户在表单中(或通过其他用户输入方式)输入信息,这些输入将恶意的客户端标记插入过程或数据库中。例如,假设站点上有一个简单的来客登记簿程序,让访问者能够留下姓名、电子邮件地址和简短的消息。恶意用户可以利用这个机会插入简短消息之外的东西,比如对于其他用户不合适的图片或将用户重定向到另一个站点的
JavaScript,或者窃取 cookie 信息。

幸运的是,PHP 提供了 strip_tags() 函数,这个函数可以清除任何包围在 HTML
标记中的内容。strip_tags() 函数还允许提供允许标记的列表,比如 或 。

清单 16 给出一个示例,这个示例是在前一个示例的基础上构建的。

清单 16. 从用户输入中清除 HTML 标记

if ($_POST[‘submit’] == “go”){

//strip_tags

$name =strip_tags($_POST[‘name’]);

$name =substr($name,0,40);

//clean out any potential hexadecimal characters

$name = cleanHex;

//continue processing….

}

function cleanHex{

$clean = preg_replace

(”![][xX]([A-Fa-f0-9]{1,3})!”, “”,$input);

return$clean;

}

“” method=”post”

Name

“text” name=”name” id=”name” size=”20″ maxlength=”40″/>

从安全的角度来看,对公共用户输入使用 strip_tags()
是必要的。如果表单在受保护区域中,而且您相信用户会正确地执行他们的任务(比如为
Web 站点创建 HTML 内容),那么使用 strip_tags()
可能是不必要的,会影响工作效率。

还有一个问题:如果要接受用户输入,比如对贴子的评论或来客登记项,并需要将这个输入向其他用户显示,那么一定要将响应放在
PHP 的 htmlspecialchars() 函数中。这个函数将与符号、< 和 >
符号转换为 HTML 实体。例如,与符号变成
&。这样的话,即使恶意内容躲开了前端 strip_tags() 的处理,也会在后端被
htmlspecialchars() 处理掉。

浏览器内的数据操纵

有一类浏览器插件允许用户篡改页面上的头部元素和表单元素。使用 Tamper
Data(一个 Mozilla
插件),可以很容易地操纵包含许多隐藏文本字段的简单表单,从而向 PHP 和
MySQL 发送指令。

用户在点击表单上的 Submit 之前,他可以启动 Tamper
Data。在提交表单时,他会看到表单数据字段的列表。Tamper Data
允许用户篡改这些数据,然后浏览器完成表单提交。

让我们回到前面建立的示例。已经检查了字符串长度、清除了 HTML
标记并删除了十六进制字符。但是,添加了一些隐藏的文本字段,如下所示:

清单 17. 隐藏变量

if ($_POST[‘submit’] == “go”){

//strip_tags

$name =strip_tags($_POST[‘name’]);

$name =substr($name,0,40);

//clean out any potential hexadecimal characters

$name = cleanHex;

//continue processing….

}

function cleanHex{

$clean =

preg_replace(”![][xX]([A-Fa-f0-9]{1,3})!”, “”,$input);

return$clean;

}

”” method=”post”

Name

“text” name=”name” id=”name” size=”20″ maxlength=”40″/>

注意,隐藏变量之一暴露了表名:users。还会看到一个值为 create 的 action
字段。只要有基本的 SQL 经验,就能够看出这些命令可能控制着中间件中的一个
SQL 引擎。想搞大破坏的人只需改变表名或提供另一个选项,比如 delete。

图 1 说明了 Tamper Data 能够提供的破坏范围。注意,Tamper Data
不但允许用户访问表单数据元素,还允许访问 HTTP 头和 cookie。

要防御这种工具,最简单的方法是假设任何用户都可能使用 Tamper
Data。只提供系统处理表单所需的最少量的信息,并把表单提交给一些专用的逻辑。例如,注册表单应该只提交给注册逻辑。

如果已经建立了一个通用表单处理函数,有许多页面都使用这个通用逻辑,那该怎么办?如果使用隐藏变量来控制流向,那该怎么办?例如,可能在隐藏表单变量中指定写哪个数据库表或使用哪个文件存储库。有
4 种选择:

不改变任何东西,暗自祈祷系统上没有任何恶意用户。

重写功能,使用更安全的专用表单处理函数,避免使用隐藏表单变量。

使用 md5()
或其他加密机制对隐藏表单变量中的表名或其他敏感信息进行加密。在 PHP
端不要忘记对它们进行解密。

通过使用缩写或昵称让值的含义模糊,在 PHP
表单处理函数中再对这些值进行转换。例如,如果要引用 users 表,可以用 u
或任意字符串(比如 u8y90×0jkL)来引用它。

后两个选项并不完美,但是与让用户轻松地猜出中间件逻辑或数据模型相比,它们要好得多了。

现在还剩下什么问题呢?远程表单提交。

远程表单提交

Web
的好处是可以分享信息和服务。坏处也是可以分享信息和服务,因为有些人做事毫无顾忌。

以表单为例。任何人都能够访问一个 Web 站点,并使用浏览器上的 File >
Save As 建立表单的本地副本。然后,他可以修改 action
参数来指向一个完全限定的 URL(不指向
formHandler.php,而是指向
Submit,服务器会把这个表单数据作为合法通信流接收。

首先可能考虑检查
$_SERVER[‘HTTP_REFERER’],从而判断请求是否来自自己的服务器,这种方法可以挡住大多数恶意用户,但是挡不住最高明的黑客。这些人足够聪明,能够篡改头部中的引用者信息,使表单的远程副本看起来像是从您的服务器提交的。

处理远程表单提交更好的方式是,根据一个惟一的字符串或时间戳生成一个令牌,并将这个令牌放在会话变量和表单中。提交表单之后,检查两个令牌是否匹配。如果不匹配,就知道有人试图从表单的远程副本发送数据。

要创建随机的令牌,可以使用 PHP 内置的 md5()、uniqid() 和 rand()
函数,如下所示:

清单 18. 防御远程表单提交

session_start();

if ($_POST[‘submit’] == “go”){

//check token

if ($_POST[‘token’] ==$_SESSION[‘token’]){

//strip_tags

$name =strip_tags($_POST[‘name’]);

$name =substr($name,0,40);

//clean out any potential hexadecimal characters

$name = cleanHex;

//continue processing….

}else{

//stop all processing! remote form posting attempt!

}

}

$token = md5(uniqid, true));

$_SESSION[‘token’]=$token;

function cleanHex{

$clean = preg_replace(”![][xX]([A-Fa-f0-9]{1,3})!”, “”,$input);

return$clean;

}

” method=”post”

Name

这种技术是有效的,这是因为在 PHP
中会话数据无法在服务器之间迁移。即使有人获得了您的 PHP
源代码,将它转移到自己的服务器上,并向您的服务器提交信息,您的服务器接收的也只是空的或畸形的会话令牌和原来提供的表单令牌。它们不匹配,远程表单提交就失败了。

结束语

本教程讨论了许多问题:

使用 mysql_real_escape_string() 防止 SQL 注入问题。

使用正则表达式和 strlen() 来确保 GET 数据未被篡改。

使用正则表达式和 strlen() 来确保用户提交的数据不会使内存缓冲区溢出。

使用 strip_tags() 和 htmlspecialchars() 防止用户提交可能有害的 HTML
标记。

避免系统被 Tamper Data 这样的工具突破。

使用惟一的令牌防止用户向服务器远程提交表单。

本教程没有涉及更高级的主题,比如文件注入、HTTP
头欺骗和其他漏洞。但是,您学到的知识可以帮助您马上增加足够的安全性,使当前项目更安全。

发表评论

电子邮件地址不会被公开。 必填项已用*标注