{"record":{"id":"6365c19fdd4d7cf4","repo":"Significant-Gravitas/AutoGPT","slug":"url-must-point-at-a-trusted-frontend-origin","errorCode":null,"errorMessage":"url must point at a trusted frontend origin.","messagePattern":"url must point at a trusted frontend origin\\.","errorType":"http","errorClass":"HTTPException","httpStatus":400,"severity":"error","filePath":"autogpt_platform/backend/backend/api/features/auth_email/routes.py","lineNumber":74,"sourceCode":"@auth_email_router.post(\n    \"/send\",\n    status_code=status.HTTP_204_NO_CONTENT,\n    dependencies=[Security(requires_auth_email_service)],\n    summary=\"Send a Better Auth transactional email via the backend mailer\",\n    # Without an explicit id the generated client name is derived from the\n    # summary, which produces an unreadable mouthful.\n    operation_id=\"sendAuthTransactionalEmail\",\n    responses={\n        400: {\"description\": \"url does not point at a trusted frontend origin\"},\n        403: {\"description\": \"Service token is missing the required scope\"},\n        503: {\"description\": \"Service-token verification is not configured\"},\n    },\n    # The tag comes from the router include in rest_api.py; repeating it here\n    # duplicates it in the generated spec.\n)\nasync def send_auth_email(request: AuthEmailRequest) -> None:\n    if not _url_origin_allowed(request.url):\n        raise HTTPException(\n            status_code=status.HTTP_400_BAD_REQUEST,\n            detail=\"url must point at a trusted frontend origin.\",\n        )\n\n    subject = _SUBJECTS[request.type]\n    action = _ACTIONS[request.type]\n    # Escape the (host-validated) URL before embedding it in HTML — a path or\n    # query on an allowed host could still carry markup-breaking characters.\n    safe_url = html.escape(request.url, quote=True)\n    body = (\n        f\"<p>Click the link below to {action} for the AutoGPT Platform:</p>\"\n        f'<p><a href=\"{safe_url}\">{safe_url}</a></p>'\n        \"<p>If you didn't request this, you can safely ignore this email.</p>\"\n    )\n\n    # The blocking RPC to the notification service runs off the event loop; a\n    # delivery failure there surfaces as a 5xx so a misconfigured mailer fails\n    # loudly instead of dropping the auth email.","sourceCodeStart":56,"sourceCodeEnd":92,"githubUrl":"https://github.com/Significant-Gravitas/AutoGPT/blob/9c8bb5550f446ba5d3046b78896578742495b3cf/autogpt_platform/backend/backend/api/features/auth_email/routes.py#L56-L92","documentation":"A 400 from the service-token-authenticated transactional email endpoint: the url in the request body does not pass _url_origin_allowed(), i.e. its origin (scheme+host+port) is not in the configured set of trusted frontend origins. The check exists because the url is embedded in the email body as a link, and an arbitrary origin would let callers phish users with the platform's own mail sender.","triggerScenarios":"POST to the auth email route with a url whose host is not in APP_BACKEND_CORS_ORIGINS / the trusted-frontend-origins config (e.g. a staging URL when the backend only trusts prod, http:// when only https origins are listed, or a completely different domain).","commonSituations":"Misconfigured FRONTEND_BASE_URL / allowed-origins env var in the environment sending the email; a local dev frontend at http://localhost:3000 while the backend only trusts the deployed origin; a URL with a typo'd hostname or wrong port; constructing the reset/verify link server-side from the wrong base URL.","solutions":["Set the url to the exact frontend origin the backend is configured to trust (scheme, host, and port must all match an allowed origin).","Check the backend's trusted-origins config/env (e.g. APP_BACKEND_CORS_ORIGINS or equivalent) and add the origin you actually send from, then redeploy/restart.","If running locally, point the url at the local frontend origin and ensure that origin is in the backend's allowed list for the dev environment.","Verify no stray path/port mismatch: the origin check runs before path escaping, so fix the scheme://host:port portion first."],"exampleFix":"# before\n{\"type\": \"reset_password\", \"url\": \"http://localhost:3000/reset?token=...\"}  # origin not trusted in prod\n\n# after\n{\"type\": \"reset_password\", \"url\": \"https://app.example.com/reset?token=...\"}  # matches configured trusted origin","handlingStrategy":"validation","validationCode":"const allowed = new Set(trustedOrigins); // from backend config\nconst origin = new URL(link).origin;\nif (!allowed.has(origin)) throw new Error(`untrusted origin: ${origin}`);\nawait sendAuthEmail({type, url: link});","typeGuard":"function isTrustedOrigin(url: string, allowed: string[]): boolean {\n  try { return allowed.includes(new URL(url).origin); } catch { return false; }\n}","tryCatchPattern":null,"preventionTips":["Build action links from a single configured frontend base URL constant.","Keep backend trusted-origins env in sync with every environment that sends mail.","Test scheme/host/port exactly — origin matching is strict."],"tags":["backend","auth","email","security","http-400"],"backgroundTag":null,"analyzedSha":"9c8bb5550f446ba5d3046b78896578742495b3cf","analyzedAt":"2026-08-14T17:17:21.957Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}