信息处理安全保障支援士 2023 年秋季午后问 1 解说——16 条评论只显示 2 条的存储型 XSS

· 更新日期: · · 信息处理安全保障支援士, 注册安全专家, XSS, 跨站脚本攻击, Web应用程序, 信息安全, 漏洞, IPA, 会话管理

更新记录(仅首版,2026年07月29日 发布)
首次发布
引用本文(DOI(已登记存档): 10.5281/zenodo.22175212)

以下 DOI 指向先前登记的存档,内容可能与当前正文不同。引用当前正文时,请使用本页网址。

Go Komura(2026)。《信息处理安全保障支援士 2023 年秋季午后问 1 解说——16 条评论只显示 2 条的存储型 XSS》。小村软件有限公司。 https://comcomponent.com/zh-CN/blog/sc-exam-r5a-pm-q1-stored-xss/

DOI(已登记存档)
10.5281/zenodo.22175212
DOI(上次登记版本)
10.5281/zenodo.22175213

“明明应该有 16 条评论,却只显示 2 条。”

情報処理安全確保支援士試験(信息处理安全保障支援士考试)2023 年度秋季午后问 1,就是从这样一条用户咨询开始的1。页面跳转没有任何异常,也没有报错。只是显示的条数对不上。

原因是存储型跨站脚本攻击(XSS)。但这道题有意思的地方,不在于 XSS 这个答案本身,而在于 Q 公司(题干中登场的电商经营者)本来就已经具备的那些“看着像模像样的对策”,被逐一穿透。评论标题的 50 字限制被突破,上传所需的令牌是按正规步骤拿到的,窃取到的会话 ID 也没有送往外部服务器就被带走了。

本文按 “发生了什么 → 哪些痕迹是答案的依据 → 哪些对策才拦得住” 的顺序来读解。不只是 XSS 这个名字,还要梳理没起作用的对策和本该起作用的对策是在哪里分道扬镳的

本文既讲考试的参考答案及其依据,也讲可用于实务的 XSS 对策全貌。请按各自的目的,从下面的入口开始读。

阅读目的 先打开哪里 如何读下去
想重做一遍这道题 第 2 章的小题对应表 打开试题册,在第 4 到 8 章确认参考答案和依据
想知道 16 条为什么看成 2 条 第 3 章的症状 抓住显示与 HTML 的差异,再走向第 5 章的分多次投稿
想把信息被带走的路径通看一遍 第 7 章的时序图 把投稿、受害者侧的执行、攻击者侧的回收分开来追
想用于实务的对策和评审 第 9 章的对策对比第 10 章的确认项 区分根本性解决与辅助性对策,确认生效范围和局限

第 4 到 8 章的参考答案是考试的答案,CSP 和图片重新编码等则是向实务延伸的补充。特别是第 8 章,要把题干写明的条件和改变配置后的条件分开来看。

另外,关于该以什么为基准来俯瞰确认整个网站的安全性,把 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',嵌入的内联脚本从一开始就不会执行

这里说的“起作用”,意思是能拦住这道题里的这条攻击链。并不是说光靠 HttpOnly 或图片重新编码,XSS 本身就消失了。第 9 章会确认手法改变后的局限。

图中实线表示始终成立的关系,虚线表示带条件的关系(成立条件写在详情页各关系的说明中)。关系的完整列表(共 25 条,附依据与确信度)以及主要概念的定义,汇总在知识地图详情页(日文)。数据:JSON-LD / Turtle

2. 关于题材——出处与本文的处理方式

本文要讲的是下面这道题。

出处:2023 年度秋季 信息处理安全保障支援士考试 午后 问 1

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 和密码认证,把会话 ID 作为 cookie 签发出去
商品评论 只有已登录的会员才能投稿。评论标题有 50 字、评论详情有 300 字的输入字数限制,两者都是自由填写
会员个人资料 提供上传头像图片的页面和登记信用卡信息的页面。两者都只有已登录会员才能使用
头像图片的上传 /user/upload 把图片文件和令牌作为参数发送。只有令牌与 /user/profile 签发的一致时才会成功
头像图片的显示 上传的头像图片会显示在会员个人资料设置页面和评论页面上

最后两行是思考“上传需要什么”和“上传之后谁能读到”的线索。令牌的获取在第 6 章确认,经由图片的回收在第 7 章确认。

3. 症状——16 条为什么变成了 2 条

先比较“页面上的条数”和“HTML 里的条数”

有会员来问:“素色 T 恤的评论页面(页面 V)本该显示 16 条评论,却只显示 2 条。”开发部的 N 打开页面 V,屏幕上看到的是这样:

  • 页头写着“16 条评论”
  • 会员 A 的评论 1 条(标题“Good”,正文“Nice shirt!”)
  • 会员 B 的评论 1 条
  • 末尾写着“以上,共 16 条评论”

条数显示的是 16 条,实际排出来的只有 2 条。这时确认 HTML 就会发现,会员 A 的投稿存在 15 条,而且其中嵌着一段很长的脚本。

也就是说,评论确实按 16 条输出到了 HTML 里。尽管如此只看得见 2 条,是因为其中大部分变成了 <script> 元素的内容,浏览器不再把它当作“应当显示的内容”。

留在页面上的,是第 1 条的前半和第 15 条的后半

被吞掉的范围,准确地说并不是“整整 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(1):答案与用于判断的痕迹

小题 1 要求从 DOM Based XSS、存储型 XSS、反射型 XSS 三个选项中,选出这次攻击所利用的 XSS 漏洞的种类。正确答案是存储型 XSS3

依据是投稿的字符串被保存在服务器上,并原样输出到了 HTML 中。不要根据脚本执行后所用 API 的名字来选种类。

而关于这道小题,IPA 的评分讲评是这样写的。4

正答率处于平均水平,但可能是因为脚本中使用了 DOM,误答为 “DOM Based XSS” 的考生时有所见。

题干中出现的攻击脚本用了 XMLHttpRequest,把响应作为 DOM 接收,并用 getElementById 取元素。它确实在碰 DOM。然而,这与漏洞的种类无关

分界线是“攻击字符串在哪里变成脚本”

区分这三种类型的,是攻击字符串在哪里变成可执行的代码,也就是脆弱的输出位置(Sink)在服务器端还是在浏览器端。

种类 Sink(攻击字符串变得可执行的位置) 攻击字符串从哪里来
反射型 XSS 服务器拼装 HTML 的处理 攻击者准备的 URL 参数等,即该请求本身
存储型 XSS 服务器拼装 HTML 的处理 服务器端已保存的数据
DOM Based XSS 浏览器上的 JavaScript(向 innerHTML 赋值、evaldocument.write 等) URL 的片段、postMessage、从服务器收到的值等

这道题里,服务器把攻击者投稿的评论标题原样嵌入 HTML 返回。Sink 在服务器端的输出处理上,而那个字符串被保存进数据库,此后分发给每一个打开页面 V 的人。所以是存储型。

浏览器上的 JS把它交给了innerHTML 等它在服务器拼装的HTML 里面已保存(此后输出给所有人)未保存(只在那次请求中)攻击者的字符串作为脚本被执行了它是在哪里变得可执行的DOM Based XSS那个字符串是否保存在服务器上存储型 XSS反射型 XSS

另外,把问题简化成“只要服务器的响应里有攻击字符串就是存储型或反射型”是危险的。即使服务器把值作为无害的文本或 JSON 返回,只要浏览器端的 JavaScript 把它赋给 innerHTML,就成了 DOM Based XSS(这就是由已保存的值引发的所谓 stored DOM XSS)。这种情况下该修的不是服务器的输出处理,而是客户端的 Sink,所以请不要按响应里能不能看到字符串来判断,而要按它在哪里变得可执行来判断

嵌入的脚本做了什么(碰 DOM、通信、读 cookie)和那段脚本是怎么混进页面的,必须分开考虑。之所以容易混淆很麻烦,不是因为选对策时会为难,而是因为会选错对策。判断成 DOM Based XSS,就会得出“改客户端的 JavaScript 就行”的结论,可实际该改的是服务器端的输出处理。

小题 1(2):对策

小题 1(2) 要求用 30 字以内回答 Web 应用 Q 中的对策。

