[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:
- Middleware fires only when the matched security scheme is cookie-based. Basic-auth requests bypass entirely.
- When active: read
Origin (preferred), fall back to Referer. Parse scheme+host.
same-origin: true → require match against the request's own scheme/host/port.
allowed-origins: → require membership in the list.
- On mismatch → 403 with Roaster's standard error shape.
- 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)
- Middleware module in
content/.
- OpenAPI spec extension parsing for
x-csrf.
- XQSuite tests: cookie+matching-origin pass; cookie+mismatched-origin 403; cookie+missing-origin 403; basic-auth bypass; no-config bypass.
- README /
doc/cookie-auth.md update.
Happy to do the implementation work on a feature branch once the shape is agreed. /cc @line-o
[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-packageendpoint in eXist'spublic-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):
cookieAuth+basicAuthwithx-constraints: { groups: ["dba"] }per route.controller.xqreadsX-Requested-Withonly to decide 401 rendering, not as a gate.Public-repo will ship an inline check in
publish-package.xqfor 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 likexst/packageservice).Proposed design
Token-less, Origin/Referer-based, OpenAPI-native. Fits the existing
security/x-constraintspatterns.Spec syntax — leaning toward a per-route
x-csrfextension that mirrorsx-constraints: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:
Origin(preferred), fall back toReferer. Parse scheme+host.same-origin: true→ require match against the request's own scheme/host/port.allowed-origins:→ require membership in the list.OriginandRefereron 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-constraintsgroup enforcement.Implementation note
Today
auth:authenticateincontent/auth.xqlpropagates the resolveduseronto the request but not the matched scheme name. A small change there to record e.g.request?auth-schemeis the natural foundation for the scheme-aware gate; the CSRF middleware then keys off it.Why this design
xst,packageservice,curl --userkeep working untouched.Proposed deliverables (if accepted)
content/.x-csrf.doc/cookie-auth.mdupdate.Happy to do the implementation work on a feature branch once the shape is agreed. /cc @line-o