Repository navigation
fix(plugin-auth): the auth IP allow-list judges the client address the server established - #24795
Conversation
…ablished The ADR-0069 D5 allow-list and the D7 login audit now establish a request's client address by the platform's one client-address trust rule, the rule the runtime's inbound rate limiter already used: forwarded headers only when the stack declares server.trustProxy, the transport's peer address otherwise. With ranges configured, an address that cannot be established is refused (403 IP_NOT_ALLOWED) and explained once in the operator log. The rule moves unchanged from runtime into plugin-auth's dependency-free ./client-address entry (runtime depends on plugin-auth), and the runtime imports it back. objectstack serve hands server.trustProxy to AuthPlugin as well as to the dispatcher. Claude-Session: https://claude.ai/code/session_011TdZ8GjixNZFLz1xJD9zzK Co-authored-by: Claude <noreply@anthropic.com>
One pin per branch, through the real raw-app mounts: a declared trusted proxy, an undeclared one, an address that cannot be established, no ranges, the IPv4-mapped peer form, and the D7 audit recording the same address or none. Claude-Session: https://claude.ai/code/session_011TdZ8GjixNZFLz1xJD9zzK Co-authored-by: Claude <noreply@anthropic.com>
…t address Also keeps the package root's export surface unchanged: the rule is reached only through the ./client-address entry. Claude-Session: https://claude.ai/code/session_011TdZ8GjixNZFLz1xJD9zzK Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011TdZ8GjixNZFLz1xJD9zzK Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 4 package(s): 10 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 3 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 54 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 830416a5d30fdbb4febf6ffc9234211919ed6865 && git checkout 830416a5d30fdbb4febf6ffc9234211919ed6865
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin a43405d596d4d670dc761e90c9fc20822dc806a9 9f60a2ba3cc5156c9b0722fd95a761adcbe7f187 && git checkout -B drift-repro a43405d596d4d670dc761e90c9fc20822dc806a9 && git merge --no-ff 9f60a2ba3cc5156c9b0722fd95a761adcbe7f187
node scripts/docs-audit/affected-docs.mjs --json a43405d596d4d670dc761e90c9fc20822dc806a9
|
席位核对 · ACCEPT ·
|
Fixes #24739
Clause-②: no
What changes
The ADR-0069 D5 IP allow-list (
allowed_ip_ranges) and the D7 login audit (last_login_ip) now establish a request's client address by the platform's one client-address trust rule, the rule the runtime's inbound rate limiter already used:X-Forwarded-For(its first entry), thenX-Real-IP, count only when the stack declaresserver.trustProxy.IHttpRequest.remoteAddress).403 IP_NOT_ALLOWED. That is the code and status the gate already uses for an address outside the ranges. The first such refusal logs one operator warning that namesallowed_ip_rangesandserver.trustProxy.The maintainer's direction, verbatim from the card: 「登录 IP 白名单信任可伪造的 IP 头,取不到 IP 时还放行」
How
packages/plugins/plugin-auth/src/client-address.ts(new)resolveClientAddress: the address half of the runtime'sresolveRateLimitKey, moved unchanged. It is published as the dependency-free@objectstack/plugin-auth/client-addresssubpath (679 B ESM, no imports), beside./rate-limit-storage.packages/runtime/src/security/inbound-rate-limit.tsresolveRateLimitKeyimports the rule instead of keeping its own copy. Its keys are unchanged, and the limiter's own pins are green (below).packages/plugins/plugin-auth/src/auth-manager.tstrustProxyoption.resolveClientAddress/trustsForwardedHeaders.isClientIpAllowedrefuses an undetermined address while ranges are set.ipMatchesRangecompares an IPv4-mapped IPv6 address as its IPv4 address: a dual-stack socket reports IPv4 peers that way, and the gate now judges the peer.handleRequest(request, { remoteAddress })establishes the address once and carries it to the D7 stamp in better-auth's request state. TheallowedIpRangesTSDoc no longer states the fail-open.packages/plugins/plugin-auth/src/request-client-address.ts(new)packages/plugins/plugin-auth/src/auth-plugin.tsresolveClientAddressover the request's headers and transport peer. The wildcard route hands the same peer tohandleRequest. Two once-per-process operator warnings.packages/cli/src/commands/serve.tsobjectstack servepassesserver.trustProxytoAuthPlugin, as it already does to the dispatcher.packages/services/service-settings/src/manifests/auth.manifest.ts,translations/{zh-CN,ja-JP,es-ES}.tsallowed_ip_rangeshelp text says which address is judged. It used to say a trusted proxy settingX-Forwarded-Forwas enough.content/docs/permissions/authentication.mdxallowed_ip_rangesrow, which stated the old IP source and the fail-open..changeset/24739-auth-ip-allowlist-trust-rule.mdWhy the rule lives in plugin-auth.
@objectstack/runtimedepends on@objectstack/plugin-auth: its limiter already counts through./rate-limit-storage. So plugin-auth cannot import the runtime's copy without a dependency cycle. The rule moved, and the runtime imports it back. The package root's export surface is unchanged.Why the peer is read from the Hono context. The D5 gate and the route that feeds the D7 audit are raw-app mounts. They receive the Hono context, not an
IHttpRequest. The read mirrors the transport's ownreadRemoteAddress(plugin-hono-server/src/adapter.ts), and returns nothing on a runtime that exposes no socket.Pins, one per branch
packages/plugins/plugin-auth/src/client-ip-allow-list-trust-rule.test.tsmountsAuthPlugin.registerAuthRouteson a real Hono app in front of a realAuthManageron the installed better-auth. It supplies the peer the way@hono/node-serverdoes. Every refusal assertsstatusandcode.auth-manager.test.ts: the old "fails OPEN when the client IP cannot be determined" case is now "refuses an address the server could not establish once ranges are configured". Two cases are new: the IPv4-mapped matcher, andresolveClientAddressbelieving forwarded headers only undertrustProxy.Ablations
Each leg was committed first and restored from
HEAD:git checkout HEAD -- PATH, orscripts/ablation-replace.mjs, which wraps the run. Each restore is proven by the blob hash matchingHEADand an emptygit diff HEAD. The pins importsrcrelatively, so nodist/is involved.auth-plugin.ts+auth-manager.tsat base795ff708d8(the old behaviour)client-address.ts: the trust branch ignoredisClientIpAllowed: no ranges refuseshandleRequestVerification
Commit readings. The merge at
9f60a2ba3cbrought onemaincommit that touches one dogfood test file and none of these packages.c42164ff22:@objectstack/plugin-authbuild exit 0 (check-dts-emitted3/3), typecheck exit 0 (test layer compiles, no new debt),vitest run166 files, 3188 passed, 10 skipped.c42164ff22:@objectstack/runtimetypecheck exit 0. 11 files that exercise the rule (security/inbound-rate-limit,endpoint-policy,endpoint-policy.auth-gate.pin,dispatcher-plugin.rate-limit.integration,dispatcher-plugin.endpoint-fallback.integration,dispatcher-plugin.multi-tenant-endpoint.integration,http-metrics-inbound-coverage.hono.integration,http-dispatcher.auth-gate-outage.pin,api-endpoint-step,error-envelope.conformance,domains/anonymous-gate-actions-automation): 244 passed. The rest of the runtime suite is declared to CI.c42164ff22:@objectstack/service-settingstypecheck exit 0, 53 files, 894 passed.9f60a2ba3c:@objectstack/cliclosure build 60/60, typecheck exit 0,vitest run --project unit314 files green. Twopublished-subpath-*pins first refused as PREREQUISITE NOT MET (packages/cliunbuilt). Afterpnpm --filter @objectstack/cli buildthey passed, 29/29. The integration layer is declared to CI.9f60a2ba3c:node scripts/pm/dispatch-gates.mjs --commandsderived 116 commands, and all 116 exit 0. Three of them (check:skill-examples,check:dual-build-cjs-loads,check:i18n-coverage) first exited 3 for unbuilt packages and were re-run after a cached repo build.--ran: 116 derived, 116 run, 0 NOT-MEASURED, 0 UNRUN.Cross-lane surfaces
packages/runtime/src/security/inbound-rate-limit.ts(domain:cli): declared in the claim.packages/cli/src/commands/serve.ts(domain:cli): not in the claim's file surface. It is one option line, and it is what letsserver.trustProxyreach the gate underobjectstack serve.packages/services/service-settings(help text) andcontent/docs/permissions/authentication.mdx: text this change made false. Neither is in the claim's file surface.Acceptance notes
x-forwarded-for/cf-connecting-ip, trust-proxy aware)". This PR implements "trust-proxy aware" as the runtime's rule, which readsX-Forwarded-For, thenX-Real-IP.CF-Connecting-IPis no longer read. The ADR text is not edited here.server.trustProxydescription names one consumer, the inbound rate limiter. This change adds a second: the auth allow-list and the login audit.packages/specis not edited here.AuthManager.handleRequestpass no transport peer. Their D7 stamp therefore records no address unlessserver.trustProxyis declared. Their D5 coverage was not measured here.objectstack dev(plugin-dev) composesAuthPluginwithouttrustProxy, the same way it composes the dispatcher without it.docs/HARDENING.mdshows a hand-written limiter keyed onx-forwarded-for. It is unchanged here.Generated by Claude Code