参考答案: “在输出评论标题之前施加转义处理”。3

答案的依据: 要改的地方不是受理投稿的页面,而是把已保存的评论标题嵌入 HTML 的处理。

这里“在输出之前”这个顺序被写明了,很重要。不是在接收输入时转义,而是在即将作为 HTML 输出之前,按输出目的地的上下文来转义。IPA《安全网站构建指南》中,XSS 的根本性解决方案的第一条也是“对输出到网页的所有要素施加转义处理”5

“SQL 注入里不是输入的转义更重要吗?”

从这里开始,是把小题 1(2) 的“在输出之前”向实务延伸的补充。请与小题的参考答案本身分开来读。

先说结论:SQL 注入同样不能把“输入时转义”当作对策

IPA 列为 SQL 注入根本性解决方案的是“SQL 语句的拼装全部用占位符实现”。虽然也提到了在不得不用字符串拼接构建 SQL 语句时可以用转义处理作为替代,但那里写的是“用字符串拼接来拼装 SQL 语句时,要使用进行转义处理等操作的数据库引擎 API,正确构成 SQL 语句的字面量”,也就是说转义是在构成 SQL 语句的那一刻做的6。不是在接收输入的时候。

这就与 HTML 的情形结构完全相同。共通的原则是这样。

转义要在数据将走向哪种语法世界已经确定的地方进行。

  • 要作为 HTML 输出,就做 HTML 的转义
  • 要成为 SQL 语句的一部分,就用占位符(万不得已时,作为 SQL 字面量做转义)
  • 要成为 shell 命令的一部分,就按 shell 的规矩处理

不在输入时处理,而在输出目的地确定的地方处理

为什么输入时不行?因为在接收输入的那一刻,这份数据将来走向哪里还没有确定。同一条评论正文,既会走向 HTML 页面,也会走向 CSV 导出、JSON 的 API 响应、通知邮件和日志。如果在输入时做了 HTML 转义,CSV 里就会原样出现 &amp; 这样的字符串,数据库里的内容也会与原始输入不同,检索和统计都会出错。这还是重复转义的温床。

那么输入侧什么都不用做吗

不是。只不过输入侧要做的不是转义,而是校验(Validation)。二者的职责不同。

  • 在输入处“拦下” ——不接受规格上不可能出现的值。邮政编码栏拒绝非 7 位数字,数量栏拒绝负数。这是为了保证数据正确性而独立需要的处理
  • 在输出处“转义” ——把已接受的数据,用它将走向的那种语法安全地表达出来。这才是漏洞的根本性解决方案

还有一点很重要:不要对输入侧的校验寄予过多的 XSS 防护期待。IPA 在讲 XSS 时,提到确认输入值是否符合应用程序规格的做法之后,明确写道该对策的有效性是有限的,当应用程序要求的规格允许输入范围广泛的字符种类时就起不到对策作用,因此不推荐依赖这种方法5

这次的评论标题和评论详情,正是自由填写、允许广泛字符种类的规格。虽然有 50 字和 300 字的上限,但让脚本跑起来所需的字数比这少得多,所以有上限这件事本身并不构成防御。Q 公司实际发生了什么,接下来的小题 2 会看到。

5. 50 字限制是怎么被突破的——小题 2

小题 2 是这道题的精彩之处。

关于图 3,请用 50 字以内回答:是用什么方法让超过输入字数限制长度的脚本得以执行的。

参考答案:分多次进行了能让 HTML 被注释掉并连成一段脚本的投稿”。3

答案里不只要有多次投稿,还要包含把夹在中间的 HTML 当作注释跳过的机理。下面分 3 个阶段来确认。

按投稿、HTML、JavaScript 的顺序看

攻击者把脚本拆成单次投稿容得下的长度,投稿了 15 次。关键在于怎样处理必然夹在投稿与投稿之间的 HTML(</div><div class="..."> 等)。它们作为 JavaScript 会造成语法错误。

于是被用上的是 JavaScript 的块注释。下面是为说明机理由本公司撰写的简化版(不是原样引用试题册的图)。

1. 投稿的字符串

