Skip to content

[urlguard] 层 1 对尾部分隔符变体整体放行:单 ASCII 尾点被拦、双尾点(localhost../127.0.0.1..)与全角尾点(。.。)全部 PASS——#266 折叠修复只覆盖 host 内部,回环/云 metadata/约定域漏过 #413

Closed

Description

@zy84338719

结论

层 1(CheckURL/CheckHost 字面量判定)对 host 尾部的分隔符变体整体失明:127.0.0.1。(单 ASCII 尾点)被正确阻断,但只要在尾部再多一个点(ASCII ..)或把尾点写成全角形态(。.。),同一目标全部从「BLOCKED」翻成「PASS」——回环 / 云 metadata / localhost 约定域三类层 1 专门要挡的目标全部漏过。包内 127。0。0。1(内部全角)被 #266 正确阻断,尾部形态是那次修复的漏网。

实测(当前 main,42d836e)

① ASCII 尾点:单点拦、双点放

CheckURL "http://localhost/"              => BLOCKED: urlguard: blocked domain suffix: host "localhost" uses blocked domain "localhost"
CheckURL "http://localhost./"             => BLOCKED: urlguard: blocked domain suffix: host "localhost" uses blocked domain "localhost"
CheckURL "http://localhost../"            => PASS
CheckURL "http://localhost.../"           => PASS
CheckURL "http://127.0.0.1/"              => BLOCKED: urlguard: address in blocked range: 127.0.0.1 is in blocked range 127.0.0.0/8
CheckURL "http://127.0.0.1./"             => BLOCKED: urlguard: address in blocked range: 127.0.0.1 is in blocked range 127.0.0.0/8
CheckURL "http://127.0.0.1../"            => PASS
CheckURL "http://169.254.169.254./"       => BLOCKED: urlguard: address in blocked range: 169.254.169.254 is in blocked range 169.254.0.0/16
CheckURL "http://169.254.169.254../"      => PASS
CheckURL "http://foo.localhost./"         => BLOCKED: urlguard: blocked domain suffix: host "foo.localhost" uses blocked domain "localhost"
CheckURL "http://foo.localhost../"        => PASS

② 全角/半角尾分隔符:内部被拦、尾部全放

CheckURL "http://127.0.0.1./"                 => BLOCKED: urlguard: address in blocked range: 127.0.0.1 is in blocked range 127.0.0.0/8
CheckURL "http://127.0.0.1。/"                 => PASS
CheckURL "http://127.0.0.1./"                 => PASS
CheckURL "http://127.0.0.1。/"                 => PASS
CheckURL "http://localhost。/"                 => PASS
CheckURL "http://localhost./"                 => PASS
CheckURL "http://169.254.169.254。/"           => PASS
CheckURL "http://foo.localhost。/"             => PASS
CheckURL "http://0.0.0.0。/"                   => PASS
CheckURL "http://127。0。0。1/"                 => BLOCKED: urlguard: address in blocked range: 127.0.0.1 is in blocked range 127.0.0.0/8
CheckURL "http://127.0.0.1。../"               => PASS

③ WHATWG 客户端(node)确实连得上——与 #266 同款威胁口径

node:
"127.0.0.1。" => hostname: "127.0.0.1"
"127.0.0.1."  => hostname: "127.0.0.1"
"169.254.169.254。" => hostname: "169.254.169.254"
"127。0。0。1"  => hostname: "127.0.0.1"
dns.lookup("127.0.0.1") => 127.0.0.1
dns.lookup("169.254.169.254") => 169.254.169.254

即:判定放行 + WHATWG 解析器归一后连回环/metadata ——与 #266 被接受时的论证逐字同构(「浏览器与 node 的 URL 解析器会把 …归一成 127.0.0.1 再连接,纯 ASCII 判定面对它们全部失明——判定放行 + 客户端连回环」)。

根因

  1. canonicalHost 用 strings.TrimSuffix(host, ".")——只剥一个 ASCII 尾点,localhost.. 剥完还剩 localhost.;且它在 checkHostLiteral 里先于 foldHostForGuard 执行,全角尾分隔符(。.。 → .)折叠发生在 trim 之后,折叠产物再也得不到剥点。
  2. 折叠后的 127.0.0.1. 两个判据都接不住:net.ParseIP("127.0.0.1.") = nil(不认尾点);looksLikeExoticIPv4 对它 split(".") 得 5 段 → len(parts) > 4 直接 return false(4 段空串变体 127.1. 也因空段 parseIPv4Part("") 早退而漏网)。
  3. 于是走到「普通域名」分支 → 无后缀命中 → checkHostPolicy(未注入钩子恒 nil)→ PASS。

边界(诚实说明)

修法方向

  • fold 之后重新执行尾点归一:canonicalHost 改 strings.TrimRight(host, ".")(剥全部 ASCII 尾点),并在 checkHostLiteral 拿到 folded 之后再剥一次尾点;
  • looksLikeExoticIPv4 补空段/尾点形态(任一段为空 → 按候选处理而不是 return false);
  • 建议加回归表:{localhost, localhost., localhost.., localhost。, 127.0.0.1, 127.0.0.1., 127.0.0.1.., 127.0.0.1。, 127.0.0.1., 169.254.169.254.。, foo.localhost..} 全组断言 BLOCKED。

探针:urlguard.New() + CheckURL 逐 URL 打印 verdict;node 侧 new URL().hostname + dns.lookup。复现零依赖,贴上的输出均为本机 GOWORK=off go run 实跑。

Activity

  1. added a commit that references this issue on Nov 12, 2026
  2. zy84338719 commented on Nov 12, 2026

    @zy84338719
    ContributorAuthor

    已合入修复 ✅(无 SemVer 发布:本次改动不发版本)

    变更内容

    urlguard 层1 尾部分隔符变体收口——剥全部 ASCII 尾点 + 全角折叠后再剥 + 空段 fail-closed

    合入:PR #421(https://github.com/oekit/pkg/pull/421),merge commit 63917515,2026-11-12T17:10:03Z

    CI:link ✅ / Lint & Test / Test ✅ / Lint & Test / Lint ✅ / Zero Third-Party Deps ✅

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions