4.15 共享资源相关的问题
由于 Web 应用能同时处理多个请求,因此会在并行处理中出现问题,特别是当操作涉及共享资源时,问题发生的几率尤大。而本节就将讲述因对共享资源的处理不完善而导致的代表性的安全隐患——竞态条件(Race Condition)漏洞。
4.15.1 竞态条件漏洞
概要
共享资源是指,被多个进程或线程同时使用的变量、共享内存、文件、数据库等。如果针对共享资源的互斥锁不完善,就可能会导致竞态条件漏洞。
竞态条件漏洞的影响很多,其中,应用中由竞态条件问题而引起的典型的影响有以下几种。
- 页面上显示其他用户的个人信息(他人问题)
- 数据库信息不一致
- 文件内容被破坏
竞态条件漏洞的对策有如下两项,实施其中一项即可。
- 尽量不使用共享资源
- 针对共享资源实施完善的互斥锁
竞态条件漏洞总览
攻击手段与影响
接下来我们就来看一下竞态条件漏洞引起问题的流程及其影响。此处介绍的案例都是突发性事件,而非蓄意攻击。示例应用由 Java Servlet 编写而成。本书的试验环境的虚拟机中没有准备 Servlet 的运行环境,如果想运行该示例可以安装 Tomcat 等 Servlet 容器。笔者已确认该示例在 Tomcat6.0 中运行正常。
Servlet 的源码如下。
代码清单 C4f-001.java
import java.io.*;
import javax.servlet.http.*;
public class C4f_001 extends HttpServlet {
String name; // 定义为实例变量
protected void doGet(HttpServletRequest req,
HttpServletResponse res)
throws IOException {
PrintWriter out = res.getWriter();
out.print("<body>name=");
try {
name = req.getParameter("name"); // 查询字符串 name
Thread.sleep(3000); // 等待 3 秒(模拟耗时的处理)
out.print(escapeHTML(name)); // 显示用户名
} catch (InterruptedException e) {
out.println(e);
}
out.println("</body>");
out.close();
}
}
该 Servlet 从查询字符串中接收了 name
的值并将其赋值给实例变量 name
,等待 3 秒钟后,再在页面上显示实例变量 name
。等待 3 秒钟是为了模拟耗时很长的处理。 escapeHTML
函数的作用在于防范 XSS(此处省略了该函数的定义)。
接下来,我们使用以下方法执行该 Servlet。打开两个浏览器窗口,在一个窗口中先使用 name=yamada
打开页面。1 秒钟后,再在另一个窗口中使用 name=tanaka
打开页面。
浏览器的显示如下图所示。
图 4-124 执行示例应用
虽然两边都是要将查询字符串中指定的名字显示在页面上,但两个浏览器上都显示了 tanaka 这个名字。这种现象被称为他人问题。显示的不是自己输入的个人信息,而是其他人的信息,这也是一种个人信息的泄漏。
为了理解这个问题,首先要知道 Servlet 类的实例变量是共享资源。默认设置下,每个 Servlet 类只生成一个实例(对象),所有的请求都由这个唯一的实例来处理。因此,实例变量也只有一个,所有的请求处理都共享这个变量(即共享资源)。
下面我们将 yamada 和 tanaka 的处理以时间轴的形式进行整理,如下图所示。
图 4-125 示例的内部处理
首先,yamada 的处理被启动,变量 name
被赋值为 "yamada"
。1 秒钟后,tanaka 的处理也开始进行,变量 name
的值被覆盖为 "tanaka"
。由于此后也一直为 "tanaka"
,因此两个浏览器中都显示了 "tanaka"
这个名字。
安全隐患的产生原因
安全隐患的产生原因有如下两点。
name
是共享变量- 没有对共享变量
name
加上互斥锁
如果没有意识到 Servlet 类的实例变量是共享资源,那么就很可能会在不知不觉中埋下隐患。
对策
竞态条件漏洞的对策有如下两项,实施其中一项即可。
- 尽量不使用共享资源
- 针对共享资源实施完善的互斥锁
下面我们来看看如何对上面的示例实施防范策略。
- 避免使用共享资源
其实上面的示例根本没有必要使用共享资源的变量
name
,使用非共享的局部变量就能解决问题。下面为修改后的代码摘要。try { String name = req.getParameter("name"); // 定义为局部变量 Thread.sleep(3000); // 等待 3 秒(模拟耗时的处理) out.print(escapeHTML(name)); // 显示用户名 } catch (InterruptedException e) { out.println(e); }
- 使用互斥锁
Java 的多线程处理中可以使用
synchronized
语句或synchronized
方法来进行互斥锁。下面展示的就是使用synchronized
语句来进行互斥锁的例子(摘要)。try { synchronized(this) { // 互斥锁 name = req.getParameter("name"); Thread.sleep(3000); // 等待 3 秒(模拟耗时的处理) out.print(escapeHTML(name)); // 显示用户名 } } catch (InterruptedException e) { out.println(e); }
第 2 行中的
synchronized(this)
的意思就是给 Servlet 的实例加上互斥锁。加上了synchronized
语句后,该 Servlet 的synchronized
代码块内便只允许一个线程执行。也就是说,赋值给变量name
后就不会再被其他线程改写了。这里我们将此时各请求的处理依然以时间轴的形式进行整理,如下图所示。
图 4-126 加上互斥锁后的内部处理
由上图可知,在进行
"yamada"
的处理时,"tanaka"
的处理暂时停止并处于待机状态。这会造成应用程序的性能底下。如果对这个 Servlet 同时发出多个请求,那么就会出现需等待请求数 ×3 秒的时间,因此也就很容易招致妨害 Servlet 的攻击(DoS 漏洞)。
鉴于这种情况,建议大家尽量不要使用互斥锁,也就是说不要使用共享资源。如果非用不可,就应当在设计上多下功夫,使互斥锁的耗时尽可能短一些。详情请参考并行处理或多线程编程的参考书。
总结
本节讲述了因对共享资源的互斥锁处理不完善而造成的问题。常见的互斥锁的形式为数据库中的锁(乐观锁和悲观锁),除此之外,在共享变量或文件时也需要用到互斥锁。
尽量不使用共享资源也能够提高应用的性能,而如果用到了共享资源,就需要在设计中下工夫以将互斥锁的处理时间压缩至最短。
参考:Java Servlet 的其他注意点
Servlet 的实例变量也能够像下面这样在 JSP 中定义。
<%! String name; %>
由于使用这种方式定义的变量也是在各请求间共享的,因此也需要加上互斥锁。但考虑到通常情况下并没有必要在 JSP 中定义实例变量,因此不推荐使用这种方法。
另外,由于实现 SingleThreadModel
接口的 Servlet 类能够保证在单线程下运作,因此可以不对 Servler 的实例变量上锁。虽然以前有时也会使用这种方法作为对策,但是在 Servlet2.4 版本以后,随着 SingleThreadModel
接口被弃用(Deprecated)74 ,今后也就不再推荐使用这种方法了。
74 SingleThreadModel 的文档: http://docs.oracle.com/javaee/1.4/api/javax/servlet/SingleThreadModel.html (英语)。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论