假设分 3 次,在评论标题栏里这样投稿。

第 1 条: 很棒<script>a=1;/*
第 2 条: */b=2;/*
第 3 条: */c=3;</script>

2. 服务器拼装出的 HTML

服务器把它们分别作为各条评论的标题,像下面这样输出到 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>

3. 浏览器执行的 JavaScript

在浏览器看来,从最初的 <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 条长度都在限制之内的投稿,每条的开头和末尾都放着注释符号。这不是“去掉”限制的痕迹,而是“绕过”限制的痕迹。应当读作:考查的是逐一确认攻击者留下的东西的态度。

这种读取能力的重要性,评分讲评中也有指出。4

输入值该校验到什么程度、怎样校验,这种设计上的思路在不要直接使用 QR 码的读取值中也作为“不信任从外部进来的数据”的设计做过梳理。

6. 脚本做了什么——小题 3(1)

N 提取出的脚本有 20 行左右。小题 3(1) 要求用 60 字以内回答其中第 6 行到第 20 行(第一次通信成功之后执行的部分)的处理内容。

参考答案:连同从 XHR 的响应中取得的令牌一起,把会话 ID 作为头像图片上传”。3

阅读的顺序是取得令牌 → 把 cookie 的字符串做成文件 → 附上令牌上传。把“把什么”“当作什么”“送到哪里”分开,答案的要点就看得见了。

下面是为说明这一动作由本公司撰写的等价代码(不是原样引用试题册的图)。小题里的“第 6 到 20 行”是试题册的行号,不是下面示例的行号。示例中,注释 ① 是准备,② 到 ④ 对应小题所问的处理。

// ① 先取得个人资料设置页面
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 没有检查上传的图片文件的格式

会话 ID 能被读到的原因,以及 HttpOnly 守护的范围

document.cookie 之所以读得到,是因为 cookie 上没有加 HttpOnly 属性。RFC 6265 对 HttpOnly 属性规定:“把 cookie 的作用域限定为 HTTP 请求。特别是,当通过会把 cookie 暴露给脚本的网页浏览器 API 这类‘非 HTTP’的 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 的字符串”。3

回收的依据在“上传之后显示在哪里”

请回想一下,题干的规格里是这样写的。

上传的头像图片会显示在会员个人资料设置页面和评论页面上。

也就是说,写着受害者会话 ID 的那张“头像图片”,被放到了网站上可以浏览的地方。攻击者不需要做什么特别的事,只要打开显示着受害者头像图片的页面,取得那张图片的 URL 就行。内容是文本,打开就能读到会话 ID。

补充一点,这种回收之所以能顺理成章地做到,前提是受害者的头像图片出现在攻击者看得到的页面上(比如自己投稿过评论的那个商品的评论页面)。头像的 URL 是按会员固定的,所以只要在哪里看到过一次,此后就能直接去取那个 URL。无论如何,攻击者那侧的操作只是作为正规浏览者 GET 页面和图片,从网站看来不构成异常通信。

把从投稿到回收连成一条流程来确认

上传的是受害者的浏览器,回收的是攻击者。请把这两方的动作分开来追这张图。

受害者的浏览器Web 应用 Q(example.jp)攻击者受害者的浏览器Web 应用 Q(example.jp)攻击者准备阶段原样保存投稿的字符串(保存这样做是对的)受害者打开页面 V★漏洞在这里★把已保存的标题不转义就嵌入 HTML作为 script 元素被执行读取 document.cookie作为 a.png 的内容不确认格式就保存作为头像图片公开回收分 15 次投稿评论(用注释符号跳过 HTML)1GET 页面 V2含有攻击脚本的 HTML3GET /user/profile4含有令牌的 HTML5POST /user/upload(uploadfile=a.png, token=…)6GET 受害者的头像图片7写着会话 ID 的文件8

出口对策拦不住

这条路径的麻烦之处,画成图就一目了然:受害者的浏览器从头到尾只与正规网站(example.jp)通信

下面的表比较了盯住外部可疑通信目的地的对策阻止脚本执行的对策。在这里列出的配置中,起作用的只有最后一项。CSP 不是题干的条件,请作为面向实务的补充来读。

对策 对这次攻击 理由
代理服务器的 URL 过滤 不起作用 访问目的地只有业务上正当的电商网站
防火墙的出站通信监控 不起作用 不会产生发往陌生域的通信
Content Security Policy 的 connect-src 限制('self' 等允许同源的配置) 不起作用 两次通信都发往同源,落在策略的允许范围内
Content Security Policy 的 script-src 限制(不允许 'unsafe-inline' 的配置) 起作用 嵌入的内联脚本从一开始就不会执行

说到 XSS 造成的信息带出,人们容易想象成“cookie 被发送到攻击者的服务器”,但这道题清楚地表明,带出的目的地可以是受害者网站自身。只要站内存在既可写入又可读出的地方,那里就会成为交接点。文件上传功能、公开的个人资料栏、公开的备注栏都是典型。

CSP 要把“通信目的地”和“执行的许可”分开

关于 CSP 补充一点:实务中常用的配置里,能确实拦住这次攻击的不是 connect-src(通信目的地的限制),而是 script-src。只要指定 script-src 'self' 且不允许 'unsafe-inline',嵌在评论里的内联 <script> 从一开始就不会执行。如果只把 CSP 理解成“收紧对外通信的东西”,就会看错这个差别。8

connect-src 那边在原理上也不是无能为力。像 connect-src 'none' 这样连同源通信都禁止的配置,或者把通信目的地收窄到特定端点的许可列表,同样能拦住这两次 XHR。只是能收窄到那个程度的网站有限,所以作为现实的准备,script-src 才是主力。准确的理解不是“同源所以不在 CSP 的管辖内”,而是“在允许同源的一般配置下它会通过”。

小题 3(3):会话 ID 能干什么

小题 3(3) 用 40 字以内问用取得的信息能做什么。

参考答案:冒充访问过页面 V 的会员,使用 Web 应用 Q 的功能”。3

据评分讲评,这道小题的正答率很高。讲评里写道,考生们很好地理解了电商网站上 cookie 被攻击者取得所带来的影响。4

再回头读题干的规格会发现,会员个人资料功能中有登记信用卡信息的页面,并且明确写着只有已登录会员才能使用。冒充之后能走到哪一步,题干从一开始就准备好了。

8. 为什么在攻击者的网站上不成立——小题 4

小题 4 是这道大题中最本质的问题。

假设攻击者在自己准备的域的网站上备好含有与图 4 相同脚本的 HTML,即使 Web 应用 Q 的已登录会员访问了该网站,由于网页浏览器的机制,攻击也不会成功。请用 40 字以内回答这个机制。

参考答案:不会从脚本向其他域的 URL 发送 cookie 的机制”。3

本章先确认照题干的脚本原样会发生什么,之后再梳理 CORS 和 SameSite 的条件。重要的是不要把题干里没有的配置当作答案的依据来断言。

假设把同样的脚本放到 evil.example 上,让 Q 公司的会员踩上去。会发生什么?

首先,第一次通信会落空。evil.example 上的脚本向 https://example.jp/user/profile 发 XMLHttpRequest,这是跨源请求。

决定性的是不带 cookie。图 4 的脚本没有指定 withCredentials,所以跨源请求上不会附带 cookie。在 Q 公司的服务器看来这是未登录的访问,含有令牌的个人资料设置页面不会返回来。脚本取不到令牌,到这里就停住了。9

有条件的补充:CORS 决定的是能不能读响应

还有一堵墙是读不到响应。如果 Q 公司没有用 Access-Control-Allow-Origin 允许 evil.example,浏览器就会在 CORS 检查上失败并把这次通信当作网络错误处理,onload 不会触发,也就到不了 xhr.response。普通的网站不会给陌生的源返回这个响应头,所以实际上在这里也会停住。

不过 Q 公司的 CORS 是怎么配置的,题干里没有写。就算允许了来自 evil.example 的访问,能读到的也是未登录状态的响应,里面没有令牌,攻击照样不成立。仅凭不带 cookie 这一点,攻击就已经停住了。

不带 cookie 是从这段脚本本身就能得出的理由。而 CORS 的限制起不起作用,取决于 Q 公司的配置。不要把这两点都当作已确认的事实来处理。

