侧边栏壁纸
博主头像
一笑痕

人生若只如初见,
是可喜亦或者是可悲?

  • 累计撰写 201 篇文章
  • 累计收到 7 条评论

Buffer.from 解码脏 base64,坏字符不一定截断

2026-10-16 / 0 评论 / 8 阅读

Buffer.from 解码脏 base64,坏字符不一定截断

Buffer.from(text, 'base64') 没报错,不等于输入符合 Base64 语法。它更像宽容解码器:某些字符被忽略,缺失的 = 通常不妨碍解码,结果只留下当前解码器能还原的完整字节。拿它做格式校验,会混淆解码成功与输入合规。它返回的是 Buffer,没有同时附带一份"原文通过语法检查"的状态。调用方若只保留返回值,原始文本里曾经出现什么非法字符也就无从追溯。

坏字符过去了,后面的字符还会解

从 SGVsbG8= 去掉填充,SGVsbG8 仍得到 Hello,十六进制是 48656c6c6f。插入一个感叹号,SGV!sbG8=,结果同样是五个字节。换行也被忽略,SGVs\nbG8= 解出的仍是 Hello。坏字符并不总意味着解码到此为止。

更容易漏掉的是尾随脏字符:YQ$ignored 解出 6108a09e8ade,不是只解出 a。$ 被跳过后,后面的字母仍参与解码。若应用预期遇到脏数据就拒绝,直接把结果交给 Buffer.from 就会产生静默接受;若仅仅是给后端一个字节串,这份字节串也可能和发送方以为的内容不同。

缺一两位时,尾巴会被丢掉

Base64 每四个符号通常表示三个字节。没有补齐的输入仍可被处理,但最后不足以组成字节的部分不会凭空补成数据。实验里,Y 得到空 buffer,YW 得到 61,YWJ 得到 6162。对应长度分别是 0、1、2 字节。

填充符也不是严格的边界检查。YQ=== 仍解出 61;但 Y=Q= 在首个符号后就停住,结果为空。两类输入都不会抛异常,字节却完全不同。只检查有没有异常,发现不了这类截断。

字符表宽容,编码也不一定唯一

标准 Base64 与 URL 安全版本的两个符号不同:+、/ 对应 -、_。Buffer 接受 URL 安全字符;____ 得到 ffffff,而 +___ 同时混用了两套字母,仍得到 fbffff。在接口边界上允许混合输入,不代表协议本身规定了混用。

还有一种更隐蔽的差异:YQ==、YR==、Yf== 都得到同一字节 61。最后一个 Base64 符号里有未用于输出字节的低位,解码结果不受这些位影响。于是多个文本表示可以指向同一份 bytes。若签名、缓存键或去重逻辑按原始文本比较,它们不会相等;若业务按解码后的字节比较,它们会相等。

接口接收文本时,边界条件最好在进入解码器前就确定。例如协议只允许标准字母表,URL 安全字符就应明确拒绝,而不是等两者混在一起再解释;若协议允许省略填充,也要把末尾只有一个符号的行为写清楚。字符清洗也不应悄悄发生在另一层,否则审计日志留下的是清理前文本,业务拿到的却是清理后字节,两边很难对账。

文本与字节的用途也要分开。Base64 是字节的文本表示,不是文本内容的校验和;同一组字节可以有不同输入表示,而不同字节串也可能在解码前看起来只差一个符号。若后续会做签名验证、文件类型识别或长度限制,应在确定解码规则后对 Buffer 本身检查。只在原始字符串上做长度判断,无法替代解码后按字节数设上限。

我倾向于把解码和校验分成两步,因为这里的"宽容"会改变边界,而不是只方便少写一个 =。需要兼容旧客户端时可以允许非规范输入。先把允许的字符范围写清楚,再单独决定是否接受缺少填充的尾部;解码完成后,还要对字节做业务校验。需要唯一文本表示的场景,则应校验格式并要求规范化编码;不能把 Buffer 没报错当作合规证明。

本机输出逐字节列在这里

验证脚本使用 Node v26.8.1,只调用 Buffer.from(input, 'base64'),按十六进制显示结果。没有把样例转成字符串再比较,以免字符显示掩盖空字节或长度差异。以下为 node tools/verify-node-base64-decoding.mjs 的原样输出:

node=v26.8.1
canonical   input="SGVsbG8="    hex=48656c6c6f  bytes=5
missing-padding input="SGVsbG8" hex=48656c6c6f  bytes=5
one-byte-no-padding input="YQ"  hex=61  bytes=1
invalid-character-middle    input="SGV!sbG8="   hex=48656c6c6f  bytes=5
whitespace  input="SGVs\nbG8="  hex=48656c6c6f  bytes=5
base64url-alphabet  input="____"    hex=ffffff  bytes=3
mixed-standard-url-alphabet input="+___"    hex=fbffff  bytes=3
extra-padding   input="YQ==="   hex=61  bytes=1
early-padding   input="Y=Q="    hex=    bytes=0
one-symbol  input="Y"   hex=    bytes=0
two-symbols input="YW"  hex=61  bytes=1
three-symbols   input="YWJ" hex=6162    bytes=2
trailing-invalid    input="YQ$ignored"  hex=6108a09e8ade    bytes=6
nonzero-unused-bits-A   input="YR=="    hex=61  bytes=1
nonzero-unused-bits-B   input="Yf=="    hex=61  bytes=1

这组样例足以说明解码器的宽容边界,但不覆盖每一种非法 Unicode 字符或所有 Node 版本。部署环境换了运行时版本,最好把关键输入留在回归测试里。解析 Base64 的职责是产出字节;判断输入是否符合接口约定,仍应由显式校验完成。

    🤞 分享