{"record":{"id":"4e00f66c39be401f","repo":"HeyPuter/puter","slug":"forbidden-4e00f6","errorCode":"forbidden","errorMessage":"Origin not allowed","messagePattern":"Origin not allowed","errorType":"http","errorClass":"HttpError","httpStatus":403,"severity":"error","filePath":"src/backend/controllers/auth/AuthController.ts","lineNumber":245,"sourceCode":"        if (!session || !validateUuid(session)) {\n            throw new HttpError(400, 'session is required.', {\n                legacyCode: 'bad_request',\n            });\n        }\n\n        // Browser-only gate. The session id is client-chosen and travels in a\n        // link, so it is not a secret — the `Origin` header is what actually\n        // says who is asking, and only a browser is prevented from lying about\n        // it. A caller with no `Origin` (curl, a server-side fetch) could\n        // otherwise collect a token minted for someone else's app just by\n        // knowing the id.\n        //\n        // `\"null\"` is rejected too: sandboxed iframes and `file://` documents\n        // serialise their opaque origin that way, and two *unrelated* opaque\n        // origins would compare equal to each other.\n        const reqOrigin = req.headers.origin;\n        if (!reqOrigin || reqOrigin === 'null') {\n            throw new HttpError(403, 'Origin not allowed', {\n                legacyCode: 'forbidden',\n            });\n        }\n\n        // The app identity this caller is allowed to collect a token for,\n        // derived from the browser-attested header rather than anything in\n        // the request body — so no client, honest or not, can influence the\n        // comparison made after the token arrives.\n        const expectedAppUid =\n            await this.services.auth.appUidFromOrigin(reqOrigin);\n\n        const { resolve, promise } = Promise.withResolvers<void>();\n\n        let token: string | null = null;\n        const listener = (_key: string, value: { authtoken: string }) => {\n            token = value.authtoken;\n            resolve();\n        };","sourceCodeStart":227,"sourceCodeEnd":263,"githubUrl":"https://github.com/HeyPuter/puter/blob/908ec23eda38526170322c3edf71ba45ecb1ca95/src/backend/controllers/auth/AuthController.ts#L227-L263","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before: server-side fetch, no Origin\nawait fetch('https://api.puter.com/login/wait', { method:'POST', body:JSON.stringify({session}) });\n\n// after: poll from the browser page that started the login\n// (browser sets Origin automatically; do not call from node/curl)","handlingStrategy":"validation","validationCode":"// Only poll from a first-party browser context\nif (!window || !location.origin || location.origin === 'null') {\n  throw new Error('/login/wait must be called from a browser page with a real origin');\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["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."],"tags":["auth","login","security","origin","forbidden","browser"],"backgroundTag":null,"analyzedSha":"908ec23eda38526170322c3edf71ba45ecb1ca95","analyzedAt":"2026-08-12T20:53:15.911Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}