“读”cookie 和把它“附加”到请求上是两回事

此外,cookie 本身也读不到。 document.cookie 返回的是这段脚本所运行的源(evil.example)的 cookie。对 example.jp 签发的会话 ID 不在里面。

IPA 的参考答案取的是前者,即“不会向其他域的 URL 发送 cookie”。

这里请不要把这两件事当作同一个机制记住。它们是不同的机制。

  • document.cookie 返回不了 example.jp 的 cookie,是因为 cookie 是按域分离管理的。像 evil.exampleexample.jp 这样属于不同网站(不同可注册域)的情况,这种分离无法通过配置放宽。不过同一个域下的子域之间是另一回事,用 cookie 的 Domain 属性可以在 sub.example.jpexample.jp 之间共享7
  • 跨源的 XHR 上不带 cookie,是因为 withCredentials 默认为 false。这一点在条件凑齐时会改变。只要脚本指定 withCredentials = true,并且 cookie 的 SameSite 属性允许跨站发送,并且服务器返回了写明来源源的 Access-Control-Allow-OriginAccess-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 有可能起作用

归纳起来,要从其他域读到已认证的响应,需要凑齐 3 点: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 公司的对策对照这次攻击来比较

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 应用程序,都可以直接拿来用。

确认的分工是:第 1 到 3 条是输入和输出,第 4 条是 cookie,第 5 到 6 条是上传,第 7 到 8 条是 CSP 和令牌。前半查的是消除根本原因的实现,后半查的是辅助性对策的生效范围。

  1. 输出时的转义是否在所有输出位置都生效。 把显式关掉模板引擎自动转义的地方(raw| safedangerouslySetInnerHTML 等)全部排查出来,逐个确认能否说明关掉它的理由
  2. 有没有把输入侧的限制算作 XSS 对策。 字数限制和可输入字符种类的限制是规格上的要求,不是 XSS 的根本性解决方案
  3. 有没有在输入时转义。 输入的职责是拦下规格之外的值的校验,而不是转义。输入时输出目的地(HTML、CSV、JSON、邮件、日志)尚未确定,所以输入时的转义会招致重复转义和数据破坏。SQL 语句的拼装也一样,根本性解决方案是占位符
  4. cookie 上是否加了 HttpOnlySecureSameSite 会话 ID 的 cookie 要特别确认
  5. 上传的文件是否按内容而不是扩展名或 Content-Type 来确认格式。 发送方申报的信息不能作为校验的材料。不过只靠格式检查,把数据藏进正当图片元数据区域的手法还是会通过
  6. 上传的图片是否重新编码后再分发。 有没有把原始的字节序列原样返回。不过重新编码也留得下作为像素编码的信息,所以要理解“既能写入又能读出的地方”无法彻底封死。另外,从别的域分发是针对上传文件在自家网站的源上被执行这一威胁的对策,不是针对这条带出路径的对策
  7. CSP 的配置是否能阻止内联脚本。 判断标准不是“有没有写 'unsafe-inline'”,而是“内联脚本实际上是否被允许”。script-src 中如果有 nonce 或 hash 的指定,CSP Level 2 之后的浏览器会忽略 'unsafe-inline',因此在同时写上 'unsafe-inline' 和 nonce 的向后兼容策略下,没有 nonce 的注入脚本会被阻断8
  8. 能否说明令牌在守护“什么”。 CSRF 对策的令牌防不住 XSS。两者各需要各自的对策

尤其第 1 条,是实际调查中会碰到的典型模式。本以为框架会自动转义所以很安全,却为了“想原样放入 HTML”这样的个别需求,只在一处关掉了自动转义——这种情形并不少见。

结语——这道题考的真正能力

到这里为止梳理的对策一览,已经汇总在开头的“先说结论”里。最后,就这道题本身再写一点。

它作为考题做得好的地方在于,考的不是“知不知道 XSS”,而是“在手头的这些对策中,能不能分辨出哪一个真正起了作用”。

