Skip to content

AuthManager trusts localhost wildcards for CSRF in production whenever trustedOrigins arrives empty — measured #10366

Description

@os-elon

Found while measuring #10202 (empty OS_AUTH_URL); not that card's defect#10202 is the swallowed diagnostic in serve.ts, and its fix does not change this. Filing unassigned so it gets triaged on its own merits.

Claim

packages/plugins/plugin-auth/src/auth-manager.ts (the trustedOrigins block, ~line 1814) substitutes a localhost wildcard trio whenever the resolved origin list is empty and OS_CORS_ORIGIN is unset or *:

if (!origins.length && (!corsOrigin || corsOrigin === '*')) {
  origins.push('http://localhost:*');
  origins.push('http://*.localhost:*');
  origins.push('https://*.localhost:*');
}

The comment calls this a development convenience — "trust all localhost ports in development for convenience" — but the condition does not test NODE_ENV, or dev mode, or anything else. Any deployment that reaches it with an empty list gets those three wildcards, production included. Two existing tests in auth-manager.test.ts already pin the substitution (should default to localhost wildcard when trustedOrigins not provided / ... when trustedOrigins array is empty), neither of which varies NODE_ENV.

Measured

Real os serve boot of examples/app-todo, NODE_ENV=production, OS_AUTH_SECRET + OS_SECRET_KEY set, OS_TRUSTED_ORIGINS / OS_ROOT_DOMAIN / OS_BASE_URL / OS_CORS_ORIGIN / preview mode all unset. Probe is POST /api/v1/auth/sign-in/email with wrong credentials, so a trusted origin answers 401 INVALID_EMAIL_OR_PASSWORD and an untrusted one 403 INVALID_ORIGIN.

With OS_AUTH_URL= set-but-empty (which empties the list — the #10202 mechanism):

Origin Status
http://localhost:PORT 401 — trusted
http://tenant.localhost:PORT 401 — trusted
https://app.example.com 403
https://evil.example.net 403

With OS_AUTH_URL unset (list contains the parsed default origin, so the substitution does not fire):

Origin Status
http://localhost:PORT 401 — trusted (this one is the real base origin)
http://tenant.localhost:PORT 403

So the wildcards are demonstrably granted in a production process, and only in the empty-list case.

Why it may be worth a card

  1. A dev convenience is applied in production. http://*.localhost:* and https://*.localhost:* are CSRF-trusted on a production server. Whether that is exploitable depends on whether an attacker can get a browser to send an Origin under .localhost — not obviously reachable, which is exactly why it wants a real security judgement rather than my guess.
  2. The narrower fact is the surprising one: set-but-empty is more permissive than unset. The two configurations differ in trust surface, and nothing announces the difference.

Not claimed

Not assessed for exploitability. I measured what is trusted, not whether the trust can be turned into an attack. .localhost resolution, browser Origin behaviour for it, and whether any deployment shape lets an attacker choose that origin are all unexamined.

Not surveyed: whether other callers construct AuthManager with an empty list on purpose and rely on this substitution. The cloud distribution's per-project ArtifactKernelFactory is the obvious one to check before changing anything.

Possible directions (triage, not a recommendation)

  • Gate the substitution on dev (NODE_ENV !== 'production'), matching what its own comment already says it is for.
  • Or keep it and make it loud in production, so an operator sees which origins were auto-trusted.
  • Or leave it and treat this as documented behaviour — in which case the production case deserves a line in the deployment docs.

Which is right depends on the exploitability question above, which I did not answer.

Metadata

Metadata

Assignees

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions