用于JSP的XSS漏洞检测工具






4.94/5 (11投票s)
一款用于检测可能导致XSS漏洞的EL不安全使用的工具。
引言
Java Server Pages 2.0 规范引入了一项新功能,即通过 JSP 页面支持表达式语言(Expression Language, EL)。这一增强功能确实简化了在页面任何位置访问存储在 JavaBean 组件或其他 Java 对象(如 request、session、application 等)中的数据。在早期的规范中,这种支持仅限于 JSTL 标签,并且需要使用诸如 c:out
、fmt:formatDate
等 JSTL 标签来访问这些组件,导致代码更为混乱。得益于 JSP 2.0 规范,这种混乱现在已成为历史。然而,这种支持也导致许多人以不当方式使用它,使其应用程序容易受到一种名为 XSS 的攻击。本文提出了防御此类攻击的可能解决方案,并介绍了一款用于查找 JSP 页面中 EL 不当用法的工具。
XSS 漏洞
根据维基百科的说法,XSS(跨站脚本)是一种攻击类型,它使攻击者能够将客户端脚本注入到其他用户查看的网页中。跨站脚本漏洞可能被攻击者用来绕过访问控制,例如同源策略。其影响可能从轻微的骚扰到重大的安全风险不等,具体取决于易受攻击站点处理的数据的敏感性以及站点所有者实施的任何安全缓解措施的性质。XSS 主要有两种类型:持久型和非持久型。下面描述一个非常简单的持久型 XSS 攻击用例:
- 用户 A 访问一个易受 XSS 攻击的社交网站,并访问某个页面,比如一个添加新帖子的页面。
- 用户没有输入文本消息,而是在此消息字段中输入一个 HTML/JavaScript 块,然后点击保存。比如说,一个 JavaScript 块。
- 网站按原样存储该消息。
- 用户 B 访问该消息页面。在查看消息时,JavaScript 执行,并可能将 cookie 和其他头信息发送到用户 A 创建的远程站点。
- 用户 A 现在可以利用这些信息来劫持用户 B 的会话。
JSP EL 与 XSS 利用
假设上述网站是使用 JSP 开发的,输出消息表单的代码如下所示:
<%@ page contentType="text/html;charset=utf-8" language="java" buffer="none" pageEncoding="utf-8"%>
<%@ taglib prefix="c" uri="<a href="http://java.sun.com/jsp/jstl/core">http://java.sun.com/jsp/jstl/core</a>" %>
<%@ taglib prefix="fmt" uri="<a href="http://java.sun.com/jsp/jstl/fmt">http://java.sun.com/jsp/jstl/fmt</a>" %>
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html;charset=utf-8"></meta>
<meta http-equiv="X-UA-Compatible" content="IE=edge"></meta>
<meta http-equiv="Expires" content="0"></meta>
<meta http-equiv="Pragma" content="no-cache"></meta>
<meta http-equiv="Cache-Control" content="no-cache"></meta>
<meta http-equiv="Pragma-directive" content="no-cache"></meta>
<meta http-equiv="cache-directive" content="no-cache"></meta>
<meta name="owner" content="Abra Ka Dabra"></meta>
<meta name="copyright" content="(c) 2020, Future Coding"></meta>
<title>Edit Message Form</title>
<link rel="stylesheet" type="text/css" href="static/css/styles.css"></link>
<link rel="stylesheet" type="text/css" href="static/css/percent-column-system-min.css"></link>
<script type="text/javascript" src="static/js/jquery/jquery-1.9.1.min.js"></script>
</head>
<body>
<div class="pageTitle">Edit Message Form</div>
<div id="frmDiv">
<form name="frmMsg" id="frmMsg" method="post" action="editform.do">
<div>
<label for="txtSubject">Subject :</label><br>
<input type="text" id="txtSubject" name="txtSubject"
size="40" maxlength="80" value="${msgBean.subject}"/>
</div>
<div>
<label for="txtBody">Message :</label><br>
<textarea rows="10" cols="72" id="txtBody"
name="txtBody">${msgBean.body}</textarea>
</div>
<div class="buttonbar">
<button type="button" id="btnPost"
name="btnPost" value="Post" onclick="postMessage()"/>
</div>
<input type="hidden" id="msgId"
name="msgId" value="${msgBean.msgId}"/>
</form>
</div>
</body>
</html>
那么代码中是什么导致了这种攻击?原因有多种。保存消息的代码按原样保存了消息,没有先进行清理。然而,如果你想支持富文本消息,这可能无法做到。检索到的消息文本在发送时也没有经过清理(HTML 转义)。如上代码所示,EL 直接嵌入在 HTML 元素中。幕后实际发生的是,当遇到 EL 时,servlet 只是简单地计算表达式并将结果嵌入到输出流中。例如,在 Tomcat 中,输出消息正文的那一行被翻译成如下所示:
out.write("\r\n");
out.write("<textarea rows=\"10\" cols=\"72\" id=\"txtBody\" name=\"txtBody\">");
out.write((java.lang.String) org.apache.jasper.runtime.PageContextImpl.proprietaryEvaluate("${msgBean.body}", java.lang.String.class,
(PageContext) _jspx_page_context, null, false));
out.write("<textarea>");
out.write
方法不会对值进行清理(编码/转义)。结果,浏览器将该代码视为有效的脚本块并执行它。在这种情况下,脚本块可能会被编写为收集 cookie 和隐藏字段,并使用 AJAX 将这些数据发送到远程站点,从而让攻击者能够访问用户的会话。
可能的解决方案
有多种解决方案可以解决这个问题,并保护您的网站免受 XSS 攻击。这些方案概述如下:
- 关闭所有页面的 EL 支持。这可以通过在 web.xml 中添加以下条目来完成:
- 为特定页面开启 EL 支持。这可以通过在相关页面的
page
指令中指定isELIgnored
属性来完成。 - 永远不要直接使用 EL 表达式。而是使用
c:out
标签来输出值。c:out
默认会对输出的值进行转义。这导致浏览器将代码呈现为 HTML 文本而不是执行它。例如,输出消息字段的行可以重写为如下所示: - 如果你不想使用额外的标签,那么你可以使用
escapeXML
JSTL 函数。在这种情况下,上述代码可以重写为如下所示: - 从 JSP 2.1 开始,可以注册一个
ELResolver
,它可以对 EL 值进行转义。这个ELResolver
由一个自定义监听器注册,该监听器通过 web.xml 进行配置。这种方法的唯一缺点是所有的 EL 值都会被转义,要真正输出自定义的 HTML,你将不得不使用 Scriplet 或 JSP 表达式。另一种方法是编写一个自定义标签,让它输出未转义的文本。更多细节可以在 Chin Huang 写的一篇很好的文章中找到。相应的代码可以在位于 GitHub 的代码库中找到。
<jsp-config>
<!-- Set to true to disable JSP scriptiing syntax -->
<jsp-property-group>
<url-pattern>*.jsp</url-pattern>
<scripting-invalid>false</scripting-invalid>
</jsp-property-group>
<!-- Set to true to disable Expression Language (EL) syntax -->
<jsp-property-group>
<url-pattern>*.jsp</url-pattern>
<el-ignored>false</el-ignored>
</jsp-property-group>
</jsp-config>
<%@ page isELIgnored ="true|false" %>
<div>
<label for="txtSubject">Subject :</label><br>
<input type="text" id="txtSubject" name="txtSubject" size="40"
maxlength="80" value="<c:out value="${msgBean.subject}"/>"/>
</div>
<div>
<label for="txtBody">Message :</label><br>
<textarea rows="10" cols="72" id="txtBody"
name="txtBody"><c:out value="${msgBean.body}"/></textarea>
</div>
<div class="buttonbar">
<button type="button" id="btnPost" name="btnPost"
value="Post" onclick="postMessage()"/>
</div>
<input type="hidden" id="msgId" name="msgId"
value="<c:out value="${msgBean.msgId}"/>"/>
<div>
<label for="txtSubject">Subject :</label><br>
<input type="text" id="txtSubject" name="txtSubject"
size="40" maxlength="80" value="${fn:escapeXml(msgBean.subject)}"/>
</div>
<div>
<label for="txtBody">Message :</label><br>
<textarea rows="10" cols="72" id="txtBody"
name="txtBody">${fn:escapeXml(msgBean.body)}</textarea>
</div>
<div class="buttonbar">
<button type="button" id="btnPost" name="btnPost"
value="Post" onclick="postMessage()"/>
</div>
<input type="hidden" id="msgId" name="msgId"
value="${fn:escapeXml(msgBean.msgId)}"/>
工具
我基于 Jasper 编译器创建了一个小工具,用于检测 JSP 页面中不安全的 EL 表达式用法。你可能知道,在 Servlet 容器中没有直接运行 JSP 的机制。因此,servlet 容器首先将 JSP 页面转换为 servlet,然后这个 servlet 会自动注册以处理相应的 URL。在 Tomcat 下,这种转换是由 Jasper 编译器(org.apache.jasper.compiler.Compiler
)完成的。JSP 页面的解析由 Parser
类(org.apache.jasper.compiler.Parser
)完成,最后由 Generator
类(org.apache.jasper.compiler.Generator
)生成 servlet 代码。Parser
类解析页面并输出一个包含节点的列表。每个节点是 JSP 页面或 JSP 文档(XML)的内部数据表示。Generator
类使用一个节点访问器(Node visitor)以递归方式遍历节点列表并生成 servlet 代码。有关各种节点类型的更多详细信息,可以在位于此处的 Jasper 编译器文档中找到。
我使用了对应于 Tomcat 6.0.36 版本的 Jasper 编译器。虽然这个工具实际上从未编译 JSP 页面,但它仍然创建了 Compiler
类的实例。这是必需的,因为解析器的内部设计需要一个对 Compiler
实例的引用。JspCompilationContext
类充当了整个 JSP 引擎中使用的各种事物的占位符。我将所有类放在 org.apache.jasper.compiler
包中的原因是,ParserController
、Parser
、Node
、NodeVisitor
类具有包作用域,无法在该包之外使用。在这种情况下,实际的验证是由 NodeVisitor
类(org.apache.jasper.compiler.NodeVisitor
)执行的。除了标记不安全的 EL 使用外,该工具还检测 Scriplet 的使用并将其标记为错误。
使用工具
作为一个基于 Java 的工具,它需要 Java JRE 6.0 或更高版本。与任何 Java 实用程序一样,该工具从命令提示符运行。在运行该工具之前,请确保已设置 classpath 以包含“工具依赖项”下提到的所有运行时依赖项。运行该工具的命令行是:
java -cp %CLASS_PATH% org.apache.jasper.compiler.JSPValidator -root <ROOT_DIR> -skipPath
<PATTERNS> -libs <LIB_DIR> -ignoreTags <IGNORE_TAGS> reportFile <REPORT_OUTPUT_FILE>
其中
CLASS_PATH
- 表示运行时依赖项(jar 文件)和工具 jar 文件的列表,由File.pathSeperator
分隔。例如:lib/*;build/dist/jspvalidator-1.0.3.jar。ROOT_DIR
- 表示包含所有 jsp、jspf、jspx 文件的根文件夹。PATTERNS
- 表示由File.pathSeperator
分隔的 Ant 风格的文件名或文件夹名模式列表。LIB_DIR
- 表示包含附加 jar 文件的文件夹,这些 jar 文件在 JSP 页面中通过import
指令或taglib
指令引用。IGNORE_TAGS
- 一个逗号分隔的 XML 限定标签名列表。例如:"c:out, spring:message"
。REPORT_OUTPUT_FILE
- 打印扫描结果的文件的完整路径和名称。
工具依赖项
除了 Java 运行时,该工具还有以下运行时依赖项。
Jar 名称 | 目的 |
---|---|
commons-cli-1.2.jar | 命令行解析工具 |
commons-lang-2.6.jar | 用于 java.lang API 的 Commons 辅助工具 |
commons-io-2.4.jar | Commons IO 工具 |
jasper-6.0.36.jar | Jasper 编译器 |
jasper-el-6.0.36.jar | Jasper EL 支持 |
servlet-api-2.4.jar | Servlet API 参考实现 |
jsp-api-2.1.jar | JSP API 参考实现 |
juli-6.0.36.jar | 日志支持 |
ecj-4.2.2.jar | 日志支持 |
历史
- 2013年3月6日 - 初始发布。
- 2013年3月29日 - 增加了扫描自定义标签属性的支持,以及忽略标签和忽略文件的功能。