Repository navigation
fix(plugin-auth): the vendored auth library's client-address source follows server.trustProxy - #24871
Conversation
…ollows server.trustProxy better-auth resolves a request's client address with its own header reader, which knows neither the transport's peer address nor server.trustProxy. The auth instance now tells it to read one platform-owned header, and AuthManager.handleRequest -- the one door to the library's HTTP handler -- writes that header from the address the client-address trust rule established, after dropping any copy the caller sent. With no address established, the library is handed none. Claude-Session: https://claude.ai/code/session_01UmqzD97e14iws6hmVhgZ5y Co-authored-by: Claude <noreply@anthropic.com>
…s the transport peer The fixture supplied its client through a forwarded header on a deployment that declares no trusted proxy. The library's reader now follows the client-address trust rule, which does not believe that header, so the fixture hands the address the rule does believe: the transport's peer address. Claude-Session: https://claude.ai/code/session_01UmqzD97e14iws6hmVhgZ5y Co-authored-by: Claude <noreply@anthropic.com>
…ndor-client-address-trust
…ndor-client-address-trust
📓 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:
What this run could not see
Coarse fallback — 16 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 803c6c2337d06805665a2333eabfa6bafafe883d && git checkout 803c6c2337d06805665a2333eabfa6bafafe883d
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 3fbef41b4a573f751fda81a875a6dd3bc4dd5009 9d43edafb9957c23ada657e1a8db77fb5c339e54 && git checkout -B drift-repro 3fbef41b4a573f751fda81a875a6dd3bc4dd5009 && git merge --no-ff 9d43edafb9957c23ada657e1a8db77fb5c339e54
node scripts/docs-audit/affected-docs.mjs --json 3fbef41b4a573f751fda81a875a6dd3bc4dd5009
|
席位核对ACCEPT · #24799 ·
Generated by Claude Code |
Fixes #24799
Clause-②: no
What changed
The vendored auth library (better-auth 1.7.3) resolves a request's client address with its own reader,
getIPin@better-auth/coreutils/ip. That reader reads request headers only: it never sees the transport's peer address and does not knowserver.trustProxy.plugin-authcomposed the library without configuring that reader, so the library used its default.advanced.ipAddress.ipAddressHeaders, which names the one header the platform owns (client-address-vendor.ts,vendorClientAddressOptions). NotrustedProxiesis set: the header carries one address the server already decided, never a chain for the library to walk.handleRequestwrites that header.AuthManager.handleRequestis the one door to the library's HTTP handler. It now rebuilds every request it hands over (withVendorClientAddress). Any copy of the header the caller sent is dropped. The address the client-address trust rule established is written in its place. That address comes fromresolveClientAddress(@objectstack/plugin-auth/client-address, after fix(plugin-auth): the auth IP allow-list judges the client address the server established #24795), which is reused here and not copied. When no address was established, nothing is written, and the library is handed no address.Requeststays readable for the auth mount's 404 hand-on. A request that cannot be rebuilt is not handed to the library with a value its caller chose. There is no catch around the rebuild.Landing:
packages/plugins/plugin-auth/src/auth-manager.ts(the composed option and thehandleRequestwrite) and a newclient-address-vendor.tsbesideclient-address.ts.packages/spec/**,packages/runtime/**andauth-plugin.tsare untouched.Reachability (the card's first step)
Measured on
origin/mainat7c2eab929with the installed better-auth 1.7.3 and@better-auth/core1.7.3, through the real auth mounts (AuthPlugin.registerAuthRouteson Hono, a realAuthManager).Three things read the library's client address in a stock composition:
ip_address, and through it thelogin/logoutaudit rows;login_failed/account_lockedaudit address (signInClientFingerprintapplies the same reader on purpose);getIPhas four callers in the installed library family: the rate limiter,createSession, the captcha plugin, and a re-export.plugin-authcomposes no captcha.In both the untrusted-proxy deployment and the trusted-proxy deployment, the address all three answered differed from the address the D5 allow-list judged and the D7 audit stamped. The detail is withheld under the disclosure ruling recorded on #9387.
M4 (does the peer address reach the library?): it does not. The library sees the Fetch
Requestonly. The peer reacheshandleRequestas itstransport.remoteAddressargument.Pins: one per branch, ablation-verified
client-address-vendor-trust-rule.test.tsuses the D5/D7 suite's fixture: real mounts, a realAuthManager, the peer onc.env.incoming. Each pin reads all three seams off one composition: the minted session'sip_address, thelogin_failedevent's address, and every key the library's limiter consumed (recorded throughrateLimitStorage). It also reads D7last_login_ipfor agreement. Every request also carries the platform-owned header, set by the caller. The refused sign-in assertscode+status(INVALID_EMAIL_OR_PASSWORD, 401).behind a declared trusted proxy, it records and keys on the forwarded client addresswithout a declared trusted proxy, it records and keys on the connection peer, whatever the request headers saywhen the server established no address, it is handed none, whatever the request headers say. This pin compares against a per-deployment control request that carries no header at all, never against a literal, because the library's own no-address answer depends on its environment.Ablations ran on
2604658d1(fix committed first) throughscripts/ablation-replace.mjs. Each mutation was proven on disk (anchor count 1 to 0, blob changed), and each was restored to the HEAD blob withgit diff HEADempty. Each "10 passed" / "12 passed" counts the 10 tests the imported fixture module registers.ipAddressoption removed (library default reader)handleRequestwrite removedTests and gates
All readings below are on head
9d43edafb, which mergesorigin/mainat9a06f0be0. That range includes #24846 and #24843, which edit other regions ofauth-manager.ts.pnpm --filter @objectstack/plugin-auth exec vitest run --maxWorkers=2exited 0: 171 files, 3273 passed, 10 skipped.pnpm --filter @objectstack/plugin-auth typecheckexited 0:tscon src and examples, andcheck:test-typecheckOK (no new debt).node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 70 commands. Each ran with its exit code captured before any pipe, and all 70 exited 0.--ranprinted:70 derived, 70 run, 0 NOT-MEASURED, 0 UNRUN.auth-manager.test.ts: theadvancedpassthrough expectations now include the always-composedipAddress.auth-account-event-audit.test.ts: the fixture hands its client as the transport peer. It had supplied the client through a forwarded header on a deployment that declares no trusted proxy, which is the behaviour this card removes.pnpm lintand the fullcheck:*farm) are left to CI.Acceptance notes
auth.api.*call is handed whatever headers its caller passes. OnlywithVendorClientAddresswrites the header, which is documented at the constant and in the changeset. Measured in-repo, no programmatic call reaches an address reader with a client's headers. The calls aregetSession(reads no address), boot-timesignUpEmail(no headers) andrequestPasswordReset(reads no address).last_login_ipstores the established address verbatim. This is pre-existing library behaviour, unchanged.domain:servicesseat 2 at review.server.trustProxy. Otherwise sessions and audit rows record the proxy's address, and the library's per-address rate limit counts every client behind the proxy as one client. This is the same posture the D5 allow-list and the runtime's inbound limiter already take.Changeset:
.changeset/24799-vendor-client-address-trust.md(@objectstack/plugin-auth: patch).Generated by Claude Code