Repository navigation
fix(plugin-auth): an IPv6 CIDR in the allowed IP ranges admits the IPv6 clients inside it, at the sign-in gate and the save check - #30392
Conversation
…IP, red) Claude-Session: https://claude.ai/code/session_01Utp6CXwcgsAkqebfzjnfiS Co-authored-by: Claude <noreply@anthropic.com>
…v6 clients inside it ipMatchesRange read only IPv4 CIDR and exact addresses, so an IPv6 entry such as 2001:db8::/32 never matched a real client at the sign-in gate or at the settings save check. Read both sides as the address they denote and compare the first n of 128 bits for an IPv6 range; IPv4 entries keep their reading, and families never cross. Claude-Session: https://claude.ai/code/session_01Utp6CXwcgsAkqebfzjnfiS Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 3 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 2 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 22 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin bbc06d90fbd6f4994caa1322a74e62a16b18a047 && git checkout bbc06d90fbd6f4994caa1322a74e62a16b18a047
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 870ebce333bf0a5794307febe077ab5652cfbfe9 aa57f6ddcbd8620bc216cb70f2b618ab5c055c91 && git checkout -B drift-repro 870ebce333bf0a5794307febe077ab5652cfbfe9 && git merge --no-ff aa57f6ddcbd8620bc216cb70f2b618ab5c055c91
node scripts/docs-audit/affected-docs.mjs --json 870ebce333bf0a5794307febe077ab5652cfbfe9
|
|
domain:server#2 · os-bill · session_01Utp6Cbvcmv8YWFhFJwzjKL 维护者速读
席位核对 — ACCEPT · domain:server#2 · head
|
Requested by Bill · project thread
domain:server#2 · os-bill · session_01Utp6CXwcgsAkqebfzjnfiS
Fixes #30320
Clause-②: no — a fix: an IPv6 CIDR entry now admits the clients the list already names; no export, no new refusal, no settings key or manifest shape changes.
Metadata-protocol: none
维护者速读
2001:db8::/32)现在按网段生效:登录闸门放行网段内的 IPv6 用户,设置页保存时也认得出保存者的 IPv6 地址在网段内。精确的 IPv6 地址不论大小写或缩写写法都能匹配。packages/plugins/plugin-auth/src/auth-manager.ts的ipMatchesRange与readMatchedAddress(地址族判定与前缀比较);packages/plugins/plugin-auth/src/allowed-ip-ranges.pin.test.ts末尾的#30320用例(经真实设置路由保存和登录闸门两道门)。pnpm --filter @objectstack/plugin-auth exec vitest run src/allowed-ip-ranges.pin.test.ts,预期 1 个文件、11 条全绿。What changes
The rule: an allowed-IP entry is read as the address or range it names, in its own family, at every door that judges the list.
ipMatchesRangereads IPv6 CIDR (@objectstack/plugin-auth). It read IPv4 CIDR and exact addresses only; an IPv6 entry such as2001:db8::/32fell to an exact text comparison against its base and never matched a real client. Both sides are now read as the address they denote (readMatchedAddress): an IPv4 address as a uint32, any other IPv6 address as eight 16-bit groups through the file's existingparseIpv6Literal(::elision, embedded IPv4 tail). An IPv6 range compares the firstnof 128 bits. The sign-in gate (AuthManager.isClientIpAllowed) and the settings save check (allowed-ip-ranges.ts, from fix(plugin-auth)!: clearing the allowed IP ranges applies at once, and a console save that leaves out the saving address is refused #30309) both call it, so they keep judging the same address the same way.2001:DB8:0:0::1matches2001:db8::1;0:0:0:0:0:0:0:1matches::1.::ffff:203.0.113.5, as before) or hex (::ffff:cb00:7105, now too), is the IPv4 address it maps, so a dual-stack listener's IPv4 peers keep matching IPv4 ranges.0.0.0.0/0does not admit::1), and an IPv6 range never admits an IPv4 one (::/0does not admit203.0.113.5nor its mapped form).fe80::1%eth0is judged asfe80::1./129, an empty/,/3x) leaves the entry matching only its base address, which is what the old exact-text fallback did. An entry that is not an address is still compared as text, which an address the server established never equals. IPv4 entries are read exactly as before, prefix parsing included, so no IPv4 list changes what it admits.unmapIpv4is gone (its job moved intoreadMatchedAddress; no other caller). TheisClientIpAlloweddocblock and one sentence of theclient-address-canonical.tsheader, which said the allow-list compares exact IPv6 entries as text, are corrected.allowed_ip_rangeshelp text (it promises CIDR ranges unqualified, which now holds for both families),auth-plugin.ts,packages/spec.Why
#30320: the setting promises CIDR ranges, and an IPv6 range locked out every IPv6 client inside it at sign-in and, after #30309, refused the save for an administrator connected from inside it, with copy saying the list does not include an address it textually covers.
Verification
Measured first, at
d1d90b70with the new pins: 6 failed | 7 passed (the two door pins, the CIDR pin, the exact-spelling pin, the cross-family pin through the hex-mapped case, andisClientIpAllowed); the malformed-prefix pin passed already, which is the preservation it states.Tests
auth-manager.test.ts› IP allow-list (ADR-0069 D5): five new cases — IPv6 CIDR inside/outside (/32,/56,/128,::/0,::1, a zone id); exact IPv6 by value; cross-family (IPv4 range vs::1and2001:db8::5, IPv6 range vs IPv4 and mapped IPv4, hex-mapped vs an IPv4 range, mapped base keeps its IPv4 prefix); malformed IPv6 prefixes never widen past the base;isClientIpAllowedwith an IPv6 range plus::1.allowed-ip-ranges.pin.test.ts›#30320(the shared harness: realAuthPlugin, realSettingsServicewith the shipped auth manifest, real settings routes): the gate admits an IPv6 client inside an allowed IPv6 range and refuses one outside and an IPv4 one; a save from an IPv6 address outside is refused (400 SETTINGS_VALIDATION,constraint.clientAddress) and stores nothing, from inside is stored and enforced.pnpm --filter @objectstack/plugin-auth test: Test Files 222 passed (222), Tests 3925 passed | 10 skipped (3935), atd9855b3cand again ataa57f6dd.typecheck: exit 0.Ablations (scripts/ablation-replace.mjs; tests import src directly, no build between legs)
never matches across address familiesred (1 failed)isClientIpAllowedred (3 failed)Number()(empty prefix becomes/0)never widens a malformed IPv6 rangered (1 failed)Each mutation landed (anchor 1 to 0, blob changed); each restore leg reported blob equal to HEAD and
git diff HEADempty.Gates (head aa57f6d, after merging main fdad178)
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack: 62 commands derived for 5 paths (189 changed lines). Each run with its exit code recorded before any pipe;--ran: "62 derived famil(ies) accounted for — 62 run, 0 NOT-MEASURED". All 62 exit 0.check:authz-resolver,check:changeset-fixed,check:error-code-casing,check:filter-alias-parity) andpnpm lint: all exit 0.pnpm --filter @objectstack/plugin-auth testafter rebuilding the closure on the merged head: Test Files 222 passed (222), Tests 3925 passed | 10 skipped (3935);typecheckexit 0.!and no(narrowing)arm, so no full dogfood run is owed.Acceptance notes
net.BlockListwas measured and not used: checked against an IPv4 subnet it admits the mapped IPv6 form, and checked as IPv4 against a::ffff:0:0/96subnet it admits a plain IPv4 address, so its family rules are not this matcher's; the file already carries an IPv6 parser.Human Minimum
packages/specholds no runtime logic (untouched).pnpm lintpasses.main, one pull request.packages/spectouched: n/a.content/docs/references/,packages/*/CHANGELOG.mdorcontent/docs/releases/.🤖 Generated with Claude Code
https://claude.ai/code/session_01Utp6CXwcgsAkqebfzjnfiS