CHARLIE SAYS

查理如是说
DATE 2026-08-24
THEME
SERIES / SECURITY / P-021 · 开发安全

开发安全 003:XSS 详解

XSS 是跨站脚本攻击(Cross Site Scripting),为不和层叠样式表(Cascading Style Sheets, CSS)的缩写混淆,故将跨站脚本攻击缩写为 XSS。恶意攻击者往 Web 页面里插入恶意 Script 代码,当用户浏览该页之时,嵌入其中 Web 里面的 Script 代码会被执行,从而达到攻击用户的目的。

XSS 简介

举一个简单的例子,就是留言板。我们知道留言板通常的任务就是把用户留言的内容展示出来。正常情况下,用户的留言都是正常的语言文字,留言板显示的内容也就没毛病。然而这个时候如果有人不按套路出牌,在留言内容中丢进去一行:

<script>alert("aaa")</script>

那么留言板界面的网页代码就会变成形如以下:

<html>
<head>
<title>Board</title>
</head>
<body>
<div id="board">
<script>alert("aaa")</script>
</div>
</body>
</html>

那么这个时候问题就来了,当浏览器解析到用户输入的代码那一行时会发生什么呢?答案很显然,浏览器并不知道这些代码改变了原本程序的意图,会照做弹出一个信息框。

既然能够执行脚本,那么,这些脚本完全可以是:

  • 链接劫持:
<script>window.location.href="http://www.baidu.com";</script>
  • 盗取 cookie:
<script>alert("document.cookie");</script>

对于攻击者来说,能够让受害者浏览器执行恶意代码的唯一方式,就是把代码注入到受害者从网站下载的网页中,这就是 XSS 攻击。

XSS 攻击类型

通常 XSS 攻击分为:反射型 XSS 攻击存储型 XSS 攻击DOM 型 XSS 攻击。同时注意以下例子只是简单的向你解释这三种类型的攻击方式而已,实际情况比这个复杂,具体可以再结合下文”XSS 攻击再深入理解”一节。

反射型 XSS 攻击

反射型的攻击需要用户主动的去访问带攻击的链接,攻击者可以通过邮件或者短信的形式,诱导受害者点开链接。如果攻击者配合短链接 URL,攻击成功的概率会更高。

在一个反射型 XSS 攻击中,恶意文本属于受害者发送给网站的请求中的一部分。随后网站又把恶意文本包含进用于响应用户的返回页面中,发还给用户。

我们来看下面这个场景:

  1. 用户误点开了带攻击的 url:http://xxx?keyword=<script>alert('aaa')</script>
  2. 网站给受害者的返回中包含了来自 URL 的恶意文本
  3. 用户的浏览器收到文本后执行页面,会在网页中弹窗 aaa
sequenceDiagram
    participant A as 攻击者
    participant U as 受害者
    participant S as 目标网站
    A->>U: 邮件/短信诱导点击恶意链接(常配合短链接)
    U->>S: 请求中携带恶意文本(URL 参数)
    S-->>U: 返回页面中"反射"了该恶意文本
    U->>U: 浏览器解析页面,执行恶意脚本

注意,这里如果页面是后端模板渲染生成,keyword 参数可能是在 model 里面设置参数的。

存储型 XSS 攻击

这种攻击方式恶意代码会被存储在数据库中,其他用户在正常访问的情况下,也有会被攻击,影响的范围比较大。

我们来看这种攻击的一个场景:

  1. 攻击者通过评论表单提交将 <script>alert('aaa')</script> 提交到网站
  2. 网站后端对提交的评论数据不做任何操作,直接存储到数据库中
  3. 其他用户正常访问网站,并且需要请求网站的评论数据
  4. 网站后端会从数据库中取出数据,直接返回给用户
  5. 用户得到页面后,直接运行攻击者提交的代码 <script>alert('aaa')</script>,所有用户都会在网页中弹出 aaa 的弹窗

DOM 型 XSS 攻击

