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

  1. Poll /login/wait only from a first-party browser context (the same origin that initiated the popup login).
  2. If embedding in an iframe, include allow-same-origin in the sandbox attribute so a real origin is sent.
  3. 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

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.