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

  1. Ensure the callback path is a relative path starting with '/' (e.g. '/magic-link') — relative same-app paths are always allowed.
  2. If an absolute URL is needed, add the origin to the server's allowedOrigins by configuring server.hosts or server.externalUrl.
  3. Avoid embedding credentials (user:pass@) in callback URLs.
  4. 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

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


AI-assisted analysis of toeverything/AFFiNE@26c515e050 (2026-08-12). Data as JSON: /api/errors/4142df5e8463ef20. Report an issue: GitHub.