{"record":{"id":"83d4893a587c9d67","repo":"laurent22/joplin","slug":"invalidorigin","errorCode":"InvalidOrigin","errorMessage":"Invalid origin: ${ctx.URL.origin}","messagePattern":"Invalid origin: (.+?)","errorType":"http","errorClass":"ErrorNotFound","httpStatus":404,"severity":"error","filePath":"packages/server/src/utils/routeUtils.ts","lineNumber":217,"sourceCode":"}\n\nfunction disabledAccountCheck(route: MatchedRoute, user: User) {\n\tif (!user || user.enabled) return;\n\n\tif (route.subPath.schema.startsWith('api/')) throw new ErrorForbidden(`This account is disabled. Please login to ${config().baseUrl} for more information.`);\n}\n\ninterface ExecRequestResult {\n\tresponse: unknown;\n\tpath: SubPath;\n}\n\nexport async function execRequest(routes: Routers, ctx: AppContext): Promise<ExecRequestResult> {\n\tconst match = findMatchingRoute(ctx.path, routes);\n\tif (!match) throw new ErrorNotFound();\n\n\tconst endPoint = match.route.findEndPoint(ctx.request.method as HttpMethod, match.subPath.schema);\n\tif (ctx.URL && !isValidOrigin(ctx.URL.origin, baseUrl(endPoint.type), endPoint.type)) throw new ErrorNotFound(`Invalid origin: ${ctx.URL.origin}`, ErrorCode.InvalidOrigin);\n\n\tconst isPublicRoute = match.route.isPublic(match.subPath.schema, ctx.request.method as HttpMethod);\n\n\t// This is a generic catch-all for all private end points - if we\n\t// couldn't get a valid session, we exit now. Individual end points\n\t// might have additional permission checks depending on the action.\n\tif (!isPublicRoute && !ctx.joplin.owner) {\n\t\tif (contextSessionId(ctx, false)) {\n\t\t\t// If we have a session but not a user it means the session was\n\t\t\t// invalid or has expired, so display a special message, since this\n\t\t\t// is also going to be displayed on the website.\n\t\t\tthrow new ErrorForbidden('Your session has expired. Please login again.');\n\t\t} else {\n\t\t\tthrow new ErrorForbidden();\n\t\t}\n\t}\n\n\tawait csrfCheck(ctx, isPublicRoute);","sourceCodeStart":199,"sourceCodeEnd":235,"githubUrl":"https://github.com/laurent22/joplin/blob/683240968be863e9657d767aa9f4103ff666539a/packages/server/src/utils/routeUtils.ts#L199-L235","documentation":"The Joplin server throws this error (as an ErrorNotFound with code InvalidOrigin) when the Origin of the incoming request does not match the base URL configured for the endpoint's route type. It is an anti-DNS-rebinding / host-spoofing check: the request's host must equal the configured app base URL host (for UserContent routes, either the exact usercontent host or any single-label subdomain of it). When the hosts differ, the request is rejected as invalid before session or permission checks run.","triggerScenarios":"Calling any Joplin server route (e.g. GET /api/ping or a /share user-content URL) from a host that differs from config().APP_BASE_URL (or baseUrl(RouteType.UserContent) for user-content routes). Concretely: accessing the server via http://localhost:22300 while APP_BASE_URL is set to https://notes.example.com; accessing a user-content route via a bare domain when the URL uses a per-user subdomain (userid.example.com vs example.com); or an HTTP client (browser fetch/POST) sending an Origin header for a different port than the configured base URL.","commonSituations":"Misconfigured APP_BASE_URL in the server env (points at a different domain/port than how clients actually reach the server); accessing the server through localhost, 127.0.0.1, a LAN IP, or a reverse proxy that rewrites Host while baseUrl is the public domain; dev environments where the base URL was set for production; proxy not forwarding the original Host header; changing domains without updating the base URL config.","solutions":["Verify how you are reaching the server (scheme, host, port) and make it match APP_BASE_URL (or the user-content base URL) exactly, including port.","Check the server config: APP_BASE_URL (and USER_CONTENT_BASE_URL if set) must be the same host clients actually use; update it after domain/proxy changes.","If behind a reverse proxy, ensure it preserves/forwards the original Host header so ctx.URL.origin reflects the real request origin.","If you intentionally need multiple hosts (e.g. localhost in dev), point APP_BASE_URL at the host you use, or add proxy/redirect rules so all access goes through the canonical base URL.","For API clients, confirm your client is not injecting an Origin header for a different origin (some SDKs and redirects do)."],"exampleFix":"// before: server env\nAPP_BASE_URL=https://notes.example.com\n// client calls http://localhost:22300/api/ping -> InvalidOrigin\n\n// after: access via the configured base URL\ncurl https://notes.example.com/api/ping\n// or, for local dev, set the env to match:\nAPP_BASE_URL=http://localhost:22300","handlingStrategy":"validation","validationCode":"// Before calling the API, check that the URL you use matches the server's configured base URL\nimport { URL } from 'url';\n\nfunction assertMatchingOrigin(requestUrl: string, configuredBaseUrl: string): boolean {\n\ttry {\n\t\treturn new URL(requestUrl).host === new URL(configuredBaseUrl).host;\n\t} catch {\n\t\treturn false;\n\t}\n}\n\n// Usage:\n// if (!assertMatchingOrigin('http://localhost:22300/api/ping', process.env.APP_BASE_URL)) throw new Error('Use the configured APP_BASE_URL host');","typeGuard":"null","tryCatchPattern":"// If you cannot pre-validate, catch and inspect the code:\ntry {\n\tawait apiClient.ping();\n} catch (error) {\n\tif (error && (error as any).code === 'InvalidOrigin') {\n\t\t// host you used differs from the server's configured base URL\n\t\tthrow new Error(`Request host does not match the server's APP_BASE_URL: ${error.message}`);\n\t}\n\tthrow error;\n}","preventionTips":["Keep APP_BASE_URL (and USER_CONTENT_BASE_URL) in sync with the exact host/port clients use to reach the server, including scheme and port.","Configure reverse proxies to pass through the original Host header (e.g. proxy_set_header Host $host).","In dev, use one canonical origin (localhost:PORT) for both the server config and all clients instead of mixing localhost, 127.0.0.1, and LAN IPs.","After domain migrations, update the base URL config before pointing clients at the new domain.","Log ctx.URL.origin alongside the configured base URL when this error occurs to spot mismatches immediately."],"tags":["joplin-server","origin-check","csrf","config","reverse-proxy","base-url"],"backgroundTag":"origin-mismatch-host-header-validation","analyzedSha":"683240968be863e9657d767aa9f4103ff666539a","analyzedAt":"2026-08-28T11:52:29.072Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}