Q 公司并不是什么都没做。它用 HTTPS 通信,对输入设了字数限制,上传要求令牌,功能限定给已登录会员。摆出来看,像是相当有对策的构造。即便如此还是被全部穿透了。反过来,欠缺的那 3 项都很不起眼,不是那种会写进发布说明的功能。

安全讨论之所以难,是因为对策的数量与防御的强度并不成正比。不是“做了什么”,而是能不能逐条说出“这项对策拦的是哪种攻击的哪个阶段”。这不只是应付资格考试的能力,对于听取安全对策说明的一方,以及负责实现的一方,都是最需要的判断力。

相关咨询领域

小村软件有限公司承接拥有会员功能和投稿功能的网站制作,以及排查现有 Web 应用程序是否开着同样口子的设计评审。

参考链接

  1. IPA 独立行政法人信息处理推进机构, 试题册、配分比例、参考答案、评分讲评(2023 年度、令和 5 年度) 所收“2023 年度 秋季 信息处理安全保障支援士考试 午后 试题”。关于 Q 公司 Web 应用 Q 的功能(会员注册、登录与用 cookie 签发会话 ID、商品评论功能中评论标题 50 字与评论详情 300 字的输入字数限制、会员个人资料功能的头像图片上传与信用卡信息登记、头像图片上传以图片文件和令牌为参数、上传的头像图片显示在会员个人资料设置页面和评论页面上)、页面 V 上 16 条评论只显示 2 条的现象、页面 V 的 HTML 中含有会员 A 的 15 条投稿和一段长脚本、被提取出的脚本的内容,以及 Web 应用 Q 存在会员输入的脚本被执行的漏洞、cookie 上没有加 HttpOnly 属性、没有检查上传的图片文件格式。小题 1 到小题 4 的题文也出自该册。  2

  2. IPA 独立行政法人信息处理推进机构, 关于考试的常见问题. 关于使用本机构公开的历年试题,除法令另有规定外无须取得许可或支付使用费,但并未放弃著作权,需要以“年度、期、考试类别、时间段、题号等”的形式标明出处(举例给出了“出处:平成 31 年度 春季 基本信息技术者考试 上午 问 1”),以及若对试题做了部分改写也需要一并注明。 

  3. IPA 独立行政法人信息处理推进机构, 2023 年度 秋季 信息处理安全保障支援士考试 参考答案. 关于问 1 的出题意图(以 Web 应用程序的漏洞被利用所引发的事件响应为题材,考查从 HTML 和 ECMAScript 中读解被利用的漏洞与问题点、并制定对策的能力),以及各小题的参考答案(小题 1(1) 是“第 2 项 存储型 XSS”,小题 1(2) 是“在输出评论标题之前施加转义处理。”,小题 2 是“分多次进行了能让 HTML 被注释掉并连成一段脚本的投稿。”,小题 3(1) 是“连同从 XHR 的响应中取得的令牌一起,把会话 ID 作为头像图片上传。”,小题 3(2) 是“下载会员的头像图片,从中取出会话 ID 的字符串。”,小题 3(3) 是“冒充访问过页面 V 的会员,使用 Web 应用 Q 的功能。”,小题 4 是“不会从脚本向其他域的 URL 发送 cookie 的机制”)。  2 3 4 5 6 7 8 9

  4. IPA 独立行政法人信息处理推进机构, 2023 年度 秋季 信息处理安全保障支援士考试 评分讲评. 关于问 1 整体的正答率处于平均水平,小题 1(1) 中“可能是因为脚本中使用了 DOM,误答为 “DOM Based XSS” 的考生时有所见”,希望连同特征和对策方法在内准确理解漏洞的提醒,小题 2 中“也有一部分答案像‘用开发者工具删除输入限制后投稿’这样,看来确认得不够充分”,希望培养仔细确认攻击者留下的痕迹、准确把握攻击方法的能力这一提醒,以及小题 3(3) 正答率很高、电商网站上 cookie 被攻击者取得的影响得到了很好理解。  2 3 4 5

  5. IPA 独立行政法人信息处理推进机构, 安全网站构建指南. 关于把网站的 11 种漏洞的威胁与对策分为“根本性解决”(消除漏洞成因本身的实现)和“辅助性对策”(漏洞残留时降低攻击成功率与损失的对策)来呈现,关于跨站脚本攻击的根本性解决方案中列举了对输出到网页的所有要素施加转义处理,以及附带的安全实现检查清单和别册《安全的 SQL 调用方法》《网站健康诊断规范》。  2 3

  6. IPA 独立行政法人信息处理推进机构, 安全网站构建指南 - 1.1 SQL 注入. 关于 SQL 注入的根本性解决方案中列举了“SQL 语句的拼装全部用占位符实现”,并指出静态占位符(预处理语句)在原理上更不易产生漏洞;关于用字符串拼接构建 SQL 语句时的实现写的是“用字符串拼接来拼装 SQL 语句时,要使用进行转义处理等操作的数据库引擎 API,正确构成 SQL 语句的字面量”,即转义是针对构成 SQL 语句的字面量的生成进行的(而不是在接收输入的时候);以及同时列为根本性解决方案的“不在传给 Web 应用程序的参数中直接指定 SQL 语句”,列为辅助性对策的“不把错误消息原样显示到浏览器上”“给数据库账户赋予适当的权限”。 

  7. IETF, RFC 6265: HTTP State Management Mechanism, Section 4.1.2.6 “The HttpOnly Attribute”. 关于 HttpOnly 属性把 cookie 的作用域限定为 HTTP 请求,特别是当通过会把 cookie 暴露给脚本的网页浏览器 API 这类“非 HTTP”的 API 提供对 cookie 的访问时,指示用户代理省略该 cookie。以及关于 Secure 属性(Section 4.1.2.5)限制只在安全通道上发送 cookie。  2

  8. W3C, Content Security Policy Level 3. 关于 script-src 对内联脚本执行的控制、connect-src 对通信目的地的控制,以及在含 nonce 或 hash 的源列表中 unsafe-inline 的处理方式。这是第 7 章和第 10 章实务补充的依据,不是表明 Q 公司配置的资料。  2

  9. WHATWG, XMLHttpRequest Standard — The withCredentials getter and setter. 关于它控制跨源请求中是否包含凭据,以及初始值为 false。第 8 章把它作为依据,从题干脚本没有指定 withCredentials 读出 cookie 不会被附带。 

