{"record":{"id":"4142df5e8463ef20","repo":"toeverything/AFFiNE","slug":"action-forbidden-4142df","errorCode":"action_forbidden","errorMessage":"You are not allowed to perform this action.","messagePattern":"You are not allowed to perform this action\\.","errorType":"http","errorClass":"ActionForbidden","httpStatus":403,"severity":"warning","filePath":"packages/backend/server/src/base/helpers/url.ts","lineNumber":138,"sourceCode":"  }\n\n  url(path: string, query: Record<string, any> = {}) {\n    const url = new URL(path, this.requestOrigin);\n\n    for (const key in query) {\n      url.searchParams.set(key, query[key]);\n    }\n\n    return url;\n  }\n\n  link(path: string, query: Record<string, any> = {}) {\n    return this.url(path, query).toString();\n  }\n\n  safeLink(path: string, query: Record<string, any> = {}) {\n    if (!this.isAllowedCallbackUrl(path)) {\n      throw new ActionForbidden();\n    }\n    return this.link(path, query);\n  }\n\n  safeRedirect(res: Response, to: string) {\n    try {\n      const finalTo = new URL(decodeURIComponent(to), this.requestBaseUrl);\n\n      for (const host of this.redirectAllowHosts) {\n        const hostURL = new URL(host);\n        if (\n          hostURL.origin === finalTo.origin &&\n          finalTo.pathname.startsWith(hostURL.pathname)\n        ) {\n          return res.redirect(finalTo.toString().replace(/\\/$/, ''));\n        }\n      }\n    } catch {","sourceCodeStart":120,"sourceCodeEnd":156,"githubUrl":"https://github.com/toeverything/AFFiNE/blob/26c515e050211269e911f7d9cfe162a26c83ed98/packages/backend/server/src/base/helpers/url.ts#L120-L156","documentation":"An ActionForbidden error (HTTP 403) thrown by URLHelper.safeLink() when the provided path fails the isAllowedCallbackUrl() security check. safeLink is used to generate callback URLs for OAuth, magic links, and redirects — it rejects any URL that isn't a same-app relative path or an allowed origin to prevent open-redirect attacks. The check validates protocol (http/https only), disallows credentials in the URL, and requires the origin to match allowedOrigins.","triggerScenarios":"Calling urlHelper.safeLink(path, query) where path is an external URL whose origin is not in the server's allowedOrigins list, or a path with protocol other than http/https, or a URL containing username/password. Also thrown for empty paths or malformed URLs.","commonSituations":"OAuth callback URLs pointing to a different domain than the configured server origin/hosts. Magic link redirects to unregistered frontend domains. Client sending a callback URL from a different deployment environment (staging vs production). URL with credentials embedded (user:pass@host).","solutions":["Ensure the callback path is a relative path starting with '/' (e.g. '/magic-link') — relative same-app paths are always allowed.","If an absolute URL is needed, add the origin to the server's allowedOrigins by configuring server.hosts or server.externalUrl.","Avoid embedding credentials (user:pass@) in callback URLs.","Use urlHelper.isAllowedCallbackUrl(path) to check before calling safeLink."],"exampleFix":"// before\nurlHelper.safeLink('https://evil.com/callback'); // throws ActionForbidden\n\n// after (relative path)\nurlHelper.safeLink('/callback'); // allowed\n// or add the origin to server config\n// server.hosts: ['your-trusted-domain.com']","handlingStrategy":"type-guard","validationCode":"const callbackUrl = req.query.callback as string;\nif (!urlHelper.isAllowedCallbackUrl(callbackUrl)) {\n  throw new Error('Callback URL is not allowed.');\n}\nconst link = urlHelper.safeLink(callbackUrl);","typeGuard":"urlHelper.isAllowedCallbackUrl(url: string): boolean  // built-in method on URLHelper","tryCatchPattern":"try {\n  const link = urlHelper.safeLink(path, query);\n} catch (e) {\n  if (e instanceof ActionForbidden) {\n    // redirect to a safe default or inform user\n    res.redirect(urlHelper.baseUrl);\n  }\n}","preventionTips":["Use isAllowedCallbackUrl() to check before calling safeLink().","Prefer relative paths ('/callback') for internal redirects.","Register all legitimate frontend origins in server.hosts or server.externalUrl.","Never pass unvalidated user input directly to safeLink()."],"tags":["affine-backend","security","url","redirect","oauth","forbidden","open-redirect"],"backgroundTag":null,"analyzedSha":"26c515e050211269e911f7d9cfe162a26c83ed98","analyzedAt":"2026-08-12T13:15:16.447Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}