{"record":{"id":"9de508a934db1e91","repo":"actualbudget/actual","slug":"invalid-redirect-url-9de508","errorCode":"Invalid redirect URL","errorMessage":"Invalid redirect URL","messagePattern":"Invalid redirect URL","errorType":"http","errorClass":null,"httpStatus":400,"severity":"error","filePath":"packages/sync-server/src/app-openid.ts","lineNumber":108,"sourceCode":"    const openIdConfig = JSON.parse(auth.extra_data);\n    res.send({ status: 'ok', data: { openId: openIdConfig } });\n  } catch {\n    res\n      .status(500)\n      .send({ status: 'error', reason: 'Invalid OpenID configuration' });\n  }\n});\n\napp.get('/callback', async (req, res) => {\n  const { error, url } = await loginWithOpenIdFinalize(req.query);\n\n  if (error) {\n    res.status(400).send({ status: 'error', reason: error });\n    return;\n  }\n\n  if (!isValidRedirectUrl(url)) {\n    res.status(400).send({ status: 'error', reason: 'Invalid redirect URL' });\n    return;\n  }\n\n  res.redirect(url);\n});\n\napp.use(errorMiddleware);\n","sourceCodeStart":90,"sourceCodeEnd":116,"githubUrl":"https://github.com/actualbudget/actual/blob/d4334cb6e6123f4d3bcea1ad6166608884c7e658/packages/sync-server/src/app-openid.ts#L90-L116","documentation":"After a successful OpenID finalize, the server validates the redirect target with isValidRedirectUrl before issuing res.redirect. If the resulting URL is not a valid/allowed redirect (typically not relative to the server's own origin or absent), it responds 400 with reason 'Invalid redirect URL'. This guards against open-redirect attacks via crafted state or configuration.","triggerScenarios":"GET /openid/callback where the finalized `url` is undefined/null, an absolute URL to a foreign origin, or fails isValidRedirectUrl (e.g. missing the server's configured webhook/base origin).","commonSituations":"Server accessed via a different host/port/protocol than the configured canonical URL (reverse proxy without forwarded headers); state tampering; missing or wrong actual server URL configuration so the computed redirect points elsewhere.","solutions":["Ensure the server's canonical URL/base origin configuration matches the URL users actually browse to","Configure the reverse proxy to pass X-Forwarded-Host/X-Forwarded-Proto so the computed redirect matches the requested origin","Start the login flow again from the app UI instead of a bookmarked/hand-built callback URL","Inspect loginWithOpenIdFinalize/isValidRedirectUrl in app-openid.ts to confirm which origin is considered valid"],"exampleFix":"// before: server behind proxy computes http://internal:5006/redirect\n// after: pass forwarded headers\nproxy_set_header X-Forwarded-Host $host;\nproxy_set_header X-Forwarded-Proto $scheme;","handlingStrategy":"validation","validationCode":"function willRedirectValidate(url, serverOrigin) {\n  try {\n    const u = new URL(url, serverOrigin);\n    return u.origin === new URL(serverOrigin).origin;\n  } catch { return false; }\n}\n// ensure the browser origin matches the server's canonical URL before logging in","typeGuard":"function isSameOriginRedirect(url, origin) {\n  try { return new URL(url, origin).origin === new URL(origin).origin; }\n  catch { return false; }\n}","tryCatchPattern":"if (res.status === 400) {\n  const body = await res.json();\n  if (body.reason === 'Invalid redirect URL') {\n    // recompute origin: open the app via the configured canonical URL and retry\n    window.location.href = canonicalServerUrl + '/openid/login';\n  }\n}","preventionTips":["Access the server via its configured canonical URL only","Forward X-Forwarded-Host/X-Forwarded-Proto on reverse proxies","Never bookmark or hand-construct /openid/callback URLs","Keep serverUrl/origin settings in sync with the public hostname"],"tags":["openid","redirect","http-400","open-redirect"],"backgroundTag":"invalid-redirect-url","analyzedSha":"d4334cb6e6123f4d3bcea1ad6166608884c7e658","analyzedAt":"2026-08-29T01:02:11.213Z","schemaVersion":2},"datasetVersion":"2026-08-29T02:17:18.158Z"}