0-introduction
- 课程介绍
- 讲师: Steve Kinney, Temporal 公司的前端工程负责人。
- 主题: Web 安全,它不仅包含常规计算机安全的所有原则,还因 Web 本身的特性而具有其独特性。
- Web 的独特性与复杂性
- 起源: Web 从一个可以点击链接跳转到其他页面的文本文档查看器,发展成为全球最大的分布式应用平台。
- 双刃剑: Web 的优点(如将所有代码带到浏览器中运行)也带来了安全风险(在用户机器上运行应用代码)。这与下载已编译的应用程序不同。
- 历史包袱: Web 是几十年来一系列决策的产物,其中一些决策经过深思熟虑,一些则是临时性的。这使得理解 Web 安全的心智模型变得复杂,需要一定的历史背景知识。
- 安全的重要性与现实的冲突
- 普遍共识: 几乎所有人都认同 Web 安全的重要性,因为我们都见过严重漏洞带来的灾难性后果。
- 现实困境: 在日常开发中,项目截止日期和功能实现等紧迫任务,往往使得对潜在安全威胁的关注退居次要位置。
- 超越清单式安全思维
- 整体性思考: 课程的目标是建立一种整体性思维,去思考应用程序可能被攻击的各种方式,而不仅仅是遵循一个清单(如“净化你的输入”)。
- 理解原理: 关键在于理解安全机制如何工作,而不是盲目依赖框架。框架无法保护你免受业务逻辑中引入的漏洞。
- 安全中的权衡(Trade-offs)
- 安全 vs. 用户体验: 极高的安全性可能会损害用户体验(例如,最强的加密算法可能导致登录时间过长)。
- 安全 vs. 基础设施复杂性: 实现某些高级安全措施可能会极大地增加基础设施的复杂性,对于新项目(greenfield)和遗留系统(legacy)的挑战不同。
- 现实制约: 很多时候,无法实施理想的安全方案是因为受到公司过去技术决策和现有系统架构的限制。
- 建立心智模型: 理解这些权衡有助于在代码审查和开发决策中做出明智的选择,避免因缺乏理解而批准有风险的代码。
- 分层防御(Defense in Depth)
- 多层保护: 安全并非单一措施就能实现,而是通过多层保护策略的结合。例如,内容安全策略(CSP)、输入净化和正确的 Cookie 属性设置可以协同工作。
- 增强鲁棒性: 这种分层方法提供了更强大的覆盖范围,即使某一层保护措施因故未能完美实施,其他层仍然可以提供防护。
- 前端是第一道防线
- 网关角色: 前端是用户与系统交互的直接界面,可能成为攻击者进入后端系统的门户。
- 身份验证的核心: 在互联网上,系统通过认证令牌(token)或 Cookie 来确认用户身份。一旦攻击者窃取了这些凭证,他们就能冒充该用户,如果该用户是管理员,后果不堪设想。
- 课程目标与方法
- 培养“第六感”: 训练开发者对潜在安全问题产生直觉。
- 从攻击者视角学习: 通过实际操作“攻破”一些简单的示例应用来学习,而不是仅仅阅读一份安全检查清单。
- 课程内容:
- 常见漏洞: 重点介绍普遍适用于各种 Web 应用的漏洞(使用 Node 和 Express 作为示例,但原理通用)。
- 动手实践: 示例应用中故意留有多种漏洞,因为真实世界的攻击往往是多种漏洞的组合利用(例如,利用 CSRF 发起 XSS 攻击,再窃取会话)。
- 深入理解: 目标是获得对常见攻击向量和相应权衡的深入、实用的理解,从而在日常工作中做出更好的决策。
1-course-repo-setup
- 课程资源
- 一个网站,包含今天将看到的所有幻灯片(以及一些不会展示的额外内容)。
- 一个包含所有示例代码的仓库。
- 仓库设置
- 克隆仓库: 使用你喜欢的方式克隆,例如 GitHub CLI, GitHub Desktop, HTTPS, 或 SSH。
- 安装依赖: 克隆后,运行
npm install。
- 运行示例
- 启动脚本: 运行
npm start,这将显示一个包含所有练习的列表,你可以选择想要运行的示例。 - 启动流程:
- 选择一个练习(例如,我们将从
cookie jar开始)。 - 脚本会自动为该练习填充(seed)数据库。
- 脚本会启动一个本地服务器,并提供一个
localhost端口地址,你可以点击或复制到浏览器中打开。
- 选择一个练习(例如,我们将从
- 启动脚本: 运行
- 可用命令与选项
npm start: 启动并选择要运行的练习。npm start -- --sql: 在运行时显示所有执行的 SQL 查询,这在学习 SQL 注入时特别有用。npm start -- --port <端口号>: 在指定的端口上运行服务器。npm run reset-db: 清除仓库中所有的 SQLite 数据库。这在你把数据库搞乱(例如,通过 XSS 注入了恶意数据)后想恢复到初始状态时很有用。这些数据库文件本身在.gitignore中,不会被提交。
2-cookies-overview
- Cookie:Web 身份识别的核心
- 用途: Cookie 是 Web 上处理身份状态最常见的方式。
- 背景: HTTP 协议本身是无状态的(stateless)。Cookie 是一种机制,它允许服务器和浏览器之间建立一个“约定”,从而创造出一种有状态的会话(session)。
- 工作流程:
- 用户登录后,服务器向浏览器发送一个包含身份信息的标识。
- 浏览器将这个标识(Cookie)保存下来。
- 在后续的每一次请求中,浏览器都会自动带上这个 Cookie,以此“提醒”服务器当前用户的身份是谁。
- 应用场景:
- 认证用户: 允许用户发布内容、访问受保护页面。
- 购物车: 即使用户未登录,也能保存购物车中的商品。
- 广告追踪: 广告商通过 Cookie 追踪用户的浏览历史,以展示相关广告。
- Cookie 的发展历史
- 起源: 1994 年由 Netscape 创造,最初的实现非常草率,没有任何正式规范。这在 Web 早期很常见(例如
<img>标签)。 - 标准化尝试: 曾有几次失败或不兼容的标准化尝试(如 1997 年的规范和 Cookie2)。许多现存的 Web 安全问题都源于这些早期“蛮荒时代”的设计决策。
- 正式规范: 直到 2011 年,Cookie 才有了一份正式的、被广泛接受的规范(RFC)。
- 起源: 1994 年由 Netscape 创造,最初的实现非常草率,没有任何正式规范。这在 Web 早期很常见(例如
- Cookie 的技术原理
- 设置 Cookie: 服务器在 HTTP 响应头(Response Header)中通过
Set-Cookie字段来设置 Cookie。 - 发送 Cookie: 浏览器在后续向该服务器发起的 HTTP 请求头(Request Header)中通过
Cookie字段将之前保存的 Cookie 发送回去。 - 安全风险: 如果攻击者能够窃取到用户的 Cookie,他们就可以冒充该用户。
- 设置 Cookie: 服务器在 HTTP 响应头(Response Header)中通过
- 在 JavaScript 中操作 Cookie
- 访问: 可以通过
document.cookie在 JavaScript 中访问和设置 Cookie。 - 怪异行为: 对
document.cookie进行赋值操作,并不会替换整个 Cookie 字符串,而是会追加一个新的键值对。这种不直观的行为是潜在的攻击点和开发者的知识盲区。 - 风险: 这种可访问性是跨站脚本(XSS)攻击窃取用户身份的关键。
- 访问: 可以通过
- 在开发者工具中查看 Cookie
- 位置: 在 Chrome 开发者工具中,进入 "Application"(应用)标签页。
- 功能:
- 在左侧 "Storage"(存储)下的 "Cookies" 菜单中,可以看到当前网站设置的所有 Cookie。
- 浏览器会将底层的 Cookie 字符串解析成一个易于阅读的表格。
- 这是调试 Cookie 行为、确认安全属性是否正确设置的重要工具。
- 你也可以在这里手动删除 Cookie,以清除登录状态或重置测试环境。
3-cookie-attributes
- Cookie 的构成:键值对与属性
- Cookie 不仅仅是一个简单的键值对,它还包含一系列特殊的属性(attributes)。
- 这些属性是浏览器和服务器之间的“约定”,用于控制 Cookie 的行为,大部分已在 2011 年的规范中标准化。
- 生命周期属性
Expires: 设置一个具体的过期日期和时间。Max-Age: 设置 Cookie 从被创建开始可以存活的秒数(例如,24 小时、30 天)。- 会话 Cookie (Session Cookie): 如果不设置
Expires或Max-Age,这个 Cookie 就是一个会话 Cookie。它会在浏览器会话结束时(通常是关闭浏览器时)被删除。 - 一个有趣的例子: Google 曾将一个 Cookie 的过期时间设置为 2038 年,这恰好是 32 位 Unix 时间戳的上限,此举让一些人感到不安。
- 如何删除 Cookie
- 没有直接的“删除”命令。
- 要删除一个 Cookie,你需要重新设置它,并将其
Expires属性设为一个过去的时间点。浏览器收到后会将其视为过期并删除。
- 作用范围属性
Path(路径)- 作用: 限制 Cookie 只在服务器的特定路径下发送(例如
/admin)。 - 警告: 这个属性在现代 Web 应用中作用有限,并且可能因为
iframe等技术被绕过,带来意想不到的副作用。除非有非常明确的理由,否则不建议轻易使用。
- 作用: 限制 Cookie 只在服务器的特定路径下发送(例如
Domain(域名)- 作用: 控制 Cookie 可以被发送到哪些域名。
- 默认: 只发送给设置该 Cookie 的那个域名。
- 包含子域名: 在域名前加上一个点(例如
.frontendmasters.com),可以让这个 Cookie 同时发送给主域名及其所有子域名。 - 重大安全风险:
- 在公共托管平台(如
github.io,vercel.app)上使用此特性是极其危险的。如果你将 Cookie 的 Domain 设置为.github.io,那么所有托管在github.io上的其他用户的网站都能接收到你的 Cookie。 - 这个属性必须在你拥有顶级域名所有权的情况下才能安全使用,否则会造成严重的安全漏洞。
- 在公共托管平台(如
4-plain-text-passwords
- 示例应用:
cookie jar- 技术栈: 一个基于 Node.js 和 Express 的简单 Web 应用,使用 SQLite 作为数据库。
- 初始状态: 应用默认没有处理 Cookie 的功能,需要手动添加,以便逐步演示安全措施。
- 添加 Cookie 解析功能
- 库: 使用
cookie-parser这个 Express 中间件。 - 功能: 它的作用是将浏览器请求头中原始的、以分号分隔的
Cookie字符串解析成一个方便在代码中使用的对象(req.cookies)。 - 核心安全原则: 不要自己造轮子。尤其在涉及安全(如解析、加密)的领域,应使用经过社区审查和广泛使用的库。但同时要做足调研,避免使用已知有漏洞的库(例如,旧的 Express CSRF 模块
csurf已因安全问题被弃用)。
- 库: 使用
- 初期的不安全实现
- 登录逻辑: 用户在登录页面输入用户名(如
bobbytables)和密码。 - 设置 Cookie: 登录成功后,服务器直接设置一个名为
username的 Cookie,其值就是用户的用户名,并且是明文存储的。
- 登录逻辑: 用户在登录页面输入用户名(如
- 漏洞:客户端篡改
- 问题所在: 由于用户名以明文形式存储在 Cookie 中,任何人都可以轻易地篡改它。
- 攻击步骤:
- 打开浏览器的开发者工具。
- 进入 “Application” (应用) 标签页,找到 Cookies。
- 直接编辑
username这个 Cookie 的值,将其从bobbytables修改为admin或任何其他已知的用户名。 - 刷新页面或发起新的请求。
- 后果: 服务器接收到请求时,会无条件信任这个被篡改过的 Cookie,将当前用户识别为
admin,从而导致了权限提升。
- 关键教训
- 永远不要在 Cookie 中以明文形式存储任何敏感的、用于身份识别的信息。
- 这是一个基础但至关重要的安全漏洞。它揭示了保护 Cookie 完整性的必要性。即使后续对数据进行加密或混淆,如果攻击者能控制会话标识符,结果同样是身份冒充。
5-sessions-httponly
- 引入会话(Session)
- 目的: 在用户身份和 Cookie 存储的值之间增加一个抽象层,提升安全性。
- 工作方式:
- 不再将用户名直接存入 Cookie。
- 当用户登录时,服务器为其创建一个唯一的、随机的会话 ID (Session ID)。
- 服务器将这个会话 ID 与用户身份关联起来(例如,存储在数据库中)。
- 只将这个会话 ID 存入用户的 Cookie 中。
- 优点:
- 会话吊销: 你可以单独撤销某个会话(例如,用户在公共电脑上忘记登出),而无需强制用户修改密码。Gmail 等服务的“登出所有其他设备”功能就是基于此原理。
- 安全事件响应: 如果某个会话 ID 泄露,只需将其作废即可,影响范围可控。
- 问题:JavaScript 脚本窃取 Cookie(XSS 风险)
- 风险描述: 即便使用了会话 ID,如果网站存在跨站脚本(XSS)漏洞,攻击者注入的恶意 JavaScript 仍然可以通过
document.cookie读取到这个会话 ID。 - 后果: 攻击者可以将窃取到的会话 ID 发送到自己的服务器,然后用它来冒充用户,劫持整个会话。
localStorage的风险: 将认证令牌(token)存储在localStorage中也面临同样的风险,因为localStorage完全可以被 JavaScript 访问。
- 风险描述: 即便使用了会话 ID,如果网站存在跨站脚本(XSS)漏洞,攻击者注入的恶意 JavaScript 仍然可以通过
- 解决方案一:
HttpOnly属性- 作用: 这是一个可以设置在 Cookie 上的标志,它告诉浏览器:此 Cookie 只能通过 HTTP 请求传输,禁止客户端脚本(JavaScript)访问。
- 效果: 设置了
HttpOnly的 Cookie 后,document.cookie将无法获取到它的值。 - 重要性: 这是防御通过 XSS 攻击劫持会话的关键防线。
- 解决方案二:
Secure属性- 作用: 这个标志告诉浏览器,此 Cookie 只能通过加密的 HTTPS 连接发送。
- 目的: 防止在不安全的网络(如公共 Wi-Fi)中发生“中间人攻击”,即攻击者通过嗅探网络流量来窃取未加密的 Cookie。
- 开发实践: 通常只在生产环境中启用
Secure标志,因为本地开发环境(localhost)一般不使用 HTTPS。
- 在开发者工具中验证属性
- 通过浏览器的开发者工具(Application -> Cookies),可以检查
HttpOnly和Secure等属性是否已经正确地打上了勾,以确认安全设置已生效。
- 通过浏览器的开发者工具(Application -> Cookies),可以检查
- Cookie 的更新机制
- 一旦 Cookie 被设置到客户端,你无法中途修改它的属性。
- 要为一个已存在的 Cookie 添加
HttpOnly等属性,你必须在服务器端重新设置一个同名的新 Cookie,并带上新的属性。浏览器会用新的 Cookie 覆盖旧的。这通常在用户下次登录时完成。
6-signing-cookies-creating-sessions
- 问题升级:如何防止 Cookie 被篡改?
- 虽然
HttpOnly防止了脚本读取 Cookie,但 Cookie 的值本身如果未受保护,在某些场景下仍可能被篡改。
- 虽然
- 解决方案一:签名 Cookie (Signing Cookies)
- 概念: 签名是一种确保数据完整性(Integrity)的轻量级加密方法。它能证明 Cookie 是由你的服务器设置的,并且在传输过程中没有被修改过。
- 工作原理:
- 服务器端: 持有一个只有自己知道的密钥 (Secret)。
- 生成签名: 将 Cookie 的值和这个密钥通过一个哈希算法(如 HMAC)混合,生成一个独一无二的“签名”。
- 发送: 将原始的 Cookie 值和生成的签名一起发送给客户端。
- 验证: 当浏览器再次发来请求时,服务器收到 Cookie 值和签名。服务器使用收到的值和自己的密钥重新计算一次签名。如果计算结果与收到的签名一致,说明数据可信;如果不一致,说明数据已被篡改。
- 在
cookie-parser中实现:- 初始化
cookie-parser中间件时,传入一个cookieSecret。 - 重要安全警告: 绝对不要将密钥硬编码在代码中。应使用环境变量来管理,否则任何能访问代码仓库的人都能得到你的密钥。
- 设置 Cookie 时,添加
signed: true选项。 - 读取已签名的 Cookie 时,应从
req.signedCookies对象中获取,而不是req.cookies。如果签名验证失败,该 Cookie 不会出现在req.signedCookies中。
- 初始化
- 解决方案二:创建完整的会话机制
- 概念: 将用户身份与 Cookie 内容完全解耦,是更彻底的安全方案。
- 工作流程:
- 生成会话 ID: 用户登录时,生成一个唯一的、无法预测的随机字符串作为会话 ID(Session ID)。Node.js 的
crypto模块是生成这种随机字符串的好工具。 - 服务器端存储: 在服务器的数据库(或 Redis、内存中)创建一个记录,将这个会话 ID 与对应的用户身份关联起来。
- 设置 Cookie: 在客户端 Cookie 中只存储这个会话 ID。
- 验证请求: 当收到后续请求时,从 Cookie 中读取会话 ID,然后在服务器的会话存储中查询,以确定当前是哪个用户。
- 生成会话 ID: 用户登录时,生成一个唯一的、无法预测的随机字符串作为会话 ID(Session ID)。Node.js 的
- 优点:
- 信息隐藏: Cookie 本身不包含任何关于用户的敏感信息。
- 远程登出/会话吊销: 只需在服务器端删除对应的会话记录,即可让该会话失效。
- 防猜测攻击: 由于会话 ID 是长而随机的,攻击者几乎不可能猜中一个有效的会话 ID。
- 会话存储方案:
- 内存对象: 最简单,但服务重启后数据丢失,且无法在多服务器环境下扩展。
- 数据库 (SQL/Redis): 持久化且可扩展。Redis 特别适合,因为可以为键值设置过期时间,自动清理旧会话。
- 组合使用:分层防御
- 最佳实践是结合所有这些技术,构建多层防御:
- 使用会话 ID(而非直接的用户数据)。
- 对存储会话 ID 的 Cookie 进行签名,防止篡改。
- 为 Cookie 设置
HttpOnly标志,防止 XSS 攻击窃取。 - 在生产环境中为 Cookie 设置
Secure标志,强制使用 HTTPS 传输,防止网络嗅探。
- 最佳实践是结合所有这些技术,构建多层防御:
7-same-origin-policy-cookie-vulnerabilities
- 同源策略 (Same-Origin Policy, SOP):Web 的基石
- 定义: SOP 是浏览器的一项核心安全机制,它限制了从一个“源”(Origin)加载的文档或脚本如何能与另一个“源”的资源进行交互。
- 目的: 防止恶意网站读取或操作其他网站的敏感数据。我们常见的 CORS 错误就是同源策略在起作用。
- “源”的定义: 一个源由以下三个部分组合而成:
- 协议 (Protocol): 如
http,https - 主机 (Host): 如
www.frontendmasters.com - 端口 (Port): 如
80,443
- 协议 (Protocol): 如
- 只要这三者中有任何一个不同,就属于不同的源。
- “同源 (Same-Origin)” vs. “同站 (Same-Site)”
- 这两个术语看似相近,但含义不同,这个细微差别在后续课程中非常重要。
- (简而言之,
Site通常指可注册的域名,如frontendmasters.com;而Origin更为具体,包含了子域名、协议和端口。)
- 绕过 SOP 的历史方法
- JSONP (JSON with Padding): 一个古老的“黑科技”,利用了
<script>标签不受同源策略限制的特点。现在已不推荐使用。 document.domain: 一个已被废弃的机制。过去,不同子域(如a.example.com和b.example.com)的页面可以通过将document.domain都设置为example.com来实现跨域通信。- 漏洞所在: 这是一个“选择性加入”(opt-in)的系统,但它存在巨大漏洞。如果
frontendmasters.com为了与某个合作伙伴网站通信而设置了document.domain,那么互联网上任何其他网站也可以将自己的document.domain设置成一样的值,从而获得访问权限。
- 漏洞所在: 这是一个“选择性加入”(opt-in)的系统,但它存在巨大漏洞。如果
- CORS (Cross-Origin Resource Sharing): 现代 Web 中,官方推荐的、安全的跨域资源共享解决方案。应将 CORS 视为一项安全功能,而不是一个麻烦。
- JSONP (JSON with Padding): 一个古老的“黑科技”,利用了
- Cookie 与同源策略
- 相比于
fetch等 API 请求,Cookie 与同源策略的关系更为宽松和微妙,这些细微差别正是漏洞可能产生的地方。
- 相比于
- Cookie 保护措施的重要性
- 丢失 Cookie 是一起严重的安全事故。
- 我们今天拥有的多层保护措施(
HttpOnly,Secure, 签名等)是 Web 在发展过程中为了修补其最初不安全的设计而逐步添加的。 - 主流浏览器(Chrome, Firefox, Safari)有时会为了提升整体安全性,而主动“破坏” Web 的向后兼容性,例如修改 Cookie 的默认行为。
- 正因为 Cookie 至关重要,它既是攻击者的主要目标,也拥有了当今 Web 上最完善的一套保护机制。
8-privilege-escalation
- 超越 Cookie 本身的防护
- 即使我们已经将 Cookie 保护得很好(使用会话 ID、签名、
HttpOnly、Secure),应用程序也并非高枕无忧。 - 下一个攻击向量不再是直接窃取 Cookie,而是欺骗服务器,让服务器主动授予你一个更高权限的会话,或者替你执行你本无权执行的操作。
- 即使我们已经将 Cookie 保护得很好(使用会话 ID、签名、
- 会话劫持 (Session Hijacking)与权限提升 (Privilege Escalation)
- 会话劫持: 指接管其他用户的有效会话。之前我们手动将 Cookie 值修改为
admin就是最简单的一种会话劫持。 - 权限提升: 这是核心目标,指攻击者获得了其正常账户不应拥有的权限或访问能力。
- 这不仅仅意味着成为管理员。
- 切换到任何其他用户的账户,也同样属于权限提升。
- 会话劫持: 指接管其他用户的有效会话。之前我们手动将 Cookie 值修改为
- 权限提升攻击的常见模式
- 获得初始访问权限: 攻击者通常以一个普通用户的合法身份进入系统。
- 侦察与探测: 他们会寻找系统中的配置错误或逻辑漏洞。这不是随机猜测,而是一个系统性的调查过程。
- 很多“白帽黑客”(漏洞赏金猎人)会花大量时间研究 API 的响应,寻找漏洞的蛛丝马迹。
- 甚至存在一些高级的探测技术,例如通过注入一段 CSS,利用
background-image属性去请求一个攻击者控制的 URL。如果某个选择器(如#admin-panel)存在,这个请求就会发出,从而泄露了页面结构的信息。
- 发现并利用漏洞: 找到一个可以利用的弱点。
- 达成目标: 最终目标是获得一个高权限的会话,或者欺骗服务器执行一个高权限操作。
- 中间人攻击 (Man-in-the-Middle, MitM)
- 概念: 攻击者将自己置于用户浏览器和服务器之间,拦截并可能修改两者之间的通信。
- 现代环境下的难度: 随着 HTTPS 的普及,这种攻击变得非常困难。因为通信内容是加密的,攻击者即使截获了数据包,也无法解密内容(前提是服务器使用了强加密算法)。
- 课程重点
- 本课程将主要关注通过利用应用层漏洞实现的会话劫持和权限提升技术,因为在当前普遍使用 HTTPS 的环境下,这类攻击比中间人攻击更为常见和现实。
9-sql-injection
- 示例应用:
quaint little store- 这是一个模拟的在线商店,有登录、商品展示、个人资料等功能。
- 在启动时使用了
-sql标志,以便在终端中看到所有执行的 SQL 查询语句。
- SQL 注入攻击演示
- 场景: 在登录页面,攻击者知道管理员的邮箱是
[email protected],但不知道密码。 - 攻击载荷 (Payload): 在密码字段输入
' OR 1=1--。 - 攻击原理:
- 原始的、不安全的 SQL 查询语句是类似这样的字符串拼接:
SELECT * FROM users WHERE email = '...' AND password = '...'。 - 攻击者输入的载荷会改变这个查询语句,使其变为:
SELECT * FROM users WHERE email = '[email protected]' AND password = '' OR 1=1--'。 ': 第一个单引号闭合了password = ''的部分。OR 1=1: 增加了一个永远为真的条件。整个WHERE子句因为这个OR条件而总是成立。-: 这是 SQL 的注释符,它会忽略掉查询语句后面所有内容(包括原始的、用于闭合字符串的最后一个单引号)。
- 原始的、不安全的 SQL 查询语句是类似这样的字符串拼接:
- 结果: 数据库查询成功返回了管理员用户的信息,攻击者成功绕过密码验证,实现了登录。
- 场景: 在登录页面,攻击者知道管理员的邮箱是
- 根本原因与扩展
- 原因: 服务器端代码将用户输入直接通过字符串拼接的方式嵌入到 SQL 查询中,而没有进行适当的转义 (Escaping) 或参数化处理。
- 适用范围: 这种注入攻击不仅限于 SQL (SQL Injection),也可能发生在其他地方,如命令行 (Command Injection) 或其他数据库查询语言中。
- 信息泄露:攻击者的帮凶
- 泄露技术栈: 如果应用在出错时直接向用户显示详细的堆栈跟踪 (Stack Trace),会暴露所使用的技术(如 SQLite, Node.js 等),这为攻击者选择特定攻击手段提供了线索。
- 泄露服务器信息: 许多 Web 框架(如 Express)默认会在 HTTP 响应头中添加
X-Powered-By: Express这样的信息。- 风险: 这使得攻击者可以轻易识别出你使用的框架和版本,并利用该版本已知的零日漏洞 (Zero-day vulnerabilities)进行攻击。
- 对策: 务必在你的框架配置中关闭这类信息头。一个有趣但可能引火烧身的想法是,可以伪造这个头信息,谎称自己是另一个有漏洞的系统。
- 修复 SQL 注入
- 核心方法: 使用参数化查询 (Parameterized Queries) 或预备语句 (Prepared Statements)。
- 实现: 不要再用字符串拼接。将查询语句中的变量替换为占位符(如
?),然后将用户的输入作为单独的参数传递给数据库驱动。 - 优点: 数据库驱动库会负责安全地处理这些输入,进行必要的转义,从而防止注入攻击。
- 注入攻击的进阶:数据窃取
- 场景: 应用提供了一个
/api/products接口,支持search和limit等查询参数,但这些参数同样没有被正确处理。 - 高级载荷: 攻击者可以在
search参数中构造一个恶意的 SQL 片段,使用UNION SELECT来合并其他表的查询结果。 - 结果: 攻击者可以利用这个接口,不仅能查询商品,还能窃取到
sessions表中的会话 ID,甚至是users表中的密码哈希。
- 场景: 应用提供了一个
- 被低估的防御手段:日志与警报 (Logging and Alerting)
- 重要性:
- 侦测攻击企图: 即使攻击未成功,日志也能记录下有人正在尝试注入攻击,让你提前发现潜在威胁。
- 事后分析: 如果攻击成功,日志是了解发生了什么、损失了多少数据的唯一途径。
- 实践: 应尽早地在项目中加入日志和警报系统,而不是把它当作“以后再做”的功能。对于可疑的输入(如包含
credit card,password等关键词的查询),应触发警报。
- 重要性:
10-stored-queries
- 代码架构即安全
- 问题: 如果 SQL 语句散落在代码库的各个角落,一旦发现漏洞,修复工作将变得非常困难和容易遗漏。
- 解决方案:抽象化:
- 将数据库查询操作封装在专门的函数中(例如
getUserFromSessionId,getProducts)。 - 好处: 即使内部实现存在漏洞(如忘记转义输入),也只需要修改这一个函数就能修复整个应用中所有调用点的问题。这种抽象本身就是一种安全特性。
- 推荐: 尽可能使用 ORM (Object-Relational Mapper),它能帮助你远离手写 SQL,从而减少犯错的机会。
- 将数据库查询操作封装在专门的函数中(例如
- 注入攻击的破坏性:不仅仅是数据泄露
- “小鲍勃表” (Bobby Tables) 漫画: 引用了经典的 XKCD 漫画,说明了 SQL 注入的终极破坏力。
- 笑话解释: 一个母亲给儿子取名为
Robert'); DROP TABLE Students; --。当学校的系统将这个名字不加处理地插入数据库时,执行的 SQL 语句会删除整个学生表。 - 教训: SQL 注入不仅能读取数据,还能修改、甚至删除数据。
- 笑话解释: 一个母亲给儿子取名为
- “小鲍勃表” (Bobby Tables) 漫画: 引用了经典的 XKCD 漫画,说明了 SQL 注入的终极破坏力。
- 库的内置保护
- 讲师提到,他使用的 SQLite 库中有一些内置保护措施(比如区分
get、all、run等操作),阻止了他执行DROP TABLE这样的破坏性命令。 - 这说明选择一个好的、经过安全考量的库非常重要,但不能完全依赖它。
- 讲师提到,他使用的 SQLite 库中有一些内置保护措施(比如区分
- 另一种修复方法:预备语句 (Prepared Statements)
- 概念: 这是一种将 SQL 查询的“结构”和“数据”分离的方法。
- 工作流程:
- 使用
db.prepare()创建一个包含占位符的预备语句。这个语句的结构被发送到数据库进行预编译。 - 然后,将用户的输入作为参数绑定到这个已准备好的语句上执行。
- 使用
- 安全性: 因为查询结构已经固定,用户输入的数据无论是什么,都只会被当作数据处理,永远无法改变查询的逻辑,从而杜绝了注入攻击。
- 表面积: 这种方法限制了攻击的表面积,将安全性委托给经过实战检验的数据库驱动。
- 依赖与信任
- 底层风险: 即使使用了最好的工具和实践,我们仍然依赖于底层系统(如 Intel 处理器、操作系统)的安全性。这些底层系统也可能存在漏洞。
- 保持更新: 这就是为什么保持项目依赖(dependencies)的更新至关重要。
- 权衡: 大多数情况下,信任经过社区广泛审查的工具比自己从头实现要安全得多。
- 高级攻击向量:时序攻击 (Timing Attack)
- 概念: 一种通过测量服务器响应时间来推断敏感信息的侧信道攻击。
- 原理: 不同的操作(如密码哈希计算)会消耗不同的时间。
- 示例:
- 当用户尝试用一个不存在的用户名登录时,服务器可以立刻返回“用户不存在”,响应非常快。
- 当用户使用一个存在的用户名和错误的密码登录时,服务器需要先从数据库找到该用户,然后对其存储的密码哈希进行计算和比较,这个过程会消耗额外几十毫秒的时间。
- 信息泄露: 攻击者通过精确测量这两个请求的响应时间差异,就能在不知道密码的情况下,判断出哪些用户名是真实存在的。
11-parameter-injection
- 另一种权限提升方式:参数注入
- 背景: 这种攻击常见于使用 NoSQL 数据库(如 MongoDB)的应用,因为它们的数据模型通常是 JSON 对象,与 JavaScript 对象非常相似。
- 漏洞根源: 当服务器端代码盲目地接受并使用客户端发送过来的所有数据来更新数据库记录时,就会产生此漏洞。
- 常见错误代码模式: 使用对象展开语法(
...)将请求体中的所有属性合并到一个新对象中,例如const updatedUser = {...currentUser, ...req.body}。
- 参数注入攻击演示
- 场景: 在用户个人资料页面,用户可以修改自己的姓名、邮箱等信息。服务器端的 PATCH 请求处理逻辑存在漏洞。
- 正常操作: 用户提交表单,服务器接收
name和email字段并更新数据库。 - 攻击步骤:
- 攻击者使用浏览器的“检查元素”工具。
- 在页面表单中手动添加一个隐藏的 input 字段:
<input type="hidden" name="admin" value="true">。 - 提交表单。
- 后果:
- 服务器端的代码不加分辨地接收了包括
admin: true在内的所有请求体数据。 - 它将
admin字段也更新到了数据库中。 - 攻击者刷新页面后,就成功将自己提升为了管理员权限。
- 服务器端的代码不加分辨地接收了包括
- 核心解决方案:白名单(Allow-listing)
- 理念: 永远不要信任客户端的输入。只接受你明确期望接收的字段。
- 错误做法:黑名单(Deny-listing): 尝试过滤掉已知的危险字段(如
admin)。这种方法很脆弱,因为攻击者可能会猜到其他有权限的字段名(如role)。 - 正确做法:白名单(Allow-listing):
- 在代码中明确指定允许更新的字段列表。
- 例如:
const { name, email } = req.body; - 然后,只使用
name和email这两个变量去更新数据库,忽略请求中任何其他多余的字段。
- 这样,即使攻击者发送了
admin: true,这个字段也会被服务器代码安全地忽略掉。
- 关键教训
- 逻辑漏洞: 参数注入是一种业务逻辑层面的漏洞。
- 工具的局限性: 没有任何自动化代码扫描工具或库能够轻易地检测出这种漏洞,因为它取决于你的应用逻辑。
- 开发者的责任: 防御此类攻击需要开发者在编写代码时具备安全意识,主动思考数据流和权限问题。
- 攻击手段多样: 攻击者不一定需要修改 HTML 表单,他们可以使用
cURL或浏览器的网络工具直接构造并发送带有恶意参数的 HTTP 请求。
12-other-types-of-injection-attacks
- 中间人攻击 (Man-in-the-Middle, MitM)
- 概念: 攻击者在用户和服务器之间拦截网络流量。
- 现状: 随着 HTTPS 的普及,这种攻击变得非常困难,因为流量是加密的。
- 自查工具: 可以使用像 Charles Proxy 这样的工具对自己进行“中间人攻击”,以调试和检查移动应用的 API 请求,这也说明了原生应用的 API 并非不可见。
- Cookie 属性回顾与深化
HttpOnly: 除非有极好的理由(如 SPA 需要在 JS 中访问令牌),否则应始终使用。Secure: 必须使用,强制 Cookie 只能通过 HTTPS 传输。SameSite:None: 过去是默认值,现在需要显式设置,并必须与Secure配合使用。主要用于跨站追踪,如广告。Lax: 现在是浏览器的默认值。允许在顶级导航(如点击链接跳转)时发送 Cookie,但在跨站的子请求(如<img>,iframe, AJAX)中不发送。在安全和用户体验之间取得了很好的平衡。Strict: 最安全。只在完全相同的站点内发送 Cookie。即使从其他网站点击链接过来,用户也需要重新登录,用户体验较差,适用于银行等高安全性网站。
- “站点 (Site)” vs. “源 (Origin)” 的区别
- 源 (Origin): 由 协议、主机、端口 三部分组成的元组。
- 站点 (Site): 范围更广,通常指顶级域名(TLD, 如
.com)加上它前面的一级(如frontendmasters)。子域名(如a.frontendmasters.com和b.frontendmasters.com)属于同一个站点,但属于不同的源。 - 公共后缀列表 (Public Suffix List): 这是一个例外列表,用于处理像
github.io这样的公共托管平台。浏览器会将github.io视为顶级域名,因此user1.github.io和user2.github.io会被视为不同的站点,防止 Cookie 泄露。
- 其他类型的注入攻击
- 命令注入 (Command Injection):
- 场景: 当服务器端代码调用系统命令行工具(如在 Node.js 中使用
exec('ls ' + userInput))时发生。 - 攻击: 攻击者可以输入
; rm -rf /来结束当前命令并执行一个恶意命令。 - 防御: 尽量使用编程语言内置的函数(如 Node.js 的
fs模块)来替代调用 shell 命令。如果必须调用,使用安全的函数(如execFile)并严格净化输入。
- 场景: 当服务器端代码调用系统命令行工具(如在 Node.js 中使用
- 文件上传漏洞 (File Upload Vulnerabilities):
- 场景: 应用允许用户上传文件,例如上传头像并进行裁剪。
- 攻击: 攻击者上传的可能不是图片,而是一个可执行文件(
.exe)或一个针对处理库(如 FFmpeg)已知漏洞的特制文件。 - 防御: 严格验证上传文件的类型和内容,确保它就是你所期望的文件。
- 远程代码执行 (Remote Code Execution, RCE):
- 场景: 当应用使用
eval()或类似函数,将用户输入的字符串当作代码来执行时发生。 - 攻击: 这是最危险的注入类型之一,攻击者可以直接在你的服务器上运行任意代码。
- 防御: 绝对不要使用
eval()。使用安全的替代方案,如沙箱环境(Sandbox, 如 Docker 容器)或专门的净化库(如用于处理 HTML 的DOMPurify)。
- 场景: 当应用使用
- 命令注入 (Command Injection):
13-cross-site-request-forgery-case-studies
- 跨站请求伪造 (Cross-Site Request Forgery, CSRF) 简介
- 核心概念: CSRF 攻击的核心不是窃取用户的会话信息(如 Cookie),而是欺骗用户的浏览器,在用户不知情的情况下,以用户的名义向一个网站发送一个恶意的、经过认证的请求。
- 前提: 用户必须已经登录了目标网站,浏览器中存有有效的 Cookie。
- 真实世界案例研究
- Twitter 蠕虫 (2010):
- 漏洞: Twitter 提供了一个通过 GET 请求发推的 API(例如
twitter.com/share/update?message=...)。 - 攻击: 攻击者将这个链接嵌入到网页中,当用户(已登录 Twitter)将鼠标悬停在链接上时,浏览器就会自动发送 GET 请求,并带上用户的 Cookie,从而自动发布了这条恶意推文,实现了病毒式传播。
- 关键教训: GET 请求绝对不应该用来执行任何会改变状态的操作(如发帖、删除、修改)。
- 漏洞: Twitter 提供了一个通过 GET 请求发推的 API(例如
- Netflix (2008):
- 漏洞: 类似于 Twitter,Netflix 允许通过 GET 请求将 DVD 添加到用户的观看队列。
- 攻击: 攻击者将这个 GET 请求的 URL 放在一个
<img>标签的src属性里。当用户的浏览器尝试加载这个“图片”时,就会向 Netflix 发送请求,把恶意的 DVD 添加到用户的队列中。
- 纽约时报 (2008) - 数据泄露:
- 漏洞: 文章页面有一个“用邮件发送本文”的功能,它会发送一个 POST 请求。
- 攻击: 攻击者创建一个页面,诱导用户的浏览器向这个功能点发送 POST 请求,但接收邮件的地址是攻击者自己的。
- 后果: 这导致用户的电子邮件地址被泄露给攻击者,可用于垃圾邮件列表。这是一个创造性的利用方式,说明 CSRF 不仅可以执行操作,还可以用于信息窃取。
- 其他严重案例 (2008):
- 在同一份研究报告中,还发现了银行网站的漏洞,允许攻击者代表用户开立新账户或转移资金。
- YouTube 的漏洞允许攻击者执行几乎所有用户操作,如添加视频到播放列表。
- TikTok (2020) - 账户接管:
- 漏洞: 一个 CSRF 漏洞允许通过第三方应用触发目标用户的密码重置流程。
- 后果: 攻击者可以完全接管用户的 TikTok 账户,证明了 CSRF 至今仍然是一个严重且相关的威胁。
- Twitter 蠕虫 (2010):
14-elements-of-a-csrf-attack
- CSRF 攻击的实施方式
- GET 请求: 这是最简单的方式。攻击者只需将恶意 URL 放在
<img>标签、链接<a>或 CSSbackground属性中,就能在用户浏览器加载页面时自动触发请求。 - POST 请求:
- 这需要更多技巧,但同样可行。
- 攻击者可以在自己的网站上创建一个隐藏的表单 (form)。
- 这个表单的
action属性指向目标网站的某个操作 URL(例如,/transfer_money)。 - 表单中包含执行操作所需的参数(例如,转账金额、收款人账户)。
- 然后,使用 JavaScript 在页面加载后自动提交这个隐藏的表单。
- 关键点: 因为请求是从用户的浏览器发出的,浏览器会自动附上用户在目标网站的有效 Cookie,使得这个伪造的请求看起来完全合法。
- GET 请求: 这是最简单的方式。攻击者只需将恶意 URL 放在
- CSRF 攻击成立的三个必要条件
- 存在一个相关的操作 (Relevant Action): 目标网站上必须有一个攻击者感兴趣的、会改变状态的操作(如转账、修改密码、删除账户、发帖)。
- 基于 Cookie 的会话处理 (Cookie-based Session Handling): 用户的身份验证必须依赖于浏览器自动发送的 Cookie。CSRF 攻击通常不需要 JavaScript 来窃取任何东西,它依赖的就是浏览器的这个默认行为。
- 没有不可预测的请求参数 (No Unpredictable Parameters): 这是最关键的一点。如果执行一个操作所需的所有参数都是固定的或可预测的(例如,
delete_account.php?confirm=1),那么攻击者就可以轻易地构造出恶意请求。
- 防御思路
- 上述三个条件中:
- 第一个条件(相关操作)无法消除,因为它是网站的核心功能。
- 第二个条件(Cookie)虽然可以通过
SameSite属性进行限制,但仍有局限性。 - 因此,防御 CSRF 的核心在于打破第三个条件,即在请求中加入一个不可预测的参数。这就是 CSRF 令牌(Token)的用武之地。
- 上述三个条件中:
- CSRF 攻击流程总结
- 用户登录: 用户登录目标网站 A,浏览器保存了网站 A 的 Cookie。
- 访问恶意网站: 用户在没有登出网站 A 的情况下,访问了攻击者的恶意网站 B。
- 触发请求: 网站 B 中的代码(HTML 或 JS)向网站 A 发送了一个伪造的请求。
- 浏览器行为: 用户的浏览器收到这个请求后,自动将网站 A 的 Cookie 附加到请求头中。
- 服务器响应: 网站 A 的服务器收到了这个请求,验证了 Cookie 是合法的,于是执行了请求中的恶意操作,因为服务器无法分辨这个请求是用户自愿发起的还是被伪造的。
15-implementing-a-csrf-attack
- 示例应用:
sea-surf-bank- 一个模拟银行应用,用户可以登录并向其他用户转账。
- CSRF 攻击实战演练
- 步骤 1: 正常操作
- 用户
finnthehuman登录银行应用。 - 他成功地向
princessbubblegum转账了 10 美元。此时,账户余额减少,一切正常。
- 用户
- 步骤 2: 访问恶意网站
- 用户在保持登录状态的情况下,点击了一个链接,访问了一个名为
/evil的页面(这个页面模拟了攻击者的网站)。 - 这个页面看起来可能只是一个有趣的图片或文章。
- 用户在保持登录状态的情况下,点击了一个链接,访问了一个名为
- 步骤 3: 攻击发生
/evil页面的 HTML 源码中,包含一个隐藏的、自动提交的表单。- 这个表单的
action指向银行的转账接口。 - 表单的参数预设为将 250 美元转给一个攻击者控制的账户。
- 步骤 4: 后果
- 当用户访问
/evil页面时,JavaScript 自动提交了该表单。 - 用户的浏览器向银行服务器发送了一个 POST 请求,并自动附带了有效的登录 Cookie。
- 银行服务器验证 Cookie 通过,执行了转账操作。
- 用户返回银行页面后,发现自己的账户余额莫名其妙地减少了 250 美元。
- 当用户访问
- 步骤 1: 正常操作
- 第一道防线:
SameSiteCookie 属性SameSite=None: 这是旧的、不安全的默认行为。它允许在任何跨站请求中发送 Cookie,这正是 CSRF 攻击能够得逞的原因。现在浏览器要求设置None必须同时设置Secure(HTTPS)。SameSite=Lax: 这是现代浏览器的默认设置。- 它会在跨站的 POST 请求(如上例中的表单提交)、
iframe或 AJAX 请求中阻止发送 Cookie,从而有效防御了大部分 CSRF 攻击。 - 漏洞: 它仍然允许在“顶级导航”(用户点击链接跳转)且请求方法为“安全”(如 GET)的情况下发送 Cookie。因此,对于那些将状态变更操作放在 GET 请求中的应用(如之前的 Twitter 案例),
Lax无法提供保护。
- 它会在跨站的 POST 请求(如上例中的表单提交)、
SameSite=Strict: 最严格的模式。只有当请求完全源自同一站点时才会发送 Cookie。这提供了最强的 CSRF 保护,但代价是用户体验下降(例如,从邮件中点击网站链接后需要重新登录)。
- 结论与警示
- 虽然现代浏览器默认的
SameSite=Lax提供了显著的保护,但不应将其视为唯一的防御措施。 - 防御 CSRF 的正确思路是组合使用多种策略,形成纵深防御。仅依赖
SameSite是不够的,因为:- 它无法防御 GET 请求型的 CSRF。
- 用户的浏览器可能版本过旧,不支持或未默认启用此特性。
- 应用架构的复杂性可能迫使你无法使用严格的
SameSite策略。
- 因此,需要其他更主动的防御机制。
- 虽然现代浏览器默认的
16-lax-versus-strict
SameSite属性的权衡Lax: 在用户体验和安全性之间提供了最佳平衡,适用于大多数 Web 应用(如社交媒体、电商)。允许用户从外部链接无缝访问网站。Strict: 提供最高级别的安全性,但可能会因为频繁要求用户重新登录而损害用户体验。适用于对安全性要求极高的应用,如网上银行、公司内部管理后台。- 这是一个业务决策: 选择哪个值取决于你的应用场景和对安全与便利性的权衡。
SameSite=Lax的局限性- 浏览器兼容性:
SameSite=Lax成为默认值是近几年的事。仍有用户可能使用不支持此功能的旧版浏览器,这些用户仍然容易受到 CSRF 攻击。因此,不能完全依赖浏览器默认行为来保护所有用户。 - OAuth 的“两分钟规则”: 在像 OAuth 这样的跨站认证流程中,浏览器有一个短暂的(约两分钟)窗口期,会暂时放宽
Lax限制,允许以 POST 方式设置 Cookie。虽然这个窗口很小,但理论上仍存在被利用的可能。
- 浏览器兼容性:
- 核心防御机制:不可预测的 CSRF 令牌 (CSRF Token)
- 原理: 这是防御 CSRF 的黄金标准。它通过在请求中加入一个攻击者无法伪造的、随机且唯一的参数,来打破 CSRF 攻击的第三个必要条件。
- 工作流程:
- 生成令牌: 当用户访问一个包含表单的页面时,服务器为该用户的会话生成一个唯一的、随机的、保密的字符串,这就是 CSRF 令牌。
- 嵌入表单: 服务器将这个令牌作为一个隐藏字段(例如
<input type="hidden" name="_csrf" value="...">)嵌入到发送给客户端的 HTML 表单中。同时,服务器也会在自己的会话存储中保留一份这个令牌的副本。 - 提交与验证: 当用户提交表单时,这个隐藏的 CSRF 令牌会随其他表单数据一起被发送回服务器。
- 服务器在处理请求前,会比较收到的令牌与自己会话中存储的令牌是否一致。
- 为什么能防御攻击?:
- 攻击者的恶意网站(
evil.com)虽然可以伪造一个提交到银行网站的表单,但它无法得知当前用户会话的正确 CSRF 令牌是什么。 - 因为同源策略,攻击者的网站无法通过脚本读取银行网站页面的内容来窃取令牌。
- 因此,攻击者伪造的请求要么没有 CSRF 令牌,要么令牌是错误的,服务器在验证时会发现不匹配,从而拒绝该请求。
- 攻击者的恶意网站(
- 其他(不可靠的)防御方法
- 检查
Referer头:- 这个 HTTP 请求头会指示请求的来源页面。服务器可以检查
Referer是否来自自己的域名。 - 问题: 这个方法非常不可靠。
Referer头可以被客户端(或代理)轻易地伪造或移除,因此不能作为可靠的安全措施。
- 这个 HTTP 请求头会指示请求的来源页面。服务器可以检查
- 检查
17-using-csrf-tokens
- CSRF 令牌(Token)的核心思想
- 在表单中嵌入一个不可预测的值(unpredictable value)。
- 这个值由服务器生成,并且只有服务器和当前用户的会话知道。
- 当表单提交时,服务器会验证这个值,如果值不正确或不存在,请求将被拒绝。
- 实现 CSRF 令牌时的常见误区
- 将令牌放在 Cookie 中: 这是无效的,因为 Cookie 会被自动发送,攻击者伪造的请求同样会带上 Cookie,起不到验证作用。
- 使用固定的令牌: 如果所有用户的令牌都一样,攻击者只需获取一次,就可以将其硬编码到恶意表单中,攻击依然有效。
- 使用可预测的令牌池: 如果令牌是从一个小的、固定的池子中循环使用的,攻击者可以通过多次尝试来猜中一个有效的令牌。
- CSRF 防护库的注意事项
csurf库已弃用: 这是一个曾经在 Express 社区中广泛使用的库,但现在已被标记为不安全并已弃用。NPM 会警告你不要安装它。tiny-csrf: 这是一个轻量级的替代品,但其实现方式是将令牌存储在 Cookie 中,这可能不是最理想的方案。- 自己实现: 虽然可以自己实现一个简单的版本,但需要注意各种边缘情况。
- 动手实现一个 CSRF 令牌机制
- 步骤 1:生成令牌
- 时机: 在用户登录或创建新会话时生成令牌。
- 方法: 使用一个能够生成唯一、随机字符串的库。
crypto模块(Node.js 内置): 可以生成随机字节。uuid库:uuidv4()方法可以生成一个碰撞概率极低的唯一标识符。使用 v4(随机)而不是 v5(确定性)非常重要。
- 错误做法: 不要为每个请求都生成新令牌。这会导致用户体验问题(如点击后退按钮后表单失效、多标签页冲突)并增加数据库负载。
- 正确做法: 为每个会话 (session) 生成一个令牌,并在会话期间保持不变。
- 步骤 2:存储令牌
- 位置: 将生成的令牌与用户的会话信息一起存储在服务器端(如数据库的
sessions表中)。不要将其存储在 Cookie 中。 - 关键: 将会话 ID(存储在 Cookie 中)与 CSRF 令牌(仅在服务器和表单中存在)分离开。这就像核武器发射需要两把不同的钥匙一样,增加了安全性。
- 位置: 将生成的令牌与用户的会话信息一起存储在服务器端(如数据库的
- 步骤 3:将令牌传递给前端
- 当渲染包含表单的页面时,从数据库中读取当前会话的 CSRF 令牌。
- 将令牌作为数据传递给模板引擎。
- 在 HTML 表单中,将令牌渲染为一个隐藏的输入字段:
<input type="hidden" name="_csrf" value="<%= token %>" /> - 这样,当用户提交表单时,令牌就会包含在请求体中。
- 步骤 4:验证令牌
- 在处理表单提交的路由中 (例如
POST /transfer): - 从请求体中获取提交上来的 CSRF 令牌 (
req.body._csrf)。 - 从当前用户的会话中获取服务器端存储的正确令牌。
- 比较两者是否相等。
- 如果不相等: 立即返回一个错误(如 401 Unauthorized 或 403 Forbidden),并终止请求处理。不要向攻击者透露过多信息(例如“无效的 CSRF 令牌”)。
- 如果相等: 说明请求是合法的,继续处理请求。
- 在处理表单提交的路由中 (例如
- 步骤 1:生成令牌
- 效果
- 实施该机制后,之前可以成功的 CSRF 攻击现在会因为缺少或错误的 CSRF 令牌而被服务器拒绝,从而保护了用户。
- 即使 Cookie 的
SameSite策略因某种原因失效,这层基于令牌的保护依然有效,实现了纵深防御。
18-csrf-token-exercise
- 练习目标
- 在一个名为
Socialite的社交媒体模拟应用中,为发帖功能实现 CSRF 令牌保护。 - 这个应用允许用户注册、登录和发帖。
- 在一个名为
- 实现过程回顾与关键点
- 数据库设计失误与教训:
- 讲师在早期设计数据库时,将
sessions表的主键命名为sessionid而不是通用的id,这导致了后续代码中的一些混乱。 - 教训: 命名约定的一致性很重要。即使是小失误,也会在未来造成技术债。
- 讲师在早期设计数据库时,将
- 抽象化的好处:
- 将创建会话 (
createSession) 和获取会话 (getSession) 的逻辑封装在独立的辅助函数中。 - 优点: 这样无论是在登录还是注册流程中需要会话操作时,都可以复用同一段代码。如果需要修改会话逻辑,只需在一个地方进行,降低了出错风险。
- 将创建会话 (
- 会话创建逻辑 (
createSession):- 为每个新会话生成两个不同的唯一 ID:
sessionid: 用于存储在客户端的 Cookie 中,作为会话的标识符。token: 即 CSRF 令牌,用于隐藏在表单中。
- 关键安全原则: 区分这两个值至关重要。如果将会话标识符和 CSRF 令牌使用同一个值,安全性会大打折扣。
- 将
sessionid、userid和token一起存储在服务器端的sessions表中。
- 为每个新会话生成两个不同的唯一 ID:
- 中间件 (
currentUser):- 这是一个在每个请求到达时都会运行的中间件。
- 它从客户端 Cookie 中读取
sessionid。 - 使用
getSession函数根据sessionid从数据库中查找对应的会话信息(包括userid和token)。 - 如果找到会话,它会将用户信息和会话信息(特别是
token)附加到res.locals对象上。 - 目的: 使得后续的路由处理器和模板都能方便地访问到当前用户的信息和 CSRF 令牌。
- 表单渲染:
- 在渲染发帖表单的页面时,从
res.locals.session.token中获取 CSRF 令牌。 - 将其作为一个隐藏字段 (
<input type="hidden" name="_csrf" ...>) 嵌入到表单中。
- 在渲染发帖表单的页面时,从
- 提交验证:
- 在处理发帖的
POST /posts路由中: - 从请求体 (
req.body._csrf) 中获取用户提交的令牌。 - 从
res.locals.session.token中获取服务器为该会话存储的正确令牌。 - 比较两者是否一致。如果不一致,则拒绝请求。
- 在处理发帖的
- 数据库设计失误与教训:
- 总结
- 通过上述步骤,为
Socialite应用的发帖功能成功添加了 CSRF 令牌保护,修复了其安全漏洞。 - 这个练习完整地演示了从会话创建、令牌生成、前端嵌入到后端验证的整个 CSRF 防护流程。
- 通过上述步骤,为
19-finding-a-csrf-exercise
- 练习场景:一个看似无害的网站
- 目标应用:
Socialite社交媒体应用,用户已登录。 - 攻击网站: 一个名为
The Void的极简网站,页面上只有一个黑色的背景。它的服务器代码非常简单,只是一个静态文件服务器,没有任何后端逻辑。 - 现象: 用户在
Socialite上发了一条帖子后,去访问了The Void网站。回到Socialite后,发现自己的账户自动发布了一条自己并未输入的新帖子。
- 目标应用:
- 漏洞揭秘:隐藏在 CSS 中的 GET 请求
- 问题所在: 用户的浏览器是如何在访问
The Void时向Socialite发送发帖请求的? - 答案: 漏洞隐藏在
The Void网站的 HTML 文件中,具体是在一个<style>标签内的一段 CSS 代码里:body { background-color: black; background-image: url("<http://localhost:4008/posts/create?content=CSRF+via+background+image!">); } - 攻击原理:
- 浏览器在渲染页面时,会尝试加载 CSS 中指定的
background-image。 - 为了加载这个“图片”,浏览器会向指定的 URL 发送一个 GET 请求。
- 这个 URL 恰好是
Socialite应用的创建帖子接口,并且通过查询参数带上了帖子内容。 - 因为这个请求是从用户的浏览器发出的,所以它会自动带上
Socialite网站的登录 Cookie。 Socialite的后端代码存在一个致命缺陷:它允许通过 GET 请求来创建帖子,并且没有对 GET 请求进行 CSRF 令牌验证。- 服务器收到这个带有有效 Cookie 的 GET 请求后,就创建了一条新的帖子,攻击成功。
- 浏览器在渲染页面时,会尝试加载 CSS 中指定的
- 问题所在: 用户的浏览器是如何在访问
- 核心教训
- GET 请求的幂等性: 绝对不能使用 GET 请求来执行任何会改变数据状态的操作(如创建、修改、删除)。GET 请求应该只用于获取数据。
- CSRF 攻击的多样性: CSRF 攻击不一定需要 JavaScript 或表单。任何能让浏览器发送跨站请求的 HTML/CSS 特性(如
<img>,<a>,background-image等)都可能成为攻击向量。 - 查询参数 (Query Params) 的危险性:
- 信息泄露: GET 请求的所有参数都暴露在 URL 中,这意味着它们会被记录在浏览器历史、服务器日志、网络代理等各种地方。永远不要在查询参数中放置任何敏感信息,如 CSRF 令牌或个人身份信息 (PII)。
- PII 风险: 在 URL 中包含邮箱、电话号码等 PII 可能会违反数据保护法规(如 GDPR)。这些信息会以明文形式在互联网上传输,并可能被第三方追踪器记录。
- 其他防御 CSRF 的策略
- 检查
Referer头: 可以检查请求的来源,但此方法不可靠,因为Referer头可以被伪造或禁用。 - 双重提交 Cookie (Double Submit Cookies): 一种替代方案,将 CSRF 令牌既存储在 Cookie 中,也放在请求参数里,服务器比较两者是否一致。这是一种无状态的 CSRF 防护方式,但比基于会话的令牌要复杂且有其自身的风险。
- 增加用户交互: 对于非常危险的操作(如删除 GitHub 仓库),可以设计一个多步骤的确认流程。
- 例如,要求用户重新输入密码,或者输入仓库的名称来确认。
- 原理: CSRF 攻击是“一次性”的,攻击者可以伪造一个请求,但无法与用户进行后续的交互。通过增加交互步骤,可以有效地 thwart CSRF 攻击。
- 检查
20-cross-origin-resource-sharing
- CORS (跨源资源共享) 的真正目的
- 误解: 开发者常常认为 CORS 是一个麻烦,因为它经常导致 API 请求失败。
- 真相: CORS 是浏览器同源策略(Same-Origin Policy)的一个安全放行机制。它的目的是保护用户,而不是为难开发者。你讨厌的不是 CORS 本身,而是后端没有正确配置 CORS 响应头。
- CORS 与 CSRF 的关系
- 重要区别: CORS 策略只对由脚本(如 AJAX, Fetch API)发起的跨源请求有效。它不覆盖传统的 HTML 表单提交。
- 安全漏洞: 这意味着,即使你的 API 配置了严格的 CORS 策略来防止来自未知域的脚本访问,一个通过简单表单 POST 请求发起的 CSRF 攻击仍然可以成功。
- 结论: 仅依赖 CORS 无法完全防御 CSRF。你仍然需要 CSRF 令牌来保护表单提交。
- 简单请求 (Simple Requests) vs. 预检请求 (Preflighted Requests)
- 浏览器将跨源请求分为两类,以决定是否需要先发送一个“预检”请求。
- 简单请求: 满足以下所有条件的请求被视为“简单请求”,浏览器会直接发送,而不会触发 CORS 预检:
- 请求方法是
GET,POST, 或HEAD之一。 - 除了浏览器自动设置的头之外,手动设置的请求头仅限于
Accept,Accept-Language,Content-Language,Content-Type。 Content-Type的值仅限于application/x-www-form-urlencoded,multipart/form-data, 或text/plain。
- 请求方法是
- 关键点: 一个标准的 HTML 表单提交就是一个典型的简单请求,因此它不受 CORS 预检的保护。
- 预检请求 (Preflight Request): 任何不满足“简单请求”条件的请求(例如,方法是
PUT,DELETE;Content-Type是application/json;或包含自定义请求头),浏览器会先发送一个OPTIONS方法的预检请求到目标服务器,询问是否允许即将到来的实际请求。
- CORS 响应头详解
- 服务器通过在响应中包含特定的
Access-Control-*头来告诉浏览器它的 CORS 策略。 Access-Control-Allow-Origin: 指定允许访问的源。- 可以是 (通配符,表示允许任何源,但非常不安全)。
- 也可以是一个具体的源(如
https://friendlywebsite.com)。 - 注意: 不能是多个域名的列表。服务器需要根据请求的
Origin头动态地决定是返回该Origin还是不返回。
Access-Control-Allow-Methods: 指定允许的 HTTP 方法(如GET, POST, PUT)。Access-Control-Allow-Headers: 指定允许的自定义请求头。Access-Control-Allow-Credentials: 如果设置为true,则允许跨源请求携带 Cookie。- 重要: 如果
Access-Control-Allow-Origin设置为 ,浏览器会忽略这个头,绝不会发送 Cookie。
- 重要: 如果
- 服务器通过在响应中包含特定的
- 危险的实践:方法覆盖 (Method Override)
- 一些旧框架(如早期的 Rails)为了让只支持 GET/POST 的 HTML 表单也能发送 PUT/DELETE 请求,会使用一个隐藏字段
_method来“覆盖”请求方法。 - 安全风险: 这样做会将一个本应受 CORS 保护的预检请求(如
PUT)伪装成一个不受保护的简单请求(POST),从而可能绕过 CORS 策略,并使应用在未受 CSRF 令牌保护的情况下暴露于危险之中。
- 一些旧框架(如早期的 Rails)为了让只支持 GET/POST 的 HTML 表单也能发送 PUT/DELETE 请求,会使用一个隐藏字段
- 额外的请求安全头 (Fetch Metadata Request Headers)
- 现代浏览器在请求中会添加一些额外的
Sec-Fetch-*头,为服务器提供关于请求上下文的更多信息。 Sec-Fetch-Site: 表明请求是same-origin,same-site,cross-site还是none(用户直接发起的)。Sec-Fetch-Dest: 表明请求的目标是什么(如document,iframe,worker)。Sec-Fetch-User: 表明请求是否由用户操作触发。- 用途: 服务器可以利用这些头信息来实现更精细的访问控制逻辑,例如,只允许用户直接发起的请求执行敏感操作。
- 现代浏览器在请求中会添加一些额外的
21-cross-site-scripting
- 跨站脚本 (Cross-Site Scripting, XSS) vs. CSRF
- CSRF: 攻击来自外部,是伪造的请求。请求本身是合法的,但用户的意图是被伪造的。
- XSS: 攻击来自内部。攻击者成功地将恶意脚本注入到你的网站页面中。这些脚本在用户的浏览器中执行,此时它们与你的网站处于同源,拥有完全的权限。
- 严重性: XSS 比 CSRF 更危险。一旦成功,攻击者可以窃取 Cookie(即使有
HttpOnly标志,虽然不能直接读取,但可以利用浏览器发送的请求)、监听用户键盘输入、抓取页面上的敏感信息(如信用卡号)、修改页面内容等。
- XSS 的主要类型
- 存储型 XSS (Stored XSS):
- 原理: 恶意脚本被存储在目标服务器的数据库中(例如,在一条评论、一篇帖子或用户个人资料里)。
- 危害: 每当有用户访问包含这段恶意数据的页面时,脚本就会在他们的浏览器中执行。这是危害最广、最严重的一种 XSS。
- 反射型 XSS (Reflected XSS):
- 原理: 恶意脚本不是存储在数据库里,而是作为请求的一部分(通常是 URL 的查询参数)发送给服务器,服务器未经处理就将其直接反射回响应的 HTML 页面中。
- 攻击方式: 攻击者需要诱骗用户点击一个特制的、包含恶意脚本的链接。脚本只在点击该链接的用户浏览器中执行一次。
- DOM 型 XSS (DOM-based XSS):
- 原理: 注入的脚本完全在客户端执行,不涉及服务器。恶意数据被 JavaScript 读取并动态地写入到页面的 DOM 中,从而导致脚本执行。
- 这种类型更为微妙和复杂。
- 存储型 XSS (Stored XSS):
- 真实世界案例研究
- Samy 蠕虫 (MySpace, 2005):
- 类型: 存储型 XSS。
- 技术细节: Samy 发现了一个极其复杂的漏洞组合,绕过了 MySpace 的多层输入过滤。他利用了 CSS 的
background属性中一个旧浏览器支持的javascript:URL,并通过拼接字符串("java" + "script:")、换行符、String.fromCharCode()等多种技巧,最终在页面上注入了一段能够自我复制并强制给 Samy 发送好友请求的脚本。 - 影响: 在 20 小时内感染了超过一百万用户,是 XSS 历史上最著名的案例之一。
- Twitter 蠕虫 (2009):
- 类型: 存储型 XSS。
- 技术细节: 攻击者发现 Twitter 在处理包含
@符号的 URL 时,其 HTML 解析器存在缺陷。这使得他们可以逃逸出<a>标签的属性,并注入一个onmouseover事件处理器。 - 攻击方式: 当用户鼠标悬停在一个看似正常的链接上时,恶意的 jQuery 代码就会执行,自动发布一条新的恶意推文。
- 教训: 即便是最微小的解析器 bug 也可能导致严重的安全漏洞。
- Samy 蠕虫 (MySpace, 2005):
22-xss-in-the-real-world
- 现实世界中的 XSS 案例(续)
- TweetDeck (2014):
- 漏洞: 一个极其简单的存储型 XSS。攻击者发布了一条包含
<script>标签的推文。 - 影响: Twitter 的 Web 客户端正确地转义了该脚本,但其桌面应用 TweetDeck(一个 Electron 应用)没有。任何在 TweetDeck 中看到这条推文的用户都会被感染,并自动转发该推文。
- 教训: 同一个数据在不同的客户端(Web、桌面、移动)上可能受到不同程度的保护,安全策略必须覆盖所有端点。
- 漏洞: 一个极其简单的存储型 XSS。攻击者发布了一条包含
- eBay (2015-2016):
- 漏洞: 反射型 XSS。网站上有一个
redirect查询参数,但验证不严。攻击者可以构造一个看似合法的 eBay 链接,将用户重定向到 eBay 自己的一个安全性较差的子页面,并在该页面上注入脚本。 - 后果: 攻击者可以修改商品价格、窃取用户信息,造成了持续数月的安全问题。
- 漏洞: 反射型 XSS。网站上有一个
- 麦当劳 (2017):
- 漏洞: 密码以加密形式存储,但在客户端进行了解密。一个 Angular Sandbox 的漏洞允许 XSS 攻击者在客户端解密过程完成后,直接抓取到明文密码。
- 教训: 永远不要在客户端进行敏感数据的解密操作。
- 英国航空 (British Airways, 2018):
- 漏洞: 一个第三方 JavaScript 库(Feedify)被攻击者植入了恶意代码。
- 后果: 注入的脚本悄悄地收集了超过 25 万名用户的姓名、地址、信用卡号和 CVV,并发送到攻击者的服务器。
- 教训: 第三方依赖是重大的安全风险来源。必须对供应链进行安全审查。
- Fortnite (2019):
- 漏洞: 与 eBay 类似,通过一个不安全的子域(一个十五年前的《虚幻竞技场》排行榜页面)实现了 XSS。该子域与主站共享认证,但没有 CSRF 保护。
- 后果: 攻击者可以获取用户的几乎所有数据。
- 教训: 即使是看似被遗忘的旧资产,如果与主站共享认证域,也可能成为整个系统的安全短板。
- CIA (2020): 一位 Google 的安全研究员声称成功对中情局网站实施了 XSS 攻击。
- VS Code 插件 (2024): 一个 VS Code 插件的漏洞被用于通过 XSS 窃取用户的敏感信息,表明 XSS 不仅仅局限于浏览器环境。
- TweetDeck (2014):
- XSS 攻击的核心流程
- 注入恶意脚本: 攻击者通过某种方式(如评论、URL 参数)将恶意脚本代码插入到你的应用中。
- 脚本执行: 当受害者用户的浏览器加载并渲染包含该恶意脚本的页面时,脚本被执行。
- 获取权限: 此时,该脚本与你的网站处于同源,可以访问
document.cookie、localStorage,可以发起同源的 AJAX 请求,并可以操纵页面的 DOM。 - 恶意操作: 脚本执行攻击者的指令,如将窃取到的 Cookie 发送到攻击者的服务器。
- 关键点
- 尽管
HttpOnlyCookie 可以防止脚本通过document.cookie直接读取 Cookie,但它无法阻止脚本通过发起 Fetch 或 XMLHttpRequest 来利用这个 Cookie。因为浏览器在发送同源请求时会自动附带HttpOnlyCookie。 - 因此,XSS 依然是窃取用户会话的有效手段。
- 尽管
23-finding-xss-exploits
- XSS 漏洞的两种基本防御策略
- 净化输入 (Sanitization): 这是第一道也是最重要的一道防线。
- 内容安全策略 (Content Security Policy, CSP): 这是第二道防线,用于在净化失败时提供保护。
- 策略一:净化输入
- 核心思想: 在将用户提交的任何数据渲染到页面上之前,必须对其进行处理,移除或转义其中的潜在危险内容(如
<script>标签、事件处理器onclick等)。 - 现代框架的内置保护:
- 大多数现代前端框架(如 React, Vue, Svelte)默认都会对动态插入的内容进行自动净化。
- 在 React 中,你需要使用一个名字非常吓人的属性
dangerouslySetInnerHTML来绕过这个保护,这本身就是一个强烈的警告。
- 专用库:
DOMPurify是一个久经考验的、非常强大的 HTML 净化库。它可以在浏览器和 Node.js 环境中运行,是行业标准之一。
- 基本原则:
- 不要自己写净化逻辑。自己写的逻辑很容易被绕过。应当使用成熟、开源、经过社区审查的库。
- 最基本的净化操作是将
<,>等特殊字符替换为它们的 HTML 实体(如<,>)。
- 核心思想: 在将用户提交的任何数据渲染到页面上之前,必须对其进行处理,移除或转义其中的潜在危险内容(如
- XSS 攻击演示:未受保护的应用
- 存储型 XSS:
- 在评论框中输入
<script>alert('hacked')</script>并提交。 - 这个脚本被保存到数据库中。
- 现在,任何加载该页面的用户,其浏览器都会执行这个脚本,弹出一个警告框。
- 在评论框中输入
- 反射型 XSS:
- 应用的 URL 中有一个用于切换主题的查询参数,例如
?theme=light。 - 服务器会将
theme参数的值不加处理地渲染到页面上。 - 攻击者可以构造一个 URL:
?theme=<script>alert('hacked')</script>。 - 当用户点击这个链接时,恶意脚本就会在他们的浏览器中执行。
- 一旦用户离开这个 URL(例如,刷新页面且不带恶意参数),攻击效果就消失了。
- 应用的 URL 中有一个用于切换主题的查询参数,例如
- 存储型 XSS:
- 存储型 vs. 反射型 XSS 的比较
- 存储型: 危害更大,影响所有访问页面的用户。但一旦发现,可以通过清理数据库来修复。
- 反射型: 危害范围较小,只影响点击了特定链接的用户。但也更难被发现,因为它不留下痕迹在服务器上。
- 结论
- 净化输入是防御 XSS 的基石。
- 几乎所有现代 Web 开发工具都提供了强大的内置净化功能。开发者需要做的是不要主动去关闭或绕过这些安全特性。
- 然而,即便是最好的净化措施也可能有漏洞(如 MySpace 案例所示),因此需要第二层防御。
24-xss-best-practices
- XSS 防御最佳实践总结
- 输入验证与净化 (Input Validation & Sanitization):
- 双重净化: 在数据存入数据库时(on the way in)和从数据库取出渲染到页面时(on the way out)都进行净化,构建纵深防御。
- 使用安全的 DOM 操作方法:
- 安全 (Safe Sinks): 优先使用
.textContent来设置元素的文本内容。这个方法会自动将传入的字符串当作纯文本处理,不会解析其中的 HTML,因此是安全的。 - 危险 (Unsafe Sinks): 避免使用
.innerHTML和document.write()。这些方法会将字符串解析为 HTML,从而可能执行嵌入的脚本。
- 安全 (Safe Sinks): 优先使用
- 依赖框架和库: 充分利用你正在使用的框架(React, Vue 等)或模板引擎(Handlebars, EJS)的内置净化功能。
- 选择安全的工具:
- DOMPurify: 如果需要处理用户输入的富文本内容,使用
DOMPurify这样的专业库来净化 HTML。 - allow-list vs. deny-list:
DOMPurify内部使用“白名单”机制,只允许已知的安全标签和属性通过,这比尝试过滤掉所有已知危险内容的“黑名单”机制要安全得多。
- DOMPurify: 如果需要处理用户输入的富文本内容,使用
- 自动化测试:
- 利用已知攻击载荷: 互联网上有公开的 XSS 攻击字符串列表(XSS Payloads)。
- 建立测试套件: 可以创建一个自动化测试(例如使用 Playwright),将这些已知的攻击载荷输入到你的应用中。
- 验证: 测试的目标是确认这些输入没有触发预期的恶意行为(例如,可以通过监视
alert函数是否被调用来判断攻击是否成功)。 - 执行频率: 这些测试可以定期(如每周)运行,以持续验证应用的安全性。
- 输入验证与净化 (Input Validation & Sanitization):
- 关键心态
- 多层防御: 没有单一的银弹。安全来自于将多种策略(输入净化、安全的 API 使用、内容安全策略、自动化测试)结合起来。
- 信任但验证: 即使你使用了最好的框架和库,通过自动化测试来验证它们是否如预期般工作,也是一个好习惯。
- 警惕危险操作: 每当在代码中看到
innerHTML,dangerouslySetInnerHTML或类似的函数时,都应该触发警报,并进行严格的代码审查。
25-content-security-policy-overview
- CSP:作为第二道防线
- 第一道防线: 永远要净化你的输入。
- 第二道防线: 内容安全策略(Content Security Policy, CSP)是另一层保护。即使攻击者成功地绕过了净化机制,注入了恶意代码,CSP 仍然可以阻止该代码的执行。
- Web 的默认行为与 CSP 的作用
- 默认行为: Web 的设计是开放的,允许从任何地方加载资源(如 CDN 上的 jQuery、Google 字体、其他网站的图片)。这既是 Web 的优点,也是其安全风险所在。
- CSP 的作用: CSP 允许你收紧这个策略,创建一个“允许列表”。它告诉浏览器:“我只信任并允许从这些指定的、已知的域名加载资源(如脚本、样式、图片)”。这与 CORS 对 API 请求的作用类似。
- CSP 的核心功能
- 阻止内联脚本 (Inline Scripts):
- 仅仅是启用 CSP(即使策略很宽松),就会默认禁止所有内联脚本(例如
<script>alert(1)</script>)和内联事件处理器(onclick="...")的执行。 - 这是 CSP 最强大和最直接的安全效益之一。
- 仅仅是启用 CSP(即使策略很宽松),就会默认禁止所有内联脚本(例如
- 选择性放行:
- 如果你确实需要内联脚本,必须在 CSP 策略中明确添加
unsafe-inline指令。但这会大大削弱 CSP 的保护作用,应极力避免。 - 课程后续会介绍在不使用
unsafe-inline的情况下处理必要内联脚本的安全方法(如 Nonce 或 Hash)。
- 如果你确实需要内联脚本,必须在 CSP 策略中明确添加
- 阻止内联脚本 (Inline Scripts):
- 策略的粒度
- CSP 策略非常灵活,可以针对不同类型的资源进行精细化控制。
- 你可以为脚本(
script-src)、样式(style-src)、图片(img-src)、字体(font-src)、iframe(frame-src)等分别设置不同的允许来源。
- 如何部署 CSP
- HTTP 响应头: 这是最常用和推荐的方式。服务器在 HTTP 响应中加入一个
Content-Security-Policy头。 - HTML Meta 标签: 如果你无法控制服务器响应头,也可以在 HTML 的
<head>部分通过<meta>标签来设置 CSP。<meta http-equiv="Content-Security-Policy" content="default-src 'self';" /> - 浏览器会从上到下解析页面,所以应将这个
<meta>标签尽可能地放在<head>的最前面,以确保在加载任何潜在的恶意资源之前策略就已生效。
- HTTP 响应头: 这是最常用和推荐的方式。服务器在 HTTP 响应中加入一个
26-implementing-a-csp
- 示例应用:
csp-playground- 这是一个专门用来演示 CSP 效果的页面,故意加载了来自多个不同源的资源:
- Google 字体
- 来自 CDN 的 Tailwind CSS
- 来自
frontendmasters.com的图片 - 内联脚本和内联样式
- 这是一个专门用来演示 CSP 效果的页面,故意加载了来自多个不同源的资源:
- 使用
helmet库在 Express 中实现 CSPhelmet: 是一个流行的 Express 中间件,可以方便地设置各种与安全相关的 HTTP 响应头,包括 CSP。- 基本用法:
const helmet = require("helmet"); app.use( helmet.contentSecurityPolicy({ directives: { // 在这里定义你的 CSP 策略 }, }) );
- 逐步收紧和放宽 CSP 策略
- 默认策略: 即使只启用一个空的 CSP 策略,
helmet也会应用一套合理的默认值。这会立刻产生效果:- 内联脚本被阻止:页面上
<script>标签里的代码不再执行。 - 远程资源被阻止:来自
frontendmasters.com的图片、Google 字体等都无法加载。 - 浏览器的控制台会明确报告哪些资源因为违反了 CSP 而被阻止。
- 内联脚本被阻止:页面上
- 白名单原则 (Allow-listing):
- 防御的最佳实践不是试图阻止所有已知的坏东西(黑名单),而是只允许你明确知道是好的东西(白名单)。
- 你应该从一个非常严格的策略开始(例如
default-src 'self',只允许加载同源资源),然后根据需要,逐步为你信任的域名“开门”。
- 放宽策略示例:
- 允许图片:
img-src: ["'self'", "static.frontendmasters.com", "fav.farm"],允许加载同源、frontendmasters和fav.farm的图片。 - 允许样式:
style-src: ["'self'", "cdn.tailwindcss.com", "fonts.googleapis.com"],允许加载同源和来自两个 CDN 的 CSS 文件。 - 允许内联脚本(不推荐):
script-src: ["'self'", "'unsafe-inline'"]。明确地加入'unsafe-inline'会重新允许内联脚本执行,但这应该作为最后的手段。
- 允许图片:
- 默认策略: 即使只启用一个空的 CSP 策略,
report-only模式- 对于已有的大型复杂应用,直接应用一个严格的 CSP 可能会破坏现有功能。
- 可以使用
Content-Security-Policy-Report-Only这个头。 - 作用: 浏览器不会真正阻止任何资源,但它会像应用了策略一样,将所有本应被阻止的违规行为报告到一个你指定的 URL 或是在开发者控制台中。
- 用途: 这允许你在不影响线上用户的情况下,收集当前网站的所有资源加载情况,逐步构建出一个完整且安全的 CSP 策略,然后再正式部署。
- CSP 的关键价值
- 即使你的净化措施存在漏洞(例如,像 Twitter 蠕虫中利用
onmouseover的注入),CSP 也能提供保护,因为它会阻止内联事件处理器的执行。 - 它能防御各种巧妙的 XSS 攻击,例如通过一个损坏的
<img>标签和它的onerror属性来执行 JavaScript。CSP 会因为图片源不在白名单里而从一开始就阻止加载,onerror也就无从触发。 - 结合输入净化,CSP 提供了一个强大的纵深防御体系。
- 即使你的净化措施存在漏洞(例如,像 Twitter 蠕虫中利用
27-nonce
- 问题:如何在启用 CSP 的同时安全地使用内联脚本?
- 在某些情况下(例如,某些打包工具的运行方式,或需要动态注入配置),你可能必须使用内联脚本。直接使用
unsafe-inline会大大降低安全性。 - 有两种更安全的替代方案:nonce 和 hash。
- 在某些情况下(例如,某些打包工具的运行方式,或需要动态注入配置),你可能必须使用内联脚本。直接使用
- 方案一:Nonce (Number used once)
- 概念: Nonce 是一个为单次请求生成的、唯一的、随机的字符串。
- 工作流程:
- 服务器端: 对于每一个请求,服务器生成一个随机的 nonce 值(类似于 CSRF 令牌)。
- 响应: 服务器将这个 nonce 值同时放在两个地方:
- CSP 响应头的
script-src指令中:script-src 'nonce-RANDOM_VALUE'。 - HTML 页面中需要被允许的
<script>标签上:<script nonce="RANDOM_VALUE">...</script>。
- CSP 响应头的
- 浏览器端: 浏览器在执行脚本前会检查:该脚本标签是否带有
nonce属性,并且其值是否与 CSP 头中指定的 nonce 值匹配。只有匹配的内联脚本才会被执行。
- 安全性: 攻击者注入的脚本因为无法预知这个为单次请求生成的随机 nonce 值,所以无法将正确的 nonce 附加到他们的恶意脚本上,因此脚本会被 CSP 阻止。
- 缺点:与缓存的冲突: 因为 nonce 是为每个请求动态生成的,包含 nonce 的 HTML 页面是动态的,不能被静态缓存。这可能会对性能产生影响。
- 方案二:哈希 (Hash) / 子资源完整性 (Subresource Integrity, SRI)
- 概念: 为脚本内容本身生成一个加密哈希值(如 SHA-256)。
- 工作流程:
- 预计算: 你需要预先计算出你信任的脚本文件(无论是内联的还是外部的)内容的哈希值。
- 响应: 服务器将这个哈希值放在 CSP 响应头的
script-src指令中:script-src 'sha256-HASH_VALUE'。- 对于外部脚本,还需要在
<script>标签上添加integrity属性:<script src="..." integrity="sha256-HASH_VALUE">。
- 对于外部脚本,还需要在
- 浏览器端: 浏览器在执行脚本前会:
- 下载脚本内容。
- 独立计算该内容的哈希值。
- 比较自己计算的哈希值与 CSP 头或
integrity属性中提供的哈希值是否一致。只有一致,脚本才会被执行。
- 安全性:
- 防止篡改: 这种机制可以确保你加载的脚本(特别是来自 CDN 的第三方脚本)没有被篡改。如果 CDN 被黑,脚本内容被修改了哪怕一个字节,哈希值就会不匹配,浏览器将拒绝执行,从而保护你的用户。
- 缓存友好:因为哈希值是基于文件内容的,只要文件不变,哈希值就不变,因此页面可以被缓存。
- 缺点:
- 脆弱性: 如果脚本文件有任何合法的改动(例如,升级版本、修改压缩方式),你都必须重新计算并更新所有的哈希值,否则网站会立刻中断。
- 哈希字符串较长,会增加响应头的大小。
- 总结与建议
- 首选: 尽量避免使用内联脚本。将脚本放在独立的
.js文件中,并通过同源加载。 - 次选: 如果必须使用,Nonce 和 Hash 是比
unsafe-inline安全得多的选择。 - 权衡: Nonce 在管理上更简单,但牺牲了缓存;Hash 提供了更强的完整性保证且缓存友好,但维护起来更脆弱。根据你的具体需求和架构进行选择。
- 首选: 尽量避免使用内联脚本。将脚本放在独立的
28-clickjacking
- 什么是点击劫持 (Clickjacking)?
- 定义: 一种视觉欺骗技术,通过在用户界面上覆盖一层透明或伪装的元素,来劫持用户的点击操作,诱使用户在不知情的情况下点击一个隐藏的、他们本意不想点击的按钮或链接。
- 本质: 这是一种利用 UI 混淆来欺骗用户,而不是直接攻击浏览器或服务器的社会工程学攻击。
- 攻击原理与实现
- 核心技术:
<iframe>。 - 步骤:
- 创建攻击页面: 攻击者创建一个自己的网页。
- 创建诱饵: 在页面上放置一个吸引用户点击的元素,例如一个“免费赢取奖品”的按钮。
- 嵌入目标页面: 攻击者使用一个
<iframe>将受害者的网站(例如,一个银行转账页面)嵌入到自己的页面中。 - 视觉伪装:
- 将
<iframe>设置为完全透明 (opacity: 0)。 - 使用 CSS 定位,将这个透明的
<iframe>精确地覆盖在诱饵按钮之上。 - 关键点:确保
<iframe>中那个危险的按钮(例如,“确认转账 100 美元”)正好位于诱饵按钮的正下方。
- 将
- 劫持点击: 当用户以为自己正在点击“免费赢取奖品”按钮时,他们的点击事件实际上是穿透了那个(在视觉上不存在的)诱饵,落在了下面那个透明
<iframe>中的“确认转账”按钮上。
- 示例代码的关键点:
- 使用
pointer-events: noneCSS 属性可以让诱饵按钮本身不响应任何鼠标事件,确保点击能够“穿透”下去。
- 使用
- 核心技术:
- 防御点击劫持的措施
- 可以通过设置 HTTP 响应头或使用 JavaScript 来防止你的网站被恶意地嵌入到
<iframe>中。
X-Frame-Options响应头:- 这是一个较早的、专门用于防御点击劫持的响应头。
DENY: 完全禁止任何页面通过<iframe>嵌入此页面。SAMEORIGIN: 只允许来自相同源的页面嵌入此页面。
- CSP 的
frame-ancestors指令:- 这是现代的、更灵活的替代方案。
frame-ancestors 'none': 效果等同于X-Frame-Options: DENY。frame-ancestors 'self': 效果等同于X-Frame-Options: SAMEORIGIN。- 更强大之处: 它可以指定一个允许嵌入的域名白名单,例如
frame-ancestors 'self' <https://partner-site.com>。 - 建议: 如果不需要考虑非常古老的浏览器,应优先使用
frame-ancestors。为了更好的兼容性,可以同时设置这两个头。
- JavaScript “破框” (Frame Busting):
- 作为一种备用或补充防御,可以在你的页面中加入一段 JavaScript 代码。
- 代码:
if (window.top !== window.self) { window.top.location = window.self.location; } - 原理:
window.self指向当前窗口,window.top指向最顶层的窗口。如果一个页面被嵌入在<iframe>中,那么window.top将不等于window.self。 - 作用: 一旦代码检测到自己被嵌入
<iframe>,它会强制将整个顶层页面重定向到自己本来的 URL,从而“打破”这个框架。 - 注意: 现代浏览器可能有安全机制会阻止这种自动重定向,并提示用户,所以它不如响应头可靠。
- 可以通过设置 HTTP 响应头或使用 JavaScript 来防止你的网站被恶意地嵌入到
- 结论
- 防御点击劫持的最佳实践是使用
X-Frame-Options和/或 CSP 的frame-ancestors响应头。 - 多层防御(同时使用响应头和 JavaScript 破框代码)能够提供更强的保护。
- 防御点击劫持的最佳实践是使用
29-postmessage
window.postMessageAPI 简介postMessage是一个用于在不同窗口(例如,父页面与嵌入的iframe、或通过window.open打开的窗口)之间安全地进行跨源通信的机制。- 尽管是为安全通信设计的,但如果使用不当,它本身也会引入安全漏洞。
- 漏洞一:接收方未验证消息来源
- 场景: 一个
iframe中的页面设置了一个事件监听器来接收来自父页面的消息。// 在 iframe 内部 (不安全的代码) window.addEventListener("message", (event) => { // 没有检查 event.origin! document.body.innerHTML += event.data; }); - 问题: 这段代码会接收任何来源发送过来的消息,并将其内容盲目地插入到 DOM 中。
- 攻击: 攻击者可以将这个
iframe嵌入到自己的恶意网站中,然后使用postMessage向其发送包含恶意脚本(XSS 载荷)的消息。由于iframe内部代码没有验证消息来源,它会执行这段恶意脚本。 - 修复方法(接收方): 在处理消息之前,必须检查
event.origin属性,确保消息来自一个可信的、预期的域名。// 安全的代码 const TRUSTED_DOMAIN = "<http://localhost:4108>"; window.addEventListener("message", (event) => { if (event.origin !== TRUSTED_DOMAIN) { return; // 忽略来自未知来源的消息 } // ...安全地处理 event.data... });
- 场景: 一个
- 漏洞二:发送方未指定目标来源
- 场景: 父页面向一个
iframe发送敏感信息。// 在父页面 (不安全的代码) const iframe = document.querySelector("iframe"); iframe.contentWindow.postMessage("这是敏感信息", "*"); - 问题: 第二个参数
targetOrigin被设置为 (通配符),这意味着这条消息可以被发送到任何来源的iframe。 - 攻击: 攻击者可以诱使用户访问一个页面,该页面看起来正常,但实际上嵌入的
iframe已经被替换成了攻击者控制的恶意页面。当父页面发送消息时,这些敏感信息就会被攻击者的iframe截获。 - 修复方法(发送方): 在调用
postMessage时,必须将targetOrigin参数指定为确切的目标窗口的来源(协议、主机和端口)。// 安全的代码 const TARGET_IFRAME_ORIGIN = "<http://localhost:4108>"; iframe.contentWindow.postMessage("这是敏感信息", TARGET_IFRAME_ORIGIN); - 如果目标
iframe的实际来源与你指定的TARGET_IFRAME_ORIGIN不匹配,浏览器将不会发送该消息。
- 场景: 父页面向一个
- 结论
- 使用
postMessage时,必须同时在发送方和接收方进行严格的来源(Origin)验证,以确保通信信道的安全。 - 接收方: 验证
event.origin。 - 发送方: 指定确切的
targetOrigin,避免使用 。
- 使用
30-tabnabbing
-
什么是标签页劫持 (Tabnabbing)?
- 定义: 一种复杂的网络钓鱼攻击。当用户从一个页面(A)点击链接打开一个新的标签页(B)后,原始页面(A)在后台利用其与新开页面(B)的连接关系,将自己重定向到一个伪造的、看起来像原始网站的钓鱼页面。
- 目标: 当用户切换回原始标签页(A)时,他们会看到一个假的登录页面,并可能在不知情的情况下输入自己的凭证。
-
攻击原理
window.opener: 当页面 A 通过window.open或<a target="_blank">打开页面 B 时,页面 B 可以通过window.opener属性获得对页面 A 的window对象的引用。- 权限: 尽管同源策略会限制大部分操作,但
window.opener仍然允许页面 B 对页面 A 进行一些操作,最危险的就是修改页面 A 的位置:window.opener.location = '<https://fake-login-page.com>'。 - 攻击流程:
- 用户访问一个恶意或被攻陷的页面。
- 用户点击一个链接,该链接在新标签页中打开一个看似无害的网站。
- 当用户的注意力在新标签页上时,原始标签页中的脚本使用
window.opener将自己重定向到一个外观一模一样的钓鱼网站。 - 用户完成在新标签页上的操作后,切换回原始标签页,看到了钓鱼登录页面,并可能被骗。
-
防御措施
-
使用
rel="noopener":- 这是防御 Tabnabbing 的主要和最有效的方法。
- 在所有打开新窗口的
<a>标签上添加rel="noopener"属性。 - 作用: 这个属性会告诉浏览器,在打开新窗口时,要将
window.opener设置为null。这样就切断了新打开的页面与原始页面之间的联系,使其无法对原始页面进行任何操作。
<a href="<https://example.com>" target="_blank" rel="noopener" >打开新窗口</a > -
使用
rel="noreferrer":- 这个属性除了
noopener的所有功能外,还会阻止浏览器在向新页面发送请求时包含Referer头。 rel="noopener noreferrer"通常被一起使用,以提供更强的隐私和安全保护。
- 这个属性除了
-
iFrame 沙箱 (Sandbox):
- 如果你需要在页面中嵌入不受信任的第三方内容,可以使用
<iframe>的sandbox属性。 - 作用:
sandbox属性可以极大地限制iframe中内容的能力,例如,你可以禁止它运行脚本、提交表单、打开弹窗等。 - 这可以防止被嵌入的内容执行 Tabnabbing 或其他恶意行为。
- 如果你需要在页面中嵌入不受信任的第三方内容,可以使用
-
-
结论
- 对于所有
target="_blank"的链接,都应该养成添加rel="noopener noreferrer"的习惯。 - 现代的建站工具和 linter 通常会自动为你添加或提示你添加这个属性。
- 对于所有
31-json-web-token-security
- JWT 简介与使用场景
- 问题: 服务器端会话(Session)在现代分布式、微服务架构中面临挑战。例如,多个服务如何共享会话存储?如何处理负载均衡?
- 解决方案: JSON Web Token (JWT) 提供了一种无状态 (Stateless) 的认证机制。服务器无需在后端存储任何会话信息。
- 适用场景:
- 服务解耦/微服务架构。
- 单点登录(SSO),当认证服务和应用服务分离时。
- JWT 的结构
- JWT 是一个由三个部分组成的、用点(
.)连接的字符串:Header.Payload.Signature。 - 1. 头部 (Header):
- 通常包含两部分:令牌类型(
typ: "JWT")和所使用的签名算法(alg: "HS256")。 - 这部分信息经过 Base64Url 编码。
- 通常包含两部分:令牌类型(
- 2. 载荷 (Payload):
- 包含所谓的“声明 (Claims)”,即关于用户实体和附加元数据的信息。例如:用户 ID (
sub)、角色 (roles)、过期时间 (exp) 等。 - 重要: 载荷部分也只是经过 Base64Url 编码,不是加密的。任何人都可以解码并读取其中的内容,因此绝不能在载荷中存放任何敏感信息(如密码)。
- 包含所谓的“声明 (Claims)”,即关于用户实体和附加元数据的信息。例如:用户 ID (
- 3. 签名 (Signature):
- 这是 JWT 安全性的核心。
- 签名是通过将编码后的 Header 和 Payload,加上一个只有服务器知道的密钥 (Secret),一起使用 Header 中指定的算法进行加密生成的。
- 作用: 用于验证令牌的发送者,并确保消息在传输过程中没有被篡改。
- JWT 是一个由三个部分组成的、用点(
- JWT 的工作流程与验证
- 签发: 用户登录成功后,服务器生成一个 JWT 并将其发送给客户端。
- 传输: 客户端在后续的每一次请求中,通常通过 HTTP 的
Authorization头(Bearer <token>)将 JWT 发送回服务器。 - 验证: 服务器收到 JWT 后:
- 使用相同的密钥和算法,对接收到的 Header 和 Payload 重新计算一次签名。
- 将计算出的签名与 JWT 中附带的签名进行比较。
- 如果两个签名一致,说明令牌是有效的、未被篡改的。服务器即可信任 Payload 中的信息。
- JWT 的主要风险:无法轻易吊销
- 问题: 因为 JWT 是无状态的,服务器端没有会话记录。如果一个 JWT 在其过期之前被盗用,服务器没有任何简单的方法可以使其失效。
- 对比: 对于服务器端会话,管理员只需从数据库中删除对应的会话记录,该会话就立即失效了。
- 缓解措施:
- 设置较短的过期时间(例如 15 分钟)。
- 使用黑名单机制来记录已吊销的令牌(但这又引入了状态,违背了 JWT 的初衷)。
- 采用刷新令牌(Refresh Token)机制。
- 常见的安全漏洞
alg:none攻击:- JWT 规范允许一个名为
none的算法,即不进行签名。 - 如果服务器端的 JWT 验证库配置不当,仅仅根据 JWT 头部中的
alg字段来决定验证方式,那么攻击者就可以构造一个 JWT,将alg设置为none,并附上一个空的签名。 - 配置不当的服务器可能会接受这个未经验证的令牌。
- 防御: 服务器在验证 JWT 时,必须强制指定并只接受预期的、安全的算法(如
HS256),而不是盲目相信头部中的alg字段。
- JWT 规范允许一个名为
32-jwt-best-practices
- 在哪里存储 JWT?—— 客户端存储方案对比
localStorage- 优点: 使用简单,跨标签页共享,持久保存。
- 缺点: 最不安全的选择。
localStorage可以被页面上的任何 JavaScript 代码访问。如果你的网站存在任何 XSS 漏洞,攻击者注入的脚本可以轻易地读取并窃取存储在其中的 JWT。 - 结论: 避免使用。
sessionStorage- 优点: 同样使用简单,但生命周期仅限于当前浏览器标签页,关闭后即销毁。
- 缺点: 和
localStorage一样,它也完全可以被 JavaScript 访问,因此同样易受 XSS 攻击。 - 结论: 比
localStorage稍好,但本质上仍然不安全。
- 内存 (In-memory)
- 优点: 最安全的方式。将 JWT 存储在 JavaScript 变量中。这样它不会在页面刷新后持久存在,XSS 攻击脚本也难以在不刷新页面的情况下窃取它。
- 缺点: 用户体验差。每次用户刷新页面或关闭标签页后,都需要重新登录。
- 适用场景: 可能适用于需要极高安全性的、短暂的、一次性操作的页面。
- Cookie
- 优点: 最推荐的安全方案。
- 可以设置
HttpOnly标志,使得 JWT 无法被 JavaScript 访问,从而可以有效抵御 XSS 攻击对令牌的直接窃取。 - 可以设置
Secure标志,确保令牌只通过 HTTPS 传输。 - 可以设置
SameSite标志(Lax或Strict),提供对 CSRF 攻击的内置防御。
- 可以设置
- 缺点:
- 存在 4KB 的大小限制。如果 JWT 的载荷过大,可能存不下。
- 仍然需要防御 CSRF 攻击(尽管
SameSite提供了很大帮助)。
- 结论: 如果 JWT 大小允许,这是平衡安全性和用户体验的最佳选择。
- 优点: 最推荐的安全方案。
- 推荐的最佳实践
- 使用
HttpOnlyCookie 存储 JWT: 这是核心。 - 设置短生命周期: 将 JWT(访问令牌 Access Token)的过期时间设置得很短(例如 15 分钟)。
- 使用刷新令牌 (Refresh Token):
- 当用户登录时,同时下发一个生命周期较长的刷新令牌。
- 这个刷新令牌被安全地存储(例如,也是在一个更严格的
HttpOnlyCookie 中,并与特定的路径绑定)。 - 当访问令牌过期时,客户端可以使用刷新令牌无缝地请求一个新的访问令牌,而无需用户重新输入密码。
- 使用
- JWT 库的使用注意事项 (以
jsonwebtoken为例)- 签发 (
jwt.sign):jwt.sign({ username: "steve" }, process.env.JWT_SECRET, { algorithm: "HS256", }); - 验证 (
jwt.verify):jwt.verify(token, process.env.JWT_SECRET, { algorithms: ["HS256"] }); - 关键安全点: 在验证时,必须通过
algorithms选项明确指定一个允许的算法列表。这可以防止前面提到的alg:none攻击。
- 签发 (
33-wrapping-up
- 未深入探讨但重要的话题:密码安全
- 问题: 直接存储用户密码是绝对禁止的。
- 解决方案:哈希 (Hashing)
- 使用单向哈希算法(如 bcrypt, Argon2)将密码转换成一个固定长度的、不可逆的字符串(哈希值)。
- 验证时,对用户输入的密码执行相同的哈希算法,然后比较结果哈希值。
- 进阶问题:彩虹表 (Rainbow Tables)
- 对于简单密码,攻击者可以预先计算出常用密码的哈希值,形成一个“彩虹表”。如果数据泄露,他们可以通过查找哈希值来反推出原始密码。
- 最终解决方案:加盐 (Salting)
- 在对密码进行哈希之前,为每个用户生成一个唯一的、随机的字符串,称之为“盐 (Salt)”。
- 将“盐”和用户的密码拼接在一起,然后对这个组合进行哈希。
- 将“盐”和最终的哈希值一起存储在数据库中。
- 效果: 即使两个用户使用了相同的密码,由于他们的盐是不同的,最终存储的哈希值也会完全不同。这使得彩虹表攻击完全失效。
- 密钥管理 (Key Management)
- 挑战: 在分布式系统中,安全地存储和分发密钥(如 JWT 签名密钥、加密盐)是一个难题。直接放在环境变量中在大型系统中难以维护和轮换。
- 解决方案: 使用专门的密钥管理服务(KMS),例如:
- AWS KMS
- Azure Key Vault
- Google Cloud KMS
- 这些服务提供了安全的密钥存储、访问控制和自动轮换功能。
- 最佳实践: 定期轮换你的密钥。
- 课程核心理念回顾
- 多层防御 (Defense in Depth): 安全不是靠单一措施实现的。你需要结合使用多种技术(净化输入、CSP、CORS、CSRF 令牌、安全的 Cookie 设置等)来构建一个纵深防御体系。
- 理解原理,而非盲从: 像 CSP 和 CORS 这样的机制,初看起来可能是开发的障碍。但真正理解它们的工作原理和设计初衷后,它们就会成为你保护应用的强大工具。
- 人为因素: 大多数安全漏洞源于两种情况:知识的盲区(“我不知道还有这种攻击”)或失误(“当时太累了,犯了个错”)。
- 目标: 通过建立对 Web 安全的整体心智模型,开发者可以在日常工作中做出更明智的决策,避免这些常见的陷阱,从而有效地保护用户和公司。