You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
plugin-auth: the vendored auth library's own client-address reader does not follow server.trustProxy, so the addresses it records and keys on are not the ones the server established (detail withheld under the disclosure ruling) #24799
① — a defect at a named site on an authentication surface, read at source; the reach is a named real producer. Reach takes the security exception, so the first step is to measure reachability. Disclosure follows the maintainer's ruling recorded on #9387 (5323310017): this card records the defect's existence, where it lives and the governing text only. ⛔ No reproduction or exploit path goes here, on its PR or in any comment.
Filed by the domain:services PM seat, seat 1 (seat post #6021, session_011TdZ8GjixNZFLz1xJD9zzK). It comes from the os-dev report on #24739 (PR #24795, out-of-scope finding 1). ⛔ Unlabelled and ungraded: grading and routing belong to triage.
Reader, and what they do. Triage grades and routes this card. The execution seat that owns packages/plugins/plugin-auth then measures reachability and takes the fix.
The defect
The rule.server.trustProxy (packages/spec/src/system/stack-server.zod.ts): forwarded headers are believed only when the deployment declares it. Otherwise the caller's address is the transport's own peer address, "which a client cannot forge". The IHttpRequest.remoteAddress contract (packages/spec/src/contracts/http-server.ts) says the same, and adds "never substitute a header for it silently, and never fall open".
The gap.plugin-auth composes the vendored auth library (auth-manager.ts, the auth instance's options) without configuring the library's client-address source. A source read at 9f60a2ba3c finds zero hits for the library's IP options under packages/plugins/plugin-auth/src. So the library falls back to its own reader (@better-auth/core 1.7.3, dist/utils/ip.mjs), which does not consult server.trustProxy. Whatever the library records or keys on by client address is therefore not governed by the rule above.
Done when
Reachability measured: which library features read that address in a stock composition, and with what effect.
Claim: PM loop round 2 · 2026-10-20T10:10Z
Session: session_01UmqzD97e14iws6hmVhgZ5y
Account: os-support-ai (the seat's linked user as GET /user answers it; always the card's assignee)
Branch: claude/issue-24799-vendor-client-address-trust
Worktree: objectstack-issue-24799
Domain: domain:services
Seat: domain:services#2 (seat post #21118)
File surface: packages/plugins/plugin-auth/src/auth-manager.ts (the vendored auth instance's options: its client-address source only), packages/plugins/plugin-auth/src/client-address* (the trust rule PR #24795 established, reused, not copied), their tests, one new .changeset/24799-*.md. ⛔ Not packages/spec/**, ⛔ not packages/runtime/** (the trust rule's other consumers are read only), ⛔ not the sys-oauth-application/register wrapper in auth-plugin.ts (#24773, this seat) (stop on breach; explain in the report)
Container & model: M, mode:subagent, model: opus (TIER_DEFAULT; dispatch-gates --tier on this surface: no path-derived mandate, floor sonnet, default opus, ceiling fable)
Clause-②: no
Responsibility: plugin-auth composes the vendored auth library without configuring its client-address source, so the library's own reader ignores server.trustProxy | the trust rule PR #24795 put behind @objectstack/plugin-auth/client-address (the D5 allow-list and D7 audit already follow it) | NOT MEASURED; the card's first step measures which library features read that address in a stock composition
Thread-read: none
Serial constraints cleared: auth-manager.ts is not a single-claim path. Open PR #24846 (#24750, seat 1) and PR #24843 edit other regions of auth-manager.ts (the invite refusal; account lockout); whichever lands second merges main. No open PR touches the auth instance's IP options or client-address. This seat's #24773 edits only auth-plugin.ts's register wrapper.
Disclosure: the maintainer's ruling recorded on #9387 (5323310017) applies. ⛔ No reproduction or exploit path on this card, its PR or any comment.
os-dev-report
{
"issue": 24799,
"status": "done",
"branch": "claude/issue-24799-vendor-client-address-trust",
"pr": "#24871",
"session": "session_01UmqzD97e14iws6hmVhgZ5y - mode:subagent, the dispatching PM's harness-stamped id (claim comment 6288560527); identity = the branch",
"premise_still_valid": true,
"summary": "Premise held on origin/main 7c2eab9 and on the installed @better-auth/core 1.7.3. M1 confirmed: the composed auth options set no advanced.ipAddress, so the library used its default header reader. M3 measured through the real auth mounts: three library seams read the address in a stock composition. They are the session row ip_address (and the login/logout audit rows copied from it), the login_failed/account_locked audit address, and the key of the library's per-address rate limiter (on by default in production). In both the untrusted-proxy and the trusted-proxy deployment, what the library answered differed from the address the D5 allow-list judged. Detail withheld under the disclosure ruling. The library's option surface has no resolver hook, only a header list, a proxy-CIDR list and an off switch that also disables its limiter. M4: the peer address never reaches the library, which sees only the Fetch Request. Fix: the auth instance now always composes advanced.ipAddress.ipAddressHeaders naming one platform-owned header (client-address-vendor.ts). AuthManager.handleRequest, the one door to auth.handler, rebuilds each request from a clone: it drops any caller copy of that header and writes the address resolveClientAddress established, which is reused and not copied (M2). With no address established, nothing is written and the library is handed none. Three pins, one per branch (trusted proxy, untrusted proxy, no address source), each read all three seams plus D7 last_login_ip. Five ablations turned the predicted branches red.",
"tests": "Final head 9d43eda, which merges origin/main 9a06f0b, including #24846 and #24843 in auth-manager.ts. (1) bash scripts/pm/os-verify-lock.sh -c 'pnpm --filter @objectstack/plugin-auth exec vitest run --maxWorkers=2 && pnpm --filter @objectstack/plugin-auth typecheck' gave VERDICT command-exit 0: Test Files 171 passed (171), Tests 3273 passed | 10 skipped (3283); typecheck tsc src + examples OK, and check:test-typecheck OK (10 files / 88 errors / 23 pinned signatures held, no new debt). (2) New pin file client-address-vendor-trust-rule.test.ts, 3 tests named by the rule; the refused sign-in asserts code INVALID_EMAIL_OR_PASSWORD + status 401. (3) Ablations on 2604658, with the fix committed first, via scripts/ablation-replace.mjs. Each mutation was proven on disk (anchor 1 to 0, blob changed) and each restore was proven (blob == HEAD, git diff HEAD empty). The subject resolves through relative src imports, so no build or dist preflight applies. A1, composed ipAddress option deleted: 3 failed | 10 passed, all three branches red. A2, the caller copy no longer dropped: 1 failed | 12 passed, no-address branch red. A3, the raw transport peer handed over instead of the established address: 1 failed | 12 passed, trusted-proxy branch red. A4, the forwarded header handed over without declared trust: 2 failed | 11 passed, untrusted and no-address branches red. A5, the handleRequest write deleted: 3 failed | 10 passed, all three red. The 10 extra passing tests are the ones registered by the imported fixture module. (4) Existing suites moved with the behaviour. auth-manager.test.ts: the advanced passthrough now includes ipAddress. auth-account-event-audit.test.ts: the fixture hands its client as the transport peer instead of an untrusted forwarded header; 3 tests went red before that fixture fix and are green after it.",
"gates": "Local subset on final head 9d43eda. node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 70 commands. Each ran with the exit code captured before any pipe; all 70 exited 0. --ran printed: Run reconciliation - 70 derived, 70 run, 0 NOT-MEASURED, 0 UNRUN (a DERIVED zero). An earlier run on 48581c8 had check:dual-build-cjs-loads exit 3 (PREREQUISITE NOT MET); it was superseded by the 9d43eda run, where it exited 0. check-platform-checklist was not run: no exported symbol was renamed or deleted. test:repo was not run locally; it belongs to CI. CI on PR 24871 head 9d43eda, read once at report time: completed success are Check PR Size; The card this PR closes must claim this branch; Check Documentation Links; filter; No other open PR may claim the same single-writer path; Part-of PR must not also close its card; No other open PR may claim the same issue. in_progress are Build Core; Test Core (1-6/6); all Lint and Type Check lanes; Check Changeset; Governed Surface Queue Guard; Dogfood Regression Gate; Flag docs affected by code changes; Auto Label. Skipped by selector: the dogfood shards, Temporal Conformance, Console Pin Gate, Build Docs, Packed-tarball smoke, 18.x upgrade harness. Not waited on, per contract.",
"mcp_calls": "0 - no MCP GitHub tool was called",
"api_writes": "2 relay strokes (POST /repos/objectstack-ai/objectstack/dispatches, each executed as objectstack-fleet[bot]), carrying 3 REST writes. Stroke 1 was pr_create: POST /repos/objectstack-ai/objectstack/pulls (draft) and POST /repos//issues/24871/assignees, with read-back reporting body identical and assignee os-support-ai. Stroke 2 was this os-dev-report: POST /repos//issues/24799/comments. Plus git push (not REST). Reads were single-card REST GETs via gh api.",
"open_questions": [],
"out_of_scope_findings": [
"class: a · reach: public door, measured once through the auth mount (route, probe and result withheld under the disclosure ruling recorded on #9387) · filed #24877 by the domain:services seat 2 at review · this entry was redacted by the seat on the same review",
"carrier: none (承接者:无) · noted in PR Acceptance notes, not filed: the library's no-address answer, which in production is a one-time vendor warning naming library options the platform composes itself, and in development and test is a loopback placeholder; IPv6, where the library stores a /64-normalized session address while D7 last_login_ip stores the established address verbatim; programmatic auth.api.* calls, which hand the library their caller's headers (documented at the header constant and in the changeset)"
],
"line_budget": "not applicable: no skills/** or ratcheted line ledger touched. The diff is 7 files, +376/-21 vs merge base 9a06f0b, under the 3000-line human-merge threshold",
"deviations": [
"auth-account-event-audit.test.ts is edited outside the claim's listed surface. It drives AuthManager, and this change made its fixture false: it fed an untrusted forwarded header with no peer. 3 tests went red until the fixture handed its client as the transport peer.",
"client-address.ts carries a comment-only edit (consumers list).",
"origin/main was merged twice before the PR opened, per AGENTS.md section 10 (pull main before opening). The second merge was because #24846 and #24843 landed edits in auth-manager.ts.",
"Attribution follows AGENTS.md over the harness reminder: commits carry the model-free Claude-Session + Co-authored-by pair, and the PR body ends with the session-URL footer form.",
"Temporary measurement test files (reachability, bridge probe) were written in the worktree and deleted before any commit; readings stay in the scratchpad.",
"test:repo not run locally (CI-owned)."
],
"files_changed": [
".changeset/24799-vendor-client-address-trust.md",
"packages/plugins/plugin-auth/src/auth-account-event-audit.test.ts",
"packages/plugins/plugin-auth/src/auth-manager.test.ts",
"packages/plugins/plugin-auth/src/auth-manager.ts",
"packages/plugins/plugin-auth/src/client-address-vendor-trust-rule.test.ts",
"packages/plugins/plugin-auth/src/client-address-vendor.ts",
"packages/plugins/plugin-auth/src/client-address.ts"
]
}
domain:services seat 2 · session_01UmqzD97e14iws6hmVhgZ5y · 2026-10-20T12:44Z · head 9d43edafb9, all checks green or expected skips; flipped to ready with auto-merge on.
closed completed by the PR's Fixes #24799; pm:dispatched removed in this act
ACCEPT 6291133399 is on the PR. The bridges that call handleRequest without the transport are #24877 (disclosure per #9387). The library's own no-address answer, IPv6 /64 normalisation and programmatic auth.api.* calls stay Acceptance notes.
Activity
objectstack-fleet commentedon Oct 20, 2026
Claim: PM loop round 2 · 2026-10-20T10:10Z
Session:
session_01UmqzD97e14iws6hmVhgZ5yAccount:
os-support-ai(the seat's linked user asGET /useranswers it; always the card's assignee)Branch:
claude/issue-24799-vendor-client-address-trustWorktree:
objectstack-issue-24799Domain:
domain:servicesSeat:
domain:services#2(seat post #21118)File surface:
packages/plugins/plugin-auth/src/auth-manager.ts(the vendored auth instance's options: its client-address source only),packages/plugins/plugin-auth/src/client-address*(the trust rule PR #24795 established, reused, not copied), their tests, one new.changeset/24799-*.md. ⛔ Notpackages/spec/**, ⛔ notpackages/runtime/**(the trust rule's other consumers are read only), ⛔ not thesys-oauth-application/registerwrapper inauth-plugin.ts(#24773, this seat) (stop on breach; explain in the report)Container & model:
M,mode:subagent,model: opus(TIER_DEFAULT;dispatch-gates --tieron this surface: no path-derived mandate, floor sonnet, default opus, ceiling fable)Clause-②: no
Responsibility:
plugin-auth composes the vendored auth library without configuring its client-address source, so the library's own reader ignores server.trustProxy | the trust rule PR #24795 put behind @objectstack/plugin-auth/client-address (the D5 allow-list and D7 audit already follow it) | NOT MEASURED; the card's first step measures which library features read that address in a stock compositionThread-read: none
Serial constraints cleared:
auth-manager.tsis not a single-claim path. Open PR #24846 (#24750, seat 1) and PR #24843 edit other regions ofauth-manager.ts(the invite refusal; account lockout); whichever lands second mergesmain. No open PR touches the auth instance's IP options orclient-address. This seat's #24773 edits onlyauth-plugin.ts's register wrapper.Disclosure: the maintainer's ruling recorded on #9387 (
5323310017) applies. ⛔ No reproduction or exploit path on this card, its PR or any comment.Generated by Claude Code
objectstack-fleet commentedon Oct 20, 2026
os-dev-report
{
"issue": 24799,
"status": "done",
"branch": "claude/issue-24799-vendor-client-address-trust",
"pr": "#24871",
"session": "session_01UmqzD97e14iws6hmVhgZ5y - mode:subagent, the dispatching PM's harness-stamped id (claim comment 6288560527); identity = the branch",
"premise_still_valid": true,
"summary": "Premise held on origin/main 7c2eab9 and on the installed @better-auth/core 1.7.3. M1 confirmed: the composed auth options set no advanced.ipAddress, so the library used its default header reader. M3 measured through the real auth mounts: three library seams read the address in a stock composition. They are the session row ip_address (and the login/logout audit rows copied from it), the login_failed/account_locked audit address, and the key of the library's per-address rate limiter (on by default in production). In both the untrusted-proxy and the trusted-proxy deployment, what the library answered differed from the address the D5 allow-list judged. Detail withheld under the disclosure ruling. The library's option surface has no resolver hook, only a header list, a proxy-CIDR list and an off switch that also disables its limiter. M4: the peer address never reaches the library, which sees only the Fetch Request. Fix: the auth instance now always composes advanced.ipAddress.ipAddressHeaders naming one platform-owned header (client-address-vendor.ts). AuthManager.handleRequest, the one door to auth.handler, rebuilds each request from a clone: it drops any caller copy of that header and writes the address resolveClientAddress established, which is reused and not copied (M2). With no address established, nothing is written and the library is handed none. Three pins, one per branch (trusted proxy, untrusted proxy, no address source), each read all three seams plus D7 last_login_ip. Five ablations turned the predicted branches red.",
"tests": "Final head 9d43eda, which merges origin/main 9a06f0b, including #24846 and #24843 in auth-manager.ts. (1) bash scripts/pm/os-verify-lock.sh -c 'pnpm --filter @objectstack/plugin-auth exec vitest run --maxWorkers=2 && pnpm --filter @objectstack/plugin-auth typecheck' gave VERDICT command-exit 0: Test Files 171 passed (171), Tests 3273 passed | 10 skipped (3283); typecheck tsc src + examples OK, and check:test-typecheck OK (10 files / 88 errors / 23 pinned signatures held, no new debt). (2) New pin file client-address-vendor-trust-rule.test.ts, 3 tests named by the rule; the refused sign-in asserts code INVALID_EMAIL_OR_PASSWORD + status 401. (3) Ablations on 2604658, with the fix committed first, via scripts/ablation-replace.mjs. Each mutation was proven on disk (anchor 1 to 0, blob changed) and each restore was proven (blob == HEAD, git diff HEAD empty). The subject resolves through relative src imports, so no build or dist preflight applies. A1, composed ipAddress option deleted: 3 failed | 10 passed, all three branches red. A2, the caller copy no longer dropped: 1 failed | 12 passed, no-address branch red. A3, the raw transport peer handed over instead of the established address: 1 failed | 12 passed, trusted-proxy branch red. A4, the forwarded header handed over without declared trust: 2 failed | 11 passed, untrusted and no-address branches red. A5, the handleRequest write deleted: 3 failed | 10 passed, all three red. The 10 extra passing tests are the ones registered by the imported fixture module. (4) Existing suites moved with the behaviour. auth-manager.test.ts: the advanced passthrough now includes ipAddress. auth-account-event-audit.test.ts: the fixture hands its client as the transport peer instead of an untrusted forwarded header; 3 tests went red before that fixture fix and are green after it.",
"gates": "Local subset on final head 9d43eda. node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 70 commands. Each ran with the exit code captured before any pipe; all 70 exited 0. --ran printed: Run reconciliation - 70 derived, 70 run, 0 NOT-MEASURED, 0 UNRUN (a DERIVED zero). An earlier run on 48581c8 had check:dual-build-cjs-loads exit 3 (PREREQUISITE NOT MET); it was superseded by the 9d43eda run, where it exited 0. check-platform-checklist was not run: no exported symbol was renamed or deleted. test:repo was not run locally; it belongs to CI. CI on PR 24871 head 9d43eda, read once at report time: completed success are Check PR Size; The card this PR closes must claim this branch; Check Documentation Links; filter; No other open PR may claim the same single-writer path; Part-of PR must not also close its card; No other open PR may claim the same issue. in_progress are Build Core; Test Core (1-6/6); all Lint and Type Check lanes; Check Changeset; Governed Surface Queue Guard; Dogfood Regression Gate; Flag docs affected by code changes; Auto Label. Skipped by selector: the dogfood shards, Temporal Conformance, Console Pin Gate, Build Docs, Packed-tarball smoke, 18.x upgrade harness. Not waited on, per contract.",
"mcp_calls": "0 - no MCP GitHub tool was called",
"api_writes": "2 relay strokes (POST /repos/objectstack-ai/objectstack/dispatches, each executed as objectstack-fleet[bot]), carrying 3 REST writes. Stroke 1 was pr_create: POST /repos/objectstack-ai/objectstack/pulls (draft) and POST /repos//issues/24871/assignees, with read-back reporting body identical and assignee os-support-ai. Stroke 2 was this os-dev-report: POST /repos//issues/24799/comments. Plus git push (not REST). Reads were single-card REST GETs via gh api.",
"open_questions": [],
"out_of_scope_findings": [
"class: a · reach: public door, measured once through the auth mount (route, probe and result withheld under the disclosure ruling recorded on #9387) · filed #24877 by the domain:services seat 2 at review · this entry was redacted by the seat on the same review",
"carrier: none (承接者:无) · noted in PR Acceptance notes, not filed: the library's no-address answer, which in production is a one-time vendor warning naming library options the platform composes itself, and in development and test is a loopback placeholder; IPv6, where the library stores a /64-normalized session address while D7 last_login_ip stores the established address verbatim; programmatic auth.api.* calls, which hand the library their caller's headers (documented at the header constant and in the changeset)"
],
"line_budget": "not applicable: no skills/** or ratcheted line ledger touched. The diff is 7 files, +376/-21 vs merge base 9a06f0b, under the 3000-line human-merge threshold",
"deviations": [
"auth-account-event-audit.test.ts is edited outside the claim's listed surface. It drives AuthManager, and this change made its fixture false: it fed an untrusted forwarded header with no peer. 3 tests went red until the fixture handed its client as the transport peer.",
"client-address.ts carries a comment-only edit (consumers list).",
"origin/main was merged twice before the PR opened, per AGENTS.md section 10 (pull main before opening). The second merge was because #24846 and #24843 landed edits in auth-manager.ts.",
"Attribution follows AGENTS.md over the harness reminder: commits carry the model-free Claude-Session + Co-authored-by pair, and the PR body ends with the session-URL footer form.",
"Temporary measurement test files (reachability, bridge probe) were written in the worktree and deleted before any commit; readings stay in the scratchpad.",
"test:repo not run locally (CI-owned)."
],
"files_changed": [
".changeset/24799-vendor-client-address-trust.md",
"packages/plugins/plugin-auth/src/auth-account-event-audit.test.ts",
"packages/plugins/plugin-auth/src/auth-manager.test.ts",
"packages/plugins/plugin-auth/src/auth-manager.ts",
"packages/plugins/plugin-auth/src/client-address-vendor-trust-rule.test.ts",
"packages/plugins/plugin-auth/src/client-address-vendor.ts",
"packages/plugins/plugin-auth/src/client-address.ts"
]
}
Generated by Claude Code
objectstack-fleet commentedon Oct 20, 2026
ACCEPT → PR #24871 comment 6291133399
domain:servicesseat 2 ·session_01UmqzD97e14iws6hmVhgZ5y· 2026-10-20T12:44Z · head9d43edafb9, all checks green or expected skips; flipped to ready with auto-merge on.Generated by Claude Code
objectstack-fleet commentedon Oct 20, 2026
Landed ·
domain:servicesseat 2 ·os-support-ai·session_01UmqzD97e14iws6hmVhgZ5y· 2026-10-20T13:15Zadded_to_merge_queue2026-10-20T12:48Z →merged2026-10-20T13:13Zorigin/mainad8cf92f01is an ancestor (git merge-base --is-ancestor, exit 0)origin/mainauth-manager.tscarrieswithVendorClientAddress(2 hits)completedby the PR'sFixes #24799;pm:dispatchedremoved in this actACCEPT
6291133399is on the PR. The bridges that callhandleRequestwithout the transport are #24877 (disclosure per #9387). The library's own no-address answer, IPv6 /64 normalisation and programmaticauth.api.*calls stay Acceptance notes.Generated by Claude Code