HeyPuter/puter · error · HttpError
forbidden
forbidden
Error message
Origin not allowed
What it means
Returned by POST /login/wait when the Origin header is absent or the literal string 'null'. Because the session id is not a secret, the server uses the browser-attested Origin to bind which app may collect the resulting token; a missing or opaque origin could let any caller claim a token minted for another app. Sandboxed iframes and file:// documents serialize their origin as 'null' and are rejected for the same reason.
Source
Thrown at src/backend/controllers/auth/AuthController.ts:245
if (!session || !validateUuid(session)) {
throw new HttpError(400, 'session is required.', {
legacyCode: 'bad_request',
});
}
// Browser-only gate. The session id is client-chosen and travels in a
// link, so it is not a secret — the `Origin` header is what actually
// says who is asking, and only a browser is prevented from lying about
// it. A caller with no `Origin` (curl, a server-side fetch) could
// otherwise collect a token minted for someone else's app just by
// knowing the id.
//
// `"null"` is rejected too: sandboxed iframes and `file://` documents
// serialise their opaque origin that way, and two *unrelated* opaque
// origins would compare equal to each other.
const reqOrigin = req.headers.origin;
if (!reqOrigin || reqOrigin === 'null') {
throw new HttpError(403, 'Origin not allowed', {
legacyCode: 'forbidden',
});
}
// The app identity this caller is allowed to collect a token for,
// derived from the browser-attested header rather than anything in
// the request body — so no client, honest or not, can influence the
// comparison made after the token arrives.
const expectedAppUid =
await this.services.auth.appUidFromOrigin(reqOrigin);
const { resolve, promise } = Promise.withResolvers<void>();
let token: string | null = null;
const listener = (_key: string, value: { authtoken: string }) => {
token = value.authtoken;
resolve();
};View on GitHub (pinned to 908ec23eda)
Solutions
- Poll /login/wait only from a first-party browser context (the same origin that initiated the popup login).
- If embedding in an iframe, include allow-same-origin in the sandbox attribute so a real origin is sent.
- Serve the page over http(s):// rather than file:// so it has a non-opaque origin.
Example fix
// before: server-side fetch, no Origin
await fetch('https://api.puter.com/login/wait', { method:'POST', body:JSON.stringify({session}) });
// after: poll from the browser page that started the login
// (browser sets Origin automatically; do not call from node/curl) Defensive patterns
Strategy: validation
Validate before calling
// Only poll from a first-party browser context
if (!window || !location.origin || location.origin === 'null') {
throw new Error('/login/wait must be called from a browser page with a real origin');
} Prevention
- Poll /login/wait from the same browser page that opened the login popup.
- Avoid sandboxed iframes without allow-same-origin and file:// pages.
- Do not call this endpoint from server-side code or curl.
When it happens
Trigger: Calling /login/wait from curl or server-side fetch (no Origin); from a sandboxed iframe without allow-same-origin; from a file:// page; or with a stripped Origin header.
Common situations: Server-side polling instead of browser polling; iframe embedding with restrictive sandbox flags; local development opening the popup via file://; a proxy that drops the Origin header.
Related errors
AI-assisted analysis of HeyPuter/puter@908ec23eda (2026-08-12).
Data as JSON: /api/errors/4e00f66c39be401f.
Report an issue: GitHub.