基于 DOM 的 XSS 攻击是反射型攻击的变种。服务器返回的页面是正常的,只是我们在页面执行 js 的过程中,会把攻击代码植入到页面中。

我们来看这种攻击的一个场景:

  1. 用户误点开了带攻击的 url:http://xxx?name=<script>alert('aaa')</script>
  2. 网站给受害者返回正常的网页
  3. 用户的浏览器收到文本后执行页面合法脚本,这时候页面恶意脚本会被执行,会在网页中弹窗 aaa

这种攻击方式发生在我们合法的 js 执行中,服务器无法检测我们的请求是否有攻击的危险。

三种类型对比

类型恶意代码存放位置触发方式影响范围
反射型受害者请求的 URL 中用户主动点击恶意链接单个用户
存储型服务端数据库中其他用户正常访问页面即触发所有访问用户,范围大
DOM 型前端 JS 执行过程中页面合法脚本解析时植入单个用户,服务端无法检测

XSS 攻击的危害

  • 通过 document.cookie 盗取 cookie
  • 使用 js 或 css 破坏页面正常的结构与样式
  • 流量劫持(通过访问某段具有 window.location.href 定位到其他页面)
  • Dos 攻击:利用合理的客户端请求来占用过多的服务器资源,从而使合法用户无法得到服务器响应
  • 利用 iframe、frame、XMLHttpRequest 或上述 Flash 等方式,以(被攻击)用户的身份执行一些管理动作,或执行一些一般的如发微博、加好友、发私信等操作
  • 利用可被攻击的域受到其他域信任的特点,以受信任来源的身份请求一些平时不允许的操作,如进行不当的投票活动
  • DOS(拒绝服务)客户端浏览器
  • 钓鱼攻击,高级的钓鱼技巧
  • 劫持用户 Web 行为,甚至进一步渗透内网
  • 蠕虫式挂马攻击、刷广告、刷浏量、破坏网上数据

XSS 攻击的防御

XSS 攻击其实就是代码的注入。用户的输入被编译成恶意的程序代码。所以,为了防范这一类代码的注入,需要确保用户输入的安全性。对于攻击验证,我们可以采用以下两种措施:

  • 编码:就是转义用户的输入,把用户的输入解读为数据而不是代码
  • 校验:对用户的输入及请求都进行过滤检查,如对特殊字符进行过滤,设置输入域的匹配规则等

具体比如:

  • 对于验证输入,我们既可以在服务端验证,也可以在客户端验证
  • 对于持久性和反射型攻击,服务端验证是必须的,服务端支持的任何语言都能够做到
  • 对于基于 DOM 的 XSS 攻击,验证输入在客户端必须执行,因为从服务端来说,所有发出的页面内容是正常的,只是在客户端 js 代码执行的过程中才发生可攻击
  • 但是对于各种攻击方式,我们最好做到客户端和服务端都进行处理

其它还有一些辅助措施,比如:

  • 入参长度限制:通过以上的案例我们不难发现 xss 攻击要能达成往往需要较长的字符串,因此对于一些可以预期的输入可以通过限制长度强制截断来进行防御
  • 设置 cookie httponly 为 true(具体请看下文的解释)

escapeHTML

前端:

// util 封装可以参考 HTMLParser.js,或者自己封装
util.escapeHtml(html);

后端,推荐使用 Apache Commons 包下 StringEscapeUtils——用于正确处理转义字符,产生正确的 Java、JavaScript、HTML、XML 和 SQL 代码:

// encode html
System.out.println(StringEscapeUtils.escapeHtml("<a>abc</a>"));
System.out.println(StringEscapeUtils.unescapeHtml("&lt;a&gt;abc&lt;/a&gt;"));
// encode js
System.out.println(StringEscapeUtils.escapeJavaScript("<script>alert('123')<script>"));
System.out.println(StringEscapeUtils.unescapeJavaScript("<script>alert(\'123\')<script>"));

