Skip to main content

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:

MethodChecked?
GET, HEAD, OPTIONSNo - always allowed
POST, PUT, PATCH, DELETEYes

For a checked request, Log Lens looks at the Origin header:

  • No Origin header 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.
  • Origin present to the host portion of Origin must match the host portion of the request's Host header (compared case-insensitively), and, when both state one, the port must match too. A match is allowed; anything else is blocked. A configured LOG_LENS_URL is 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 Origin and Host no 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.