构建更加安全的 Web 应用程序
 
2008-12-05 作者:Derek Fong 来源:IBM
 
本文内容包括:
开发人员总是在与 Web 应用程序中的操作和数据篡改作斗争。本文提供了一个保护这些漏洞的框架。可以嵌入这个框架,它为像 Struts 这样常用的表示框架提供了逻辑安全设计。

Web 应用程序安全设计的目的是消除漏洞。仅仅基于用户凭证的已知安全设计元素(如验证和授权)可以很好地满足基本安全要求。不过,需要对收到的客户数据作进一步的审查,以便将安全边界从常用的设计元素扩展到应用程序代码。为了满足这一要求,我提供了一个新的安全设计框架,它保护了两类常见的漏洞:操作篡改和参数操纵(也称为 数据篡改)。

我将在表示层引入这个框架,负责对由浏览器发送的静态数据进行安全检查。这个框架还检测来自浏览器的操作事件,并为 Web 应用程序提供简单的浏览控制。

更多安全性的要求

考虑恶意用户用代理工具劫持由浏览器发送的数据这一情况。后果可能是严重的,因为服务器会处理规定之外的数据。下面的场景更详细地展示了 Web 应用程序的漏洞。

Web 页浏览

Web 页浏览控制属于应用程序的访问策略。访问策略必须指定特定用户角色可能的浏览路径,以防止恶意用户浏览他或她无权浏览的某一页(例如,通过书签)。

HTML 标记注入

与页浏览控制类似,当恶意用户试图在服务器将数据发回浏览器之前修改 HTML 内容时就会发生HTML 标记注入。HTML 标记篡改可能包含超链接、提交按钮或者其他表单标记。例如,如果一个 Web 页包含两个提交按钮,并根据特定条件只显示一个按钮,恶意用户可以使用代理工具在 Web 页显示在浏览器中之前在其中加上第二个按钮。这样用户就可以向他或她无权访问的内容提交操作。

参数操纵

参数操纵(即数据篡改)使恶意用户可以改变浏览器与 Web 应用服务器之间发送的数据。用户通常可以修改包含 URL 查询字符串和隐藏字段的参数。例如,如果 Web 页包含一个表示客户 ID 的隐藏字段,那么恶意用户就可以在浏览器将数据发送给服务器之前改变隐藏字段的值。

我在这里介绍的解决方案会处理 Web 应用程序漏洞,将对应用程序代码的影响降至最低,与应用程序代码松散耦合,而且可以根据将来的需求而扩展。

使用这个框架

可以在以下情况下使用这个保护框架:

  • Web 应用程序/Struts 框架要求简单的浏览控制。
  • Web 应用程序需要确认用户操作事件(如返回和刷新按钮)。
  • Web 应用程序要求保护页面静态数据中的漏洞,如操作、链接、按钮和隐藏字段。

模型结构

为了防止恶意用户劫持 Web 页中的 HTML 静态内容,这个框架使用了服务器端验证技术。这个框架的程序模型如 图 1 所示,它描述了技术设计。

图 1. 保护框架的类图
保护框架的类图

Processor

Processor 定义了保护建模为 ActionParams 的用户操作的操作。它也是处理客户请求的单元素对象。Processor 用 ParamsRepository 注册用户操作,并生成一个引用代码。它通过引用代码验证通过 http 请求发送的用户操作(Post、Get 和 Put)。

ParamsRepository

ParamsRepository 维护对所有 ActionParams 对象的引用。它提供了对在 http 会话中获取和存储 ActionParams 的基本访问操作,并提供了一个生成引用代码的机制。

ActionParams

ActionParams 定义了对所有所支持的查询操作和静态数据通用的接口。它实现了一个 validate 操作以验证查询操作和静态数据。可以扩展 ActionParams 以支持应用程序特有的需求。

FormActionParams

FormActionParams 扩展了 ActionParams 以支持 HTML 表单操作和静态(隐藏)字段。它还覆盖了 validate 方法以支持表单操作验证。

框架对象的合作

本节描述客户机与框架交互以注册和验证用户操作的过程。

客户机注册要受到数据保护的静态内容

首先,客户机创建并向 Processor 传递 ActionParams 对象的一个实例。然后 Processor 和 ParamRepository 交互以存储 ActionParams 实例,并向客户机返回一个引用代码,如 图 2 所示。

图 2. 客户机注册要保护的数据
客户机注册要保护的数据

服务器验证来自客户机的数据

然后 Processor 将特定 ActionParams 的引用代码从客户机转发给 ParamRepository。Processor 与 ActionParams 实例交互以验证用户请求(请参阅 图 3)。

图 3. 服务器验证用户操作
服务器验证用户操作

实现思路

现在,我将讨论在实现这个框架时需要知道的设计细节。

代码惟一性

为每个 ActionParams 生成的引用代码必须在 Web 页中是惟一的,并且应当可以在不同的页面间重复。不过,如果要扩展这个框架以支持双击检测(请参阅 其他扩展),那么引用代码必须在应用程序中是惟一的。

