识别与成因
重复编码最直观的标志是 %25:因为百分号自身编码为 %25,被编码两次就会出现 %25E4%25B8%25AD 这类内容,解开一层后还是编码字符串。成因通常不是恶意构造,而是链路叠加:框架自动编码一次,业务代码又编码一次;或者数据经过网关、队列、脚本转换时被重复处理。长期存在的后果不止是「不好看」——长度膨胀、日志难以检索、索引与去重失效,甚至在签名与校验环节引入不一致。
基本信息
| 单层编码外观 | %E4%B8%AD 形式,解开即得中文 |
|---|---|
| 双层编码外观 | %25E4%25B8%25AD,解开一层仍是编码串 |
| 层数判据 | 数 %25 的嵌套次数,或逐次解码直到可读 |
| 常见成因 | 框架自动编码叠加业务代码编码、链路重复处理 |
| 影响 | 长度膨胀、检索与去重失效、签名不一致 |
| 修复原则 | 数据与链路同时修,避免复发 |
典型使用场景
- 接口联调:参数值里出现 %25,值是「中文的编码」而不是中文
- 数据清洗:历史数据被重复编码,需要确定层数后逐层还原
- 日志分析:检索关键字时常因编码层数不同而漏查
- 签名排查:多层编码导致签名基串与预期不符
- 安全审查:重复编码常被用来绕过过滤器,需按最终形态校验
层数判断与还原
层数判断与还原
# 逐层解码并观察内容变化
python - <<'PY'
import urllib.parse as u
s = '%25E4%25B8%25AD'
for i in range(3):
s2 = u.unquote(s)
print(i, s2)
if s2 == s:
break
s = s2
PY
# 快速数层:统计 %25 出现的次数
python -c "s='%25E4%25B8%25AD'; print(s.count('%25'))" # 1 表示还有一层
# 修复:按实际层数逐层还原,而不是不管层数统一解一次
python -c "import urllib.parse as u; print(u.unquote(u.unquote('%25E4%25B8%25AD')))" # 中文层数与处置
| 外观 | 层数 | 处置 |
|---|---|---|
| 中文 | 0 | 无需处理 |
| %E4%B8%AD | 1 | 解码一次即可 |
| %25E4%25B8%25AD | 2 | 解码两次;同时检查编码环节 |
| %2525E4%2525B8%2525AD | 3 以上 | 按层数循环解码,并排查链路 |
| 部分可读部分仍编码 | 不一致 | 说明不同环节规则不同,需分段核对 |
修复要看「数据里存的是编码后的字符串」还是「展示层多解了一层」:前者要修数据,后者只需修显示逻辑。混淆这两种情况会把正确数据改成错误数据,动手前先用样本确认。
重复编码与安全过滤的顺序很关键:过滤器只解码一次、校验后仍残留编码内容,攻击者可能借此绕过规则。校验应对解码后的最终内容进行,并限制解码层数,防止无限解码造成资源消耗或绕过。
根治要落在链路上:确认框架是否自动编码、网关是否改写参数、脚本是否重复处理。只修数据不改链路,下一批数据很快又会被叠加编码,运维成本持续累积。