过滤或者校验

校验是一种过滤用户输入以至于让代码中恶意部分被移除的行为。校验都是通过一定的经验和规则,对用户的输入进行匹配、过滤,去除掉存在攻击风险的部分。

我们可以通过黑名单的方式和白名单的方式来设置我们的规则,对用户提交的数据进行有效性验证,仅接受符合我们期望格式的内容提交,阻止或者忽略除此外的其他任何数据:

  • 黑名单:我们可以把某些危险的标签或者属性纳入黑名单,过滤掉它。比如:
<script>alert(1)</script>
<a href="javascript:alert(1)"></a>
<img src="abc" onerror="alert(1)"/>
  • 白名单:这种方式只允许部分标签和属性,不在这个白名单中的,一律过滤掉它

这里举个例子,富文本的防御:富文本的情况非常的复杂,js 可以藏在标签里、超链接 url 里、各种属性里,所以我们不能用上面的方法做简单的转义,因为情况实在太多了。思路就是黑白名单校验,这里提供一个包(jsoup/cheerio 类工具)帮助我们去解析 html 树状结构,它使用起来和 jquery 非常的类似:

npm install cheerio --save
var xssFilter = function (html) {
    if (!html) return '';
    var cheerio = require('cheerio');
    var $ = cheerio.load(html);
    // 白名单
    var whiteList = {
        'html': [''],
        'body': [''],
        'head': [''],
        'div': ['class'],
        'img': ['src'],
        'a': ['href'],
        'font': ['size', 'color']
    };
    $('*').each(function (index, elem) {
        if (!whiteList[elem.name]) {
            $(elem).remove();
            return;
        }
        for (var attr in elem.attribs) {
            if (whiteList[elem.name].indexOf(attr) === -1) {
                $(elem).attr(attr, null);
            }
        }
    });
    return $.html();
}

console.log(xssFilter('<div><font color="red">你好</font><a href="http://www.baidu.com">百度</a><script>alert("哈哈你被攻击了")</script></div>'));

CSP(Content Security Policy)

内容安全策略(Content Security Policy,简称 CSP)是一种以可信白名单作机制,来限制网站中是否可以包含某来源内容。

CSP 对你用于浏览页面的浏览器做出了限制,以确保它只能从可信赖来源下载的资源。资源可以是脚本、样式、图片或者其他被页面引用的文件。这意味着即使攻击者成功地在你的网站中注入了恶意内容,CSP 也能免于它被执行——例如外部攻击代码 <script src='ls.js'></script> 将被拦截。

默认配置下不允许执行内联代码(<script> 块内容、内联事件、内联样式),以及禁止执行 eval()new Function()setTimeout([string], ...)setInterval([string], ...)

示例:

只允许本站资源:

Content-Security-Policy: default-src 'self'

允许本站的资源以及任意位置的图片以及 script-src 指定来源下的脚本:

Content-Security-Policy: default-src 'self'; img-src *; script-src ...

XSS 攻击再深入理解

对 script 再做阐述

有很多初学者以为只要对 <script> 进行替换就可以了?其实不是,用户的输入通过 <script>这里是脚本</script> 插入脚本只是最基本的一种方式,其它方式还有:

<!-- 注释方法防止过滤 -->
<script>alert/*注释*/('XSS')</script>

<!-- img 链接地址 xss -->
<img src="javascript:alert('XSS')">

<!-- 加载图像失败执行 -->
<img src='null' onerror="alert('XSS')">

<!-- 利用 onload、onXXX 事件 -->
<iframe onload="alert('XSS')">

<!-- a 链接的 xss -->
<a href="javascript:alert('XSS')"></a>

在 css 样式中加入:

body { background-image: url(javascript:alert('XSS')) }

csrf 攻击其实是不能盗用 cookie 的,它只是以当前的名义进行恶意操作;而 xss 攻击是可以直接盗用 cookie。

那盗用 cookie 的危害是什么?比如拿到用户的 cookie 信息,然后传送到攻击者自己的服务器,从 cookie 中提取敏感信息,拿到用户的登录信息;或者攻击者可以通过修改 DOM 在页面上插入一个假的登陆框,也可以把表单的 action 属性指向他自己的服务器地址,然后欺骗用户提交自己的敏感信息。

这就是为什么 cookie 也是要防御的,比如:

  • 设置 http-only
  • 设置 cookie 的有效周期

XSS 攻击和 CSRF 攻击配合

一般攻击可能不是单一的行为,而是可能会组合攻击;比如 xss 攻击一般还可以配合 csrf 攻击进行配合攻击,这里给个例子,方便你理解;注意,只是仅仅方便你理解,实际不是这么简单。

假设你可以通过如下 GET 请求方式进行修改密码,这是典型的 csrf 攻击方式(详见CSRF 详解):

http://127.0.0.1/test/vulnerabilities/csrf/?password_new=123456&password_conf=123456&Change=Change

那么你可以通过如下方式 xss 攻击添加脚本:

<script type="text/javascript" src="http://127.0.0.1/test/vulnerabilities/csrf/?password_new=123456&password_conf=123456&Change=Change#"></script>

对于后端渲染 HTML 框架

在实际的开发中,后端框架(比如 SpringMVC 相关框架)modelAndView 是可以通过 viewResolver 渲染生成 html 的;而一般的视图渲染解析框架都已经可以处理一部分 xss 攻击的情况,比如 thymeleaf、freemarker 等等。

这里要提醒你的是避免误用,举个 thymeleaf 的例子,这个漏洞是我在实际的渗透测试中遇到的一个 xss 攻击。

html 页面,这是一个封装的一个消息 module:

<div id="msgContainer">
    <div th:utext="${msg}"></div>
</div>

后端添加:

// param
String param = "your input name";
// 后端根据不同的处理情况,返回不同的信息
String msg = "<b>Warning</b>: this is pdai's warning, invalid for your input name: " + param;
model.addMsg("msg", msg);

这里后端的本意是期望封装一个 msg 组件,并且可以通过 html 方式高亮一些关键字,做到最大程度的通用。

但问题是 th:utext 是不做内容编码的,而且存在输入参数(可能这两块是不同的人写的,写后端 msg 组件的人觉得 <b>Warning</b> 很方便;但是不会意识到,其他人可能会添加用户的输入参数进来)。一旦用户的输入中包含脚本,将导致 xss 注入的可能。

对于前端渲染 HTML

在使用 .innerHTML.outerHTMLdocument.write() 时要特别小心,不要把不可信的数据作为 HTML 插到页面上,而应尽量使用 .textContent.setAttribute() 等。

如果用 Vue/React 技术栈,并且不使用 v-html/dangerouslySetInnerHTML 功能,就在前端 render 阶段避免 innerHTML、outerHTML 的 XSS 隐患。

DOM 中的内联事件监听器(如 location、onclick、onerror、onload、onmouseover 等)、<a> 标签的 href 属性、JavaScript 的 eval()setTimeout()setInterval() 等,都能把字符串作为代码运行。如果不可信的数据拼接到字符串中传递给这些 API,很容易产生安全隐患,请务必避免。

在操作时可以参考如下:

  • 前端有个 JS 库叫 HTMLParser.js,可以用来 encodeHtml
  • 有一个类库是针对防 XSS 攻击的:js-xss

浏览器之间对 XSS 处理有差异

对于有些 xss 漏洞可能在某些浏览器无法复现的问题?在某些浏览器某些版本下:

  • chrome 浏览器自带防御,可拦截反射性 XSS(HTML 内容和属性)
  • firefox 不会自动拦截

这个在做渗透测试,或者开发修复测试时要注意下这个问题。

参考文章

系列导航

← 网络协议 003:网络基础(2):7层协议,4层,5层 目录 开发工具 003:Google Guava包 →
← 返回文章列表