Skip to content

Proposal: declarative CSRF protection for cookie-authenticated routes #117

Description

@joewiz

[This response was co-authored with Claude Code. -Joe]

Summary

Propose adding declarative, opt-in CSRF protection to Roaster, enforced only when the matched security scheme is cookie-based. This builds directly on the cookie-login polish in v1.12.0 and closes a real gap that affects every Roaster app accepting state-changing requests from a browser.

Motivating threat model

The publish-package endpoint in eXist's public-repo (and any analogous admin-upload endpoint) is a textbook CSRF target: a logged-in dba who visits a malicious page can be tricked into POSTing a multipart form that installs an arbitrary XAR — i.e. arbitrary code execution on the eXist server.

Survey of the current state (2026-05-17):

  • eXide (on Roaster): no CSRF protection. Uses cookieAuth+basicAuth with x-constraints: { groups: ["dba"] } per route. controller.xq reads X-Requested-With only to decide 401 rendering, not as a gate.
  • monex (not on Roaster): no CSRF protection.
  • public-repo (not on Roaster): no CSRF protection.

Public-repo will ship an inline check in publish-package.xq for the eXist 7 beta next week, but adding the same logic to every app in turn duplicates work and misses future Roaster apps. Roaster already resolves which security scheme matched per request — exactly the signal needed to apply CSRF only where it matters (cookie auth) and skip it where it doesn't (Basic auth from CLI clients like xst / packageservice).

Proposed design

Token-less, Origin/Referer-based, OpenAPI-native. Fits the existing security / x-constraints patterns.

Spec syntax — leaning toward a per-route x-csrf extension that mirrors x-constraints:

paths:
  /publish:
    post:
      x-csrf:
        same-origin: true
        allowed-origins: ["https://exist-db.org"]

A top-level csrf: default with per-route override is the obvious alternative, but the per-route extension reads more like the rest of Roaster's vocabulary and keeps the spec local to the endpoint that needs protection. Happy to be argued out of it.

Behavior:

  1. Middleware fires only when the matched security scheme is cookie-based. Basic-auth requests bypass entirely.
  2. When active: read Origin (preferred), fall back to Referer. Parse scheme+host.
  3. same-origin: true → require match against the request's own scheme/host/port.
  4. allowed-origins: → require membership in the list.
  5. On mismatch → 403 with Roaster's standard error shape.
  6. Missing both Origin and Referer on a cookie-auth state-changing request → 403.

Where it slots in: after auth resolution (so the matched scheme is known), before the route handler runs — same lifecycle slot as x-constraints group enforcement.

Implementation note

Today auth:authenticate in content/auth.xql propagates the resolved user onto the request but not the matched scheme name. A small change there to record e.g. request?auth-scheme is the natural foundation for the scheme-aware gate; the CSRF middleware then keys off it.

Why this design

  • Token-less. OWASP-recommended "simple, robust CSRF defense" for state-changing endpoints. No token store, no template cooperation needed.
  • Backwards compatible. Off by default; existing apps see no change.
  • CLI-friendly. The auth-scheme gate is the key insight — xst, packageservice, curl --user keep working untouched.
  • One line of OpenAPI config to protect a whole app.

Proposed deliverables (if accepted)

  1. Middleware module in content/.
  2. OpenAPI spec extension parsing for x-csrf.
  3. XQSuite tests: cookie+matching-origin pass; cookie+mismatched-origin 403; cookie+missing-origin 403; basic-auth bypass; no-config bypass.
  4. README / doc/cookie-auth.md update.

Happy to do the implementation work on a feature branch once the shape is agreed. /cc @line-o

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions