toeverything/AFFiNE · warning · ActionForbidden
action_forbidden
action_forbidden
Error message
You are not allowed to perform this action.
What it means
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.
Source
Thrown at packages/backend/server/src/base/helpers/url.ts:138
}
url(path: string, query: Record<string, any> = {}) {
const url = new URL(path, this.requestOrigin);
for (const key in query) {
url.searchParams.set(key, query[key]);
}
return url;
}
link(path: string, query: Record<string, any> = {}) {
return this.url(path, query).toString();
}
safeLink(path: string, query: Record<string, any> = {}) {
if (!this.isAllowedCallbackUrl(path)) {
throw new ActionForbidden();
}
return this.link(path, query);
}
safeRedirect(res: Response, to: string) {
try {
const finalTo = new URL(decodeURIComponent(to), this.requestBaseUrl);
for (const host of this.redirectAllowHosts) {
const hostURL = new URL(host);
if (
hostURL.origin === finalTo.origin &&
finalTo.pathname.startsWith(hostURL.pathname)
) {
return res.redirect(finalTo.toString().replace(/\/$/, ''));
}
}
} catch {View on GitHub (pinned to 26c515e050)
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.
Example fix
// before
urlHelper.safeLink('https://evil.com/callback'); // throws ActionForbidden
// after (relative path)
urlHelper.safeLink('/callback'); // allowed
// or add the origin to server config
// server.hosts: ['your-trusted-domain.com'] Defensive patterns
Strategy: type-guard
Validate before calling
const callbackUrl = req.query.callback as string;
if (!urlHelper.isAllowedCallbackUrl(callbackUrl)) {
throw new Error('Callback URL is not allowed.');
}
const link = urlHelper.safeLink(callbackUrl); Type guard
urlHelper.isAllowedCallbackUrl(url: string): boolean // built-in method on URLHelper
Try / catch
try {
const link = urlHelper.safeLink(path, query);
} catch (e) {
if (e instanceof ActionForbidden) {
// redirect to a safe default or inform user
res.redirect(urlHelper.baseUrl);
}
} Prevention
- 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().
When it happens
Trigger: 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.
Common situations: 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).
Related errors
- action_forbidden
- Invalid `server.externalUrl` configured. It must be a valid
- Invalid rowId
- bad_request
- Invalid config for module [${module}] with key [${key}] Valu
AI-assisted analysis of toeverything/AFFiNE@26c515e050 (2026-08-12).
Data as JSON: /api/errors/4142df5e8463ef20.
Report an issue: GitHub.