{"record":{"id":"6b7da239a00e332f","repo":"actualbudget/actual","slug":"invalid-redirect-url","errorCode":null,"errorMessage":"Invalid redirect URL","messagePattern":"Invalid redirect URL","errorType":"http","errorClass":null,"httpStatus":400,"severity":"error","filePath":"packages/sync-server/src/app-account.js","lineNumber":100,"sourceCode":"        '*'.repeat(headerVal.length) || 'No password provided.';\n      console.debug('HEADER VALUE: ' + obfuscated);\n      if (headerVal === '') {\n        res.send({ status: 'error', reason: 'invalid-header' });\n        return;\n      } else {\n        if (validateAuthHeader(req)) {\n          tokenRes = await loginWithPassword(headerVal);\n        } else {\n          res.send({ status: 'error', reason: 'proxy-not-trusted' });\n          return;\n        }\n      }\n      break;\n    }\n    case 'openid': {\n      if (!isValidRedirectUrl(req.body.returnUrl)) {\n        res\n          .status(400)\n          .send({ status: 'error', reason: 'Invalid redirect URL' });\n        return;\n      }\n\n      const { error, url } = await loginWithOpenIdSetup(\n        req.body.returnUrl,\n        req.body.password,\n      );\n      if (error) {\n        res.status(400).send({ status: 'error', reason: error });\n        return;\n      }\n      res.send({ status: 'ok', data: { returnUrl: url } });\n      return;\n    }\n\n    default:\n      tokenRes = await loginWithPassword(req.body.password);","sourceCodeStart":82,"sourceCodeEnd":118,"githubUrl":"https://github.com/actualbudget/actual/blob/d4334cb6e6123f4d3bcea1ad6166608884c7e658/packages/sync-server/src/app-account.js#L82-L118","documentation":"The /login (openid) path of app-account.js rejects the request with 400 'Invalid redirect URL' when req.body.returnUrl fails isValidRedirectUrl(). The server only accepts return URLs pointing back to trusted origins (typically the server's own canonical URL), preventing open-redirect attacks during the OpenID login flow.","triggerScenarios":"POST to the login endpoint with loginMethod 'openid' and a returnUrl that is absent, not absolute, uses an unexpected protocol, or points to a host other than the server's configured URL.","commonSituations":"Reverse-proxy on a different domain than the configured server URL; missing/wrong serverURL config so the allowlist does not match; client sending a relative path like '/gocardless' without origin; http vs https mismatch behind a TLS proxy.","solutions":["Send returnUrl as an absolute URL matching the server's configured canonical origin","Fix serverURL / trust-proxy configuration so the allowlist includes the host the browser is actually on","Ensure the reverse proxy forwards correct X-Forwarded-Proto/Host headers so the server sees https","Align http/https between the configured server URL and the returnUrl"],"exampleFix":"// before\nawait request('/login', { method: 'POST', body: { loginMethod: 'openid', returnUrl: '/app' } });\n// 400 Invalid redirect URL\n// after\nconst returnUrl = new URL('/app', location.origin).href; // absolute, same origin\nawait request('/login', { method: 'POST', body: { loginMethod: 'openid', returnUrl } });","handlingStrategy":"validation","validationCode":"function isValidClientReturnUrl(url) {\n  try {\n    const u = new URL(url, location.origin);\n    return u.origin === location.origin && (u.protocol === 'https:' || u.hostname === 'localhost');\n  } catch { return false; }\n}\n// assert before POST /login: isValidClientReturnUrl(returnUrl)","typeGuard":"function isInvalidRedirectResponse(body) {\n  return body?.status === 'error' && body?.reason === 'Invalid redirect URL';\n}","tryCatchPattern":"const res = await request('/login', { method: 'POST', body: { loginMethod: 'openid', returnUrl } });\nif (res.status === 400 && res.reason === 'Invalid redirect URL') {\n  returnUrl = new URL(returnUrl, serverUrl).href; // rebuild absolute, retry\n}","preventionTips":["Always send absolute return URLs on the server's canonical origin","Keep serverURL and reverse-proxy host/proto headers consistent","Avoid relative paths; build URLs with new URL()"],"tags":["sync-server","openid","open-redirect","validation"],"backgroundTag":"invalid-redirect-url","analyzedSha":"d4334cb6e6123f4d3bcea1ad6166608884c7e658","analyzedAt":"2026-08-29T01:02:11.213Z","schemaVersion":2},"datasetVersion":"2026-08-29T02:17:18.158Z"}