security

Security 工具

Web 工程里那些日常的安全杂活——算哈希、估密码熵、看 JWT 声明、写 CSP、以及把抓取结果交出去之前先脱敏——以及每件事对应哪个工具。

11 工具

§01 领域指南

Web 安全里的“杂活层”

一个普通工程周里的大部分安全工作并不是对抗性的,而是机械的:挑一个摘要算法、生成一 个能满足某人策略要求的凭证、读一个令牌的声明看看认证为什么失败、写一份不会把站点搞 坏的 Content-Security-Policy、在把抓取结果粘进工单之前先清洗它。这些看上去都不像 渗透测试,而真实事故恰恰就从这里开始——失效模式是坏习惯,不是能力缺失。

能预防掉大部分坏习惯的模型,是一个被很多人压成一个概念的三分法。编码——Base64、 百分号编码、Punycode——是可逆的,且不涉及任何秘密;它改变数据的形状,从不改变其机密 性。哈希——SHA-256SHA-512——是单向的,同样不涉及秘密;它能证明两串字节完全 相同,仅此而已。带密钥的运算——HMAC、JWT 签名、TLS——是唯一能证明某个东西由谁 产生的,而且只在密钥保持秘密期间成立。没有密钥就没有认证:一个 Base64 块、一个裸摘 要、一个解码出来的 JWT 载荷,都只是可读的声明,背后什么都没有。

第二条轴是方向。产出策略的杂活——CSP 指令、响应头、生成的密钥——只有当你在已部 署的响应里验证过那个产物之后才算做对。检查别人递给你的东西的杂活,风险刚好相反: 风险在那个产物内部装了什么。脱敏属于这一类,因为秘密离开一家公司最常见的方式是一份 附在 bug 报告上的 HAR,不是一次漏洞利用。

每种原语实际保证什么

原语能证明不提供常见误用
Base64 / URL 编码让字节安全地穿过文本信道机密性——谁都能反过来解在载荷里“藏”一个 API 密钥
SHA-256 / SHA-512完整性:同样的输入得到同样的摘要保密性;对低熵输入的暴力破解成本存储密码哈希
SHA-1与历史校验和保持兼容抗碰撞(实际上已被攻破)签名;对恶意输入做去重
HMAC(哈希 + 密钥)密钥保持秘密期间的真实性机密性——载荷仍然可读把密钥发到浏览器
Argon2id / bcrypt / scrypt加盐且刻意放慢的密码存储速度——慢就是它的特性为了“性能”换成快哈希
JWT 签名(HS256RS256签发方真实性——仅在验证之后只解码的话,什么都不能证明相信解码出来的载荷里的声明
Content-Security-Policy浏览器执行的资源加载白名单修掉注入本身;非浏览器客户端script-src 里留着 'unsafe-inline'
TLS + Strict-Transport-Security传输机密性;不会降级到 http关于应用逻辑的任何论断把小锁图标读成“这个应用是安全的”

需要多少熵才够

熵是 长度 × log2(池大小),所以字符池的重要程度不亚于长度。

字符池每字符比特8 字符14 字符20 字符
a–z(26)4.70386694
a–z0–9(36)5.174172103
a–zA–Z0–9(62)5.954883119
四类全用(87)6.445290129

低于 40 比特就是坏的;80 比特是重要账户的下限;一个随机 UUID v4 携带 122 比特。

选对工具

要指纹的话,哈希生成器 经由 Web Crypto 一次算出 SHA-1SHA-256SHA-384SHA-512,所以得到的摘要与 echo -n "text" | shasum -a 256 逐字节一致——用来核对一个没人记下算法的校验和是最快 的办法。没有 MD5,因为 Web Crypto 没有实现它。

要自己创建密钥时,问题是这个值最终由谁持有。 密码生成器 适合由人类或密码管理器持有、并且站点强加了长度或字符 集策略的场合;它会按你启用的字符池实时报告熵。UUID v4 生成器 适合 消费方是机器、且你想要一个不需要协商字符集的不透明标识符的场合。两者都基于 CSPRNG—— 密码生成器取自 crypto.getRandomValues,UUID 生成器取自 crypto.randomUUID()——所以 都不会退回到 Math.random

要处理令牌时,JWT 令牌解码器header.payload.signature 切开,把前两段格式化输出,并把 iatexpnbf 渲染成 ISO 8601 并给出过期判定——一次粘贴就能定下“这是过期了,还是声明写错了?”。如果你从 日志里只抢救出一个分段,就用 Base64 编码 / 解码;Base64url 用的是 -_,所以先把它们换回 +/

要处理策略时,把编写和验证分开。 Content Security Policy 生成器 在一套加固基线之上、用 14 个按 指令划分的字段拼出这个头——default-src 'self'object-src 'none'base-uri 'self'frame-ancestors 'none'——而 CSP 工具集 会逐条 指令展开讲。然后用 HTTP 头信息检查工具 证明已部署的响应确实带上 了它,该工具在服务端发起请求,展示源站原样发出的响应头。CDN 会改写响应头,而一个 <meta> 标签承载不了 frame-ancestors

分享之前,HAR 文件查看器 用来读你自己的抓取——每条的方法、状态、 MIME 类型、大小和耗时——而 HAR 文件清洗器 负责准备那份会离开 你机器的副本,把 cookieauthorizationx-api-key 之类的头替换成 [REDACTED], 清空正文,并掩掉形似令牌的查询参数。当怀疑对象只是一个 URL 时, URL 解析和分析工具 会解码查询串,让你看出里面是否坐着一个签名 令牌;而 IDN / Punycode 转换工具 能揭示一个仿冒域名在 xn-- 形式下是不是其实用的西里尔字母。

除了头信息检查工具会把你的 URL 发到 api.sitekits.dev,以上全部在本地运行。 /zh/for/security/ 把其中大部分收在一页——通用型的那几个(UUID、 Base64、HAR 查看器)归在别的角色集合里;隐私工具集 讲的是你的 浏览器会泄露什么。

真正会引发事故的坑

解码过的令牌不是验证过的令牌

解码只能证明它是格式正确的 Base64url。验证需要签发方的密钥、一个钉死的算法,以及服务 端对 exp / nbf / iss / aud 的检查。绝不要从令牌自己的 alg 头里取算法—— alg: none 和 RS256 转 HS256 的混淆攻击就是这么成立的。

快哈希不是密码存储

对密码做一次裸 SHA-256 是一场 GPU 基准测试,不是防御;不加盐的话,一张彩虹表就能 破掉每一个用户。用哈希比较文件则是相反的情形——那里快摘要才是正确选择。

'unsafe-inline' 抵消掉 CSP 的大半

它恰好重新放行了 CSP 存在的意义所要阻止的那种被注入的内联脚本。请用 nonce 或 hash。 通配的 connect-src 是同一个陷阱的另一面:即便脚本执行被锁死,数据外传的通道仍然 敞着。

脱敏是模式匹配,不是理解

清洗器比对的是一份固定的头名清单,以及参数名中包含 tokenkeysecretpasswordpasswdpwdauthsessionsigsignature 的那些。藏在 URL 路径段里的凭证,或者名字叫 t 的凭证,都会存活下来——而在一个已登录会话上得到零条 脱敏记录,是一个信号,不是一次通过。

熵描述的是生成器,不是那个字符串

这个公式只有在每个字符都是随机抽取的前提下才成立。P@ssw0rd!2024 有 13 个字符、四类 字符齐全,却躺在每一份破解字典里。一个字符串是怎么被生成的,才决定强度指示器的分数 有没有意义。

FAQ
用 SHA-256 存密码够用吗?
不够。SHA-256 在设计上就是快的,商用 GPU 每秒能算几十亿次摘要,所以攻击者拿到泄露的哈希表之后,几小时内就能破掉短密码和常见密码。请用加盐且刻意放慢的 KDF——Argon2id、bcrypt 或 scrypt——并把工作因子调好。SHA-256 适合的是文件完整性、内容寻址和变更检测,不是凭证。
我把一个 JWT 解码出来了,就可以信它吗?
不可以。JWT 的头和载荷是 Base64url 编码的,既没有加密也没有保护,所以谁都能读,谁也都能造一个带任意声明的令牌。只有拿签发方的密钥验证签名,才能证明是谁签发的。解码是给调试用的——exp、nbf 和受众必须在服务端强制校验,因为客户端的时钟可能不准,也可能是故意撒谎。
密码到底需要多少个字符?
取决于字符池,因为熵等于长度 × log2(池大小)。同时用大写、小写、数字和符号——一个 87 字符的池——每个字符约值 6.44 比特,所以 13 个字符大约是 84 比特,20 个字符约 129 比特。只用小写时每字符 4.70 比特,于是 8 个小写字符只有约 38 比特,可被暴力破解。在重要账户上请以 80 比特以上为目标。
Content-Security-Policy 能阻止 XSS 吗?
它是在遏制 XSS,而不是修掉它。一份严格的策略能阻止被注入的脚本执行或把数据外传,但仅限于会执行 CSP 的浏览器,而且前提是 script-src 里没有 'unsafe-inline' 和 'unsafe-eval'。请用正确的输出编码去修掉注入本身,把 CSP 用来限制你漏掉的那个 bug 所能造成的破坏。先用 Content-Security-Policy-Report-Only 上线,然后确认真实响应里确实带上了强制执行的那个头。
把 API 密钥 Base64 编码后放进请求载荷,能藏住它吗?
藏不住。Base64 是一种编码,不是密码算法:它不使用密钥,任何拿到载荷的人一条命令就能还原出原始字节,这也正是解码器不需要你提供任何东西就能读它的原因。更深的问题在于位置而不是格式——任何被发到浏览器或移动端二进制里的密钥都已经泄露了,无论外面裹了什么。请把长期有效的密钥留在服务端,给客户端一个能证明真实性但本身不是那个秘密的东西:一个短期令牌,或者一个用 HMAC 签名的请求,而密钥永远不离开你的基础设施。