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
The auth plugin's signout handler takes sessionId from the request body, resolves the
backend, and calls revokeSession(sessionId) with no authentication and no ownership check. Any
caller who learns or guesses a session id can terminate that session; the auth service accepts the
request because createPluginService cannot guard it at all (#1383). NetScript's own CLI exercises
this path with a raw fetch carrying no credential, which is how the hole stayed invisible.
Evidence
Corpus: research/repo-audit/auth.md §3.1, gap G6.
plugins/auth/services/src/routers/v1-handlers.ts:189-234 — export async function signout(input: SignoutInput, context: AuthServiceContext); const sessionId = input.sessionId ?? await backend.interactive?.getSessionId(...), then await backend.sessions.revokeSession(sessionId).
There is no principal check anywhere in the function.
packages/plugin-auth-core/src/contracts/v1/auth.contract.ts:56-61 — SignoutInput = Readonly<{ sessionId?: string; everywhere?: boolean; redirectTo?: string }>; the contract itself invites a
caller-supplied session id.
packages/cli/src/public/features/plugins/auth/auth-session-client.ts:7-23 — first-party caller
using raw fetch with no credential (auth.md §0).
The everywhere flag in the same input widens the blast radius: a global logout is reachable on
the same unauthenticated route.
Current surface
signout has two modes. With input.sessionId present it revokes that id directly. With it absent
it falls back to backend.interactive?.getSessionId(request), i.e. the cookie — the only
identity-bearing path, and it is the optional one. The handler emits a session.revoked audit
event and telemetry for the revocation it just performed, so the audit trail records a successful
revocation with no authenticated actor.
Session ownership is enforced in the handler, not only at the middleware: the resolved session's
subject must equal the principal's subject, otherwise the call fails with the contract's UNAUTHORIZED error and no revocation and no success audit record.
everywhere: true revokes only sessions belonging to the authenticated subject.
Caller-supplied sessionId is either removed from SignoutInput (preferred — the session comes
from the credential) or retained solely as a same-subject selector for multi-session sign-out.
Whichever is chosen, a foreign id can never be revoked. Removing the field is a contract-breaking
change and must be released as such.
The failure path is indistinguishable to the caller between "session does not exist" and "session
belongs to someone else" (no enumeration oracle).
Acceptance
signout rejects requests that carry no authenticated principal.
signout rejects a sessionId whose subject differs from the principal's subject.
everywhere: true revokes only sessions owned by the authenticated subject.
A rejected signout emits no session.revoked audit event and no success telemetry.
Negative test: unauthenticated POST /api/v1/auth/signout with a valid foreign sessionId returns 401 and the session stays active.
Negative test: authenticated POST /api/v1/auth/signout with a foreign sessionId returns 401/403 and the session stays active.
Negative test: POST /api/rpc/v1/auth/signout is guarded identically to the REST route.
Negative test: unknown and foreign session ids produce the same response shape and status.
The CLI's session commands send a credential and no longer rely on the unauthenticated path.
Any SignoutInput contract change is recorded as breaking with a migration note.
docs/site/identity-access/how-to/add-authentication.md:243 advertises POST /api/v1/auth/signout
as "Revoke the current session and clear the session cookie" — "the current session" is exactly the
guarantee the code does not provide. Adoption is proven when that row is accurate, when the docs show
the authenticated call shape, and when docs/site/identity-access/auth.md documents the ownership
rule and the deliberate non-enumeration of foreign session ids.
Provenance
Seed run plan-fable5-remediation-roadmap--seed, PR #1347, 2026-08-08. Sourced from research/repo-audit/auth.md gap G6; all cited line numbers re-verified against worktree fac9e339042c on 2026-08-08. Split from the TA-03 cluster so each session-lifecycle defect carries
its own acceptance and negative tests.
Filed from planning seed PR #1347 · source Draft-ID TA-03a · live issue #1384.
Summary
The auth plugin's
signouthandler takessessionIdfrom the request body, resolves thebackend, and calls
revokeSession(sessionId)with no authentication and no ownership check. Anycaller who learns or guesses a session id can terminate that session; the auth service accepts the
request because
createPluginServicecannot guard it at all (#1383). NetScript's own CLI exercisesthis path with a raw
fetchcarrying no credential, which is how the hole stayed invisible.Evidence
research/repo-audit/auth.md§3.1, gap G6.plugins/auth/services/src/routers/v1-handlers.ts:189-234—export async function signout(input: SignoutInput, context: AuthServiceContext);const sessionId = input.sessionId ?? await backend.interactive?.getSessionId(...), thenawait backend.sessions.revokeSession(sessionId).There is no principal check anywhere in the function.
packages/plugin-auth-core/src/contracts/v1/auth.contract.ts:56-61—SignoutInput = Readonly<{ sessionId?: string; everywhere?: boolean; redirectTo?: string }>; the contract itself invites acaller-supplied session id.
plugins/auth/services/src/main.ts:70-84— the service is built bycreatePluginServicewith noauth configuration, so no middleware ever populates a principal (fix(plugin):
createPluginServicehas no auth seam, so every first-party plugin API is unguardable by construction #1383 evidence).packages/cli/src/public/features/plugins/auth/auth-session-client.ts:7-23— first-party callerusing raw
fetchwith no credential (auth.md§0).everywhereflag in the same input widens the blast radius: a global logout is reachable onthe same unauthenticated route.
Current surface
signouthas two modes. Withinput.sessionIdpresent it revokes that id directly. With it absentit falls back to
backend.interactive?.getSessionId(request), i.e. the cookie — the onlyidentity-bearing path, and it is the optional one. The handler emits a
session.revokedauditevent and telemetry for the revocation it just performed, so the audit trail records a successful
revocation with no authenticated actor.
Target contract
signoutrequires an authenticated principal. With fix(plugin):createPluginServicehas no auth seam, so every first-party plugin API is unguardable by construction #1383's seam applied toplugins/auth/services/src/main.ts,/api/v1/auth/signoutand/api/rpc/v1/auth/signoutareinside
DEFAULT_PROTECTED_PREFIXES.subject must equal the principal's subject, otherwise the call fails with the contract's
UNAUTHORIZEDerror and no revocation and no success audit record.everywhere: truerevokes only sessions belonging to the authenticated subject.sessionIdis either removed fromSignoutInput(preferred — the session comesfrom the credential) or retained solely as a same-subject selector for multi-session sign-out.
Whichever is chosen, a foreign id can never be revoked. Removing the field is a contract-breaking
change and must be released as such.
belongs to someone else" (no enumeration oracle).
Acceptance
signoutrejects requests that carry no authenticated principal.signoutrejects asessionIdwhose subject differs from the principal's subject.everywhere: truerevokes only sessions owned by the authenticated subject.session.revokedaudit event and no success telemetry.POST /api/v1/auth/signoutwith a valid foreignsessionIdreturns 401 and the session stays active.POST /api/v1/auth/signoutwith a foreignsessionIdreturns 401/403 and the session stays active.POST /api/rpc/v1/auth/signoutis guarded identically to the REST route.SignoutInputcontract change is recorded as breaking with a migration note.Boundaries
PluginServiceConfig.authseam here — fix(plugin):createPluginServicehas no auth seam, so every first-party plugin API is unguardable by construction #1383 owns it; this issue consumes itand is blocked until it lands.
Set-Cookiediscard on signin/callback — fix(auth): oRPCsignin/callbackdiscard the backendSet-Cookie, so the interactive browser flow can never establish a session #1385 owns it.origin: '*', which makes credentialed browser calls impossible on every generated and plugin service #1386 owns it.localhost:4437session-stream URL — auth: session list --stream-url default pins localhost:4437 which no longer exists post-#1211 #1243 owns it;this issue only changes whether the CLI sends a credential.
Docs/consumer proof
docs/site/identity-access/how-to/add-authentication.md:243advertisesPOST /api/v1/auth/signoutas "Revoke the current session and clear the session cookie" — "the current session" is exactly the
guarantee the code does not provide. Adoption is proven when that row is accurate, when the docs show
the authenticated call shape, and when
docs/site/identity-access/auth.mddocuments the ownershiprule and the deliberate non-enumeration of foreign session ids.
Provenance
Seed run
plan-fable5-remediation-roadmap--seed, PR #1347, 2026-08-08. Sourced fromresearch/repo-audit/auth.mdgap G6; all cited line numbers re-verified against worktreefac9e339042con 2026-08-08. Split from the TA-03 cluster so each session-lifecycle defect carriesits own acceptance and negative tests.
Filed from planning seed PR #1347 · source Draft-ID TA-03a · live issue #1384.