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_URLunset (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
- 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. - 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.
Found while measuring #10202 (empty
OS_AUTH_URL); not that card's defect — #10202 is the swallowed diagnostic inserve.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(thetrustedOriginsblock, ~line 1814) substitutes a localhost wildcard trio whenever the resolved origin list is empty andOS_CORS_ORIGINis unset or*: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 inauth-manager.test.tsalready pin the substitution (should default to localhost wildcard when trustedOrigins not provided/... when trustedOrigins array is empty), neither of which variesNODE_ENV.Measured
Real
os serveboot ofexamples/app-todo,NODE_ENV=production,OS_AUTH_SECRET+OS_SECRET_KEYset,OS_TRUSTED_ORIGINS/OS_ROOT_DOMAIN/OS_BASE_URL/OS_CORS_ORIGIN/ preview mode all unset. Probe isPOST /api/v1/auth/sign-in/emailwith wrong credentials, so a trusted origin answers401 INVALID_EMAIL_OR_PASSWORDand an untrusted one403 INVALID_ORIGIN.With
OS_AUTH_URL=set-but-empty (which empties the list — the #10202 mechanism):http://localhost:PORThttp://tenant.localhost:PORThttps://app.example.comhttps://evil.example.netWith
OS_AUTH_URLunset (list contains the parsed default origin, so the substitution does not fire):http://localhost:PORThttp://tenant.localhost:PORTSo the wildcards are demonstrably granted in a production process, and only in the empty-list case.
Why it may be worth a card
http://*.localhost:*andhttps://*.localhost:*are CSRF-trusted on a production server. Whether that is exploitable depends on whether an attacker can get a browser to send anOriginunder.localhost— not obviously reachable, which is exactly why it wants a real security judgement rather than my guess.Not claimed
⛔ Not assessed for exploitability. I measured what is trusted, not whether the trust can be turned into an attack.
.localhostresolution, browserOriginbehaviour for it, and whether any deployment shape lets an attacker choose that origin are all unexamined.⛔ Not surveyed: whether other callers construct
AuthManagerwith an empty list on purpose and rely on this substitution. The cloud distribution's per-projectArtifactKernelFactoryis the obvious one to check before changing anything.Possible directions (triage, not a recommendation)
NODE_ENV !== 'production'), matching what its own comment already says it is for.productioncase deserves a line in the deployment docs.Which is right depends on the exploitability question above, which I did not answer.