代码生命周期

图 4 描绘了引用代码的生命周期。

图 4. 引用代码生命周期
引用代码生命周期

来自浏览器的每一个 JSP 请求都会生成一组新的引用代码并将它们存储到 http 会话中。当用户向服务器提交请求时,一个引用代码会映射到会话中的 ActionParams。当 Processor 完成了验证后,它会删除 http 会话中的所有代码。

异常处理

这个框架不会向调用者抛出任何异常。可以根据应用程序的需要处理返回的错误代码。一般来说,应用程序应当在发现任何攻击时,中止用户会话并返回到欢迎/登录页。

加密方式

ParamsRepository 用 http 会话存储受保护的信息。还可以加密受保护的数据并将它直接到存储到 Web 页中。向服务器提交数据时,也发送加密的数据。Processor 将解密数据并验证它是否是真正的参数。这种方式会减小会话的大小,不过,它使性能降低很多。

场景分析

下面,我将描述需要保护 JSP 中静态内容的不同场景。然后展示这个保护框架如何在运行时防止漏洞。这些场景可以使您更好地理解这个框架。

场景 1:JavaServer Page scriptlet

为了定义所有超链接和表单隐藏字段的 JSP scriptlet,必须:

  1. 定义每一个 Processor JSP scriptlet 的操作和参数为 ActionParams。
  2. 为每一个超链接定义一个以 ActionParams 为参数的 Processor JSP scriptlet,如 图 5 中的代码所示。
    图 5. 保护超链接的 scriptlet
    保护超链接的 scriptlet
  3. 为隐藏字段定义一个以 ActionParams 为参数的 Processor JSP scriptlet,如 图 6 中的代码所示。
    图 6. 保护隐藏字段的 scriptlet
    保护隐藏字段的 scriptlet

服务器装载 JSP 时,它执行这些 scriptlet 并调用 Processor。Processor 将受保护的数据存储到 http 会话中的 ParamsRepository 中。然后 Processor 向 Web 页返回引用代码并显示为超链接参数或者隐藏参数。引用代码见 图 7 (红色部分)。

图 7. 得到的 HTML
得到的 HTML

场景 2:在正常情况下验证用户操作

在这个场景中,用户单击 Web 页面中的 Submit,然后:

  1. 服务器上的处理程序(如 Struts 中的 RequestProcessor 或者 ActionServlet)调用 Processor。
  2. 然后 Processor 从 http 请求中提取引用代码并发送给 ParamsRepository。由于用户单击了 Submit 按钮,所以浏览器向服务器发送引用代码 x2w3e (请参阅 图 7)。
  3. ParamsRepository 查找匹配的 ActionParam 实例并将它返回给 Processor,如 图 8 所示。
    图 8. 存储在 ParamsRepository 中的 ActionParam
    存储在 ParamsRepository 中的 ActionParam
     
  4. 然后 Processor 用收到的 ActionParams 验证用户提交的操作。这个场景的 ActionParams 实例是一个 FormActionParams,并调用了 FormActionParams 的 validate 方法。由于发送给服务器的数据没有改变,因此 Processor 将返回成功的代码,如 图 9 所示。
    图 9. 用 ParamsRepository 中的 ActionParams 验证 FormActionParams
    用 ParamsRepository 中的 ActionParams 验证 FormActionParams

场景 3:验证用户操作是否篡改数据

在这里,当用户单击 Web 页中的 Submit 时:

  1. 用户在浏览器将请求发送给服务器之前,将隐藏值从 12345 改为 XYZ。
  2. 重复 场景 2 中的第 2 步到第 4 步。
  3. Processor 查明用户请求中的参数与 ParamsRepository 中 FormActionParams 实例中的不一样。Processor 返回错误代码。

如 图 10 所示,ActionParams 包含一个无效的参数 ID XYZ,Processor 将其与 ParamsRespository 中的有效参数 ID 12345 进行比较。

图 10. Processor 验证收到的 ActionParams
Processor 验证收到的 ActionParams

这种验证防止用操作/参数操纵和 HTML 标记注入进行篡改,因为对操作/参数的任何修改都不会通过验证。

场景 4:验证用户操作是否修改引用代码

在这种场景中,当用户单击 Web 页上的 Submit 时:

  1. 用户在浏览器将请求发送给服务器之前,将隐藏字段标记中的引用代码值从 x2w3e 修改为 hackValue。
  2. 服务器端的处理程序(例如 Struts 中的 RequestProcessor 或者 ActionServlet)会调用 Processor。
  3. Processor 从 http 请求中提取引用代码并发送给 ParamsRepository。由于用户修改了值,所以浏览器向服务器发送的引用代码是 hackValue。
  4. ParamsRepository 无法找到匹配的 ActionParams 实例。
  5. 如 图 11 所示,Processor 无法在 ParamsRepository 中找到收到的代码。
    图 11. Processor 无法找到收到的引用代码
    Processor 无法找到收到的引用代码
     
  6. Processor 返回一个错误代码。
  7. 由于 ParamsRepository 规定了特定 Web 页上可以进行的操作,所以这个保护框架会拒绝发送给它的所有不能识别的操作。这防止了 Web 浏览劫持(如书签)。

场景 5:验证用户操作是否没有代码

最后一种情况,当用户单击 Web 页中的 Submit 时:

  1. 用户在浏览器向服务器发送请求之前,删除了引用代码的隐藏字段。
  2. 服务器端的处理程序(例如 Struts 中的 RequestProcessor 或者 ActionServlet)调用 Processor。
  3. Processor 确定从请求中不能提取代码并返回一个错误代码。

示例代码

下面的代码展示了如何在 Java 中实现这个框架。

ParamsRepository

storeParams 方法将 ActionParams 存储到 http 会话里的一个映射中。代码参数标识特定的 ActionParams 实例。因为实现是与调用者隔离的,所以可以用其他类型的实现存储 ActionParams,如 图 12 所示。

图 12. storeParams 方法实现
storeParams 方法实现

retrieveParams 方法根据引用代码返回一个 ActionParams 实例,如 图 13 所示。

图 13. retrieveParams 方法实现
retrieveParams 方法实现

ActionParams

validate 方法可以让 ActionParams 用另一个实例验证自身。它将操作和参数验证委派给不同的方法,如 图 14 所示。

图 14. validate 方法实现
validate 方法实现

FormActionParams

FormActionParams 覆盖了 doValidateParams 方法以验证表单中的静态隐藏字段(请参阅 图 15)。

图 15. doValidateParams 方法
doValidateParams 方法

Processor

Processor 调用 verifyAction 方法以验证用户操作,如果验证失败,它就返回错误代码。表 1 描述了会让 Processor 发出错误代码的可能场景。

表 1. Processor 对于以下场景生成错误代码
 
场景 错误代码 原因
检查 http 请求中是否缺少引用代码。 CODEMISSING_ERR 用户向服务器发送未知请求。
用 verifyWithRepository 方法检查 ParamsRepository 中是否有代码。 INVALIDCODE_ERR 用户向服务器发送以前做过书签的请求。
检查用户是否篡改过 http 请求中的静态数据。 DATATAMPERING_ERR 用户修改了发送给服务器的数据。

verifyAction 方法可以用存储在信息库中的 ActionParams 验证用户操作(请参阅 图 16)。

图 16. 验证操作
验证操作

Struts

可以容易地用 Struts 标记库集成这个保护框架,使这个框架的实现变为透明的。要在 Struts 中集成这个框架,必须继承以下 Struts 标记库。

表单和隐藏标记库

为了保护表单和隐藏字段,要修改 Struts 的 FormTag 和 HiddenTag 以调用 Processor。创建一个子类,它:

  • 扩展 Struts HiddenTag 类,收集所有隐藏字段参数并将它们存储到 pageContext 属性中。
  • 扩展 Struts FormTag 类,覆盖 doAfterBody 方法以向 Processor 注册 pageContext 属性和表单操作。返回的引用代码输出作为隐藏字段(请参阅 图 17)。
图 17. FormTag 的 doAfterBody 方法
FormTag 的 doAfterBody 方法

链接标记库

为了保护链接,可以修改 Struts LinkTag 以调用 Processor。 创建一个子类,它:

扩展 Struts LinkTag 类,覆盖 calculateURL 方法以填充 ActionParams 对象并向 Processor 注册。返回的引用代码附加到 URL 的最后,如 图 18 所示。

图 18. calculateURL 方法

calculateURL 方法

将保护框架集成到 Struts 后,只要在 JSP 中使用 Struts 标记库,就可以自动应用这个框架。

其他扩展

只要扩展 ActionParams,这个框架就可以支持其他类型的保护。类似于 FormActionParams,可以对 ActionParams 扩展下拉框保护,用一组有效的下拉值验证所选的值。通过维护以前处理的代码值,这个框架可以用同步的用户请求探测双击事件。如果收到具有相同代码值的请求,那么可以将以前的响应指定给这个请求。

结束语

本文描述了常见的 Web 应用程序攻击类型,并提供了一个解决这些问题的框架。这个框架覆盖了 Web 应用程序的逻辑安全方面,并保护了 Web 页中的静态数据。虽然这个框架可以减少受攻击的危险,但是设计者应当反复分析应用程序体系结构并找出所有可能的漏洞。这可让您增强或者修改这个框架以防止新的攻击形式。

参考资料

学习 获得产品和技术 讨论
  • Cert.org :跟踪关于安全漏洞和策略的最新消息。
  • BugTraq :在这个公共邮件列表中讨论漏洞和安全隐患。
  • developerWorks blogs :参与 developerWorks 社区。

火龙果软件/UML软件工程组织致力于提高您的软件工程实践能力,我们不断地吸取业界的宝贵经验,向您提供经过数百家企业验证的有效的工程技术实践经验,同时关注最新的理论进展,帮助您“领跑您所在行业的软件世界”。
资源网站: UML软件工程组织