共享相同标签的最新文章。可以围绕相近的主题进一步加深理解。

与本文相近的主题页面。以本文为起点,可以进一步了解相关服务和其他文章。

本文与以下服务页面相关联,欢迎从最接近的入口查看。

网站制作

因为在拥有会员功能和投稿功能的网站上,输出时的转义、cookie 的属性配置这些本文的论点,会直接决定制作质量。

常见问题

汇总了咨询这一主题时常见的问题。

这道题里的 XSS 为什么是“存储型”?脚本明明在操作 DOM,为什么不是 DOM Based XSS?
漏洞的种类取决于攻击者的字符串“在哪里”变成可执行的代码,也就是脆弱的输出位置(Sink)是在服务器端还是在浏览器端。这道题中,服务器把攻击者投稿的评论标题原样嵌入 HTML 返回。Sink 在服务器的输出处理上,而且那个字符串被保存下来,此后分发给每一个打开该页面的人,所以是存储型 XSS。DOM Based XSS 指的是浏览器上的 JavaScript 通过向 innerHTML 赋值等方式让某个值变得可执行的类型。嵌入的脚本用不用 XMLHttpRequest、getElementById 这类 DOM 的 API,与漏洞的种类无关。另外,即使服务器把已保存的值作为无害的文本或 JSON 返回,只要浏览器端的 JavaScript 把它交给 innerHTML,就是 DOM Based 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,攻击者的代码运行在与受害者相同的源上,所以正规用户能做的事情,它全都能做。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 公司,只要实施了其中任何一项,攻击链就会在某处断掉。不过辅助性对策在攻击者改变手法后有可能被绕过,所以代替不了作为根本性解决方案的转义。

作者简介

本文作者的个人简介页面。

Go Komura

小村软件有限公司 代表

以 Windows 软件开发、技术咨询与故障排查为中心,擅长难以复现的故障调查,以及既有资产仍在运行的项目。

返回博客列表