Cross-origin (CSRF) protection
Log Lens guards every state-changing request with a same-origin check. This stops a malicious web page you happen to have open from silently issuing writes to your local Log Lens instance on your behalf - the classic cross-site request forgery (CSRF) attack - without getting in the way of scripts, curl, cron jobs, or the AI skills.
How it works
The guard only inspects state-changing requests. Safe, read-only methods are always allowed through:
| Method | Checked? |
|---|---|
GET, HEAD, OPTIONS | No - always allowed |
POST, PUT, PATCH, DELETE | Yes |
For a checked request, Log Lens looks at the Origin header:
- No
Originheader to allowed. Browsers add this header automatically and forbid page JavaScript from forging or removing it, so its absence marks a non-browser client (curl, cron, the Claude/Codex skills). These pass through unchanged. Originpresent to the host portion ofOriginmust match the host portion of the request'sHostheader (compared case-insensitively), and, when both state one, the port must match too. A match is allowed; anything else is blocked. A configuredLOG_LENS_URLis always an accepted origin.
When the two origins disagree, Log Lens returns:
HTTP/1.1 403 Forbidden
Content-Type: application/json
{"error":"Cross-origin request blocked. State-changing requests must come from the same origin."}
Because the browser attaches a truthful Origin on every write and cannot be tricked into forging one, a mismatch is a reliable CSRF signal.
What this means for you
Using the dashboard on the same host you serve it from - the normal case - you will never see this. Requests from the UI carry a matching Origin, so they are allowed.
You may hit a 403 if you:
- Reach the dashboard through a different hostname or port than the one it is served on (for example a reverse proxy or tunnel that rewrites the host), so
OriginandHostno longer agree. - Drive writes from browser-based tooling on another site.
The fix is to make the origin and host line up - access Log Lens through the same hostname the server sees, or point your tooling at that hostname.
Note
Host and port are compared; the scheme is not. A reverse proxy that terminates TLS without setting X-Forwarded-Proto leaves the server believing it serves http while the browser correctly reports an https origin, and comparing schemes would reject those deployments for no gain - a page cannot be served over http by a host:port that answers https.
The port is compared, because on a development machine every local site is localhost: without it, a page on localhost:3000 could drive a Log Lens on localhost:8080. A side that states no port (the usual production shape, https://app.example.com against Host: app.example.com) compares on host alone.
The cross-origin guard is separate from, and complementary to, the optional API key. When a key is configured, that check runs first; the origin check applies regardless.