Setup and every other POST return 403 over plain HTTP, so the documented docker run is unusable #189

Closed
opened 2026-09-09 14:39:08 +02:00 by clawbot · 1 comment
Collaborator

What is wrong

Every state-changing POST is rejected with HTTP 403 when µPaaS is reached over plain HTTP, which is exactly how the README tells you to run it. A fresh install cannot get past /setup, so the product is unusable when followed as documented.

internal/middleware/middleware.go:258 builds the CSRF middleware as:

return csrf.Protect(
    []byte(m.params.Config.SessionSecret),
    csrf.Secure(false), // Allow HTTP for development; reverse proxy handles TLS
    csrf.Path("/"),
)

csrf.Secure(false) only clears the Secure flag on the CSRF cookie. It does not tell gorilla/csrf that the request arrived over plaintext HTTP. In gorilla/csrf v1.7.3 (csrf.go:271) the request URL scheme defaults to https and is only set to http when the request was marked by csrf.PlaintextHTTPRequest. The origin check at csrf.go:288 then compares the browser's Origin: http://host:8080 against a synthesised https://host:8080 and fails.

How to reproduce

make docker
mkdir -p /tmp/upaas-data
docker run -d -p 8080:8080 \
  -v /var/run/docker.sock:/var/run/docker.sock \
  -v /tmp/upaas-data:/var/lib/upaas \
  -e UPAAS_HOST_DATA_DIR=/tmp/upaas-data \
  upaas

Open http://127.0.0.1:8080/ in a browser, fill in the setup form, submit. The response is 403 Forbidden - origin invalid and no admin user is created. Same for /login and every other POST.

Without a browser:

curl -s -c c.jar http://127.0.0.1:8080/setup \
  | grep -o 'name="gorilla.csrf.Token" value="[^"]*"'
# POST with the token and the header a browser sends:
curl -i -b c.jar -X POST http://127.0.0.1:8080/setup \
  -H 'Origin: http://127.0.0.1:8080' \
  --data-urlencode "gorilla.csrf.Token=<token>" \
  --data-urlencode username=admin \
  --data-urlencode password=adminadmin \
  --data-urlencode password_confirm=adminadmin
# -> 403 Forbidden - origin invalid

Omitting the Origin header instead gives 403 Forbidden - referer not supplied, because gorilla/csrf also applies its TLS-only strict Referer check.

Putting a TLS-terminating reverse proxy in front (nginx, proxy_pass to the container over HTTP) makes every one of these requests succeed, which confirms the scheme mismatch is the whole cause.

What acceptable looks like

The README's own docker run and docker compose recipes work end to end in a browser over http://host:8080: setup creates the admin, login works, apps can be created and edited.

Either mark plaintext requests for gorilla/csrf (wrap the handler with csrf.PlaintextHTTPRequest when the request is not TLS and no X-Forwarded-Proto: https is present), or make the trusted origins configurable and document that TLS is mandatory. Whichever way it goes, add a test that drives a POST with an Origin header matching a plaintext listener and asserts it is not rejected.

Model: opus-5

## What is wrong Every state-changing POST is rejected with HTTP 403 when µPaaS is reached over plain HTTP, which is exactly how the README tells you to run it. A fresh install cannot get past `/setup`, so the product is unusable when followed as documented. `internal/middleware/middleware.go:258` builds the CSRF middleware as: ```go return csrf.Protect( []byte(m.params.Config.SessionSecret), csrf.Secure(false), // Allow HTTP for development; reverse proxy handles TLS csrf.Path("/"), ) ``` `csrf.Secure(false)` only clears the `Secure` flag on the CSRF cookie. It does not tell gorilla/csrf that the request arrived over plaintext HTTP. In gorilla/csrf v1.7.3 (`csrf.go:271`) the request URL scheme defaults to `https` and is only set to `http` when the request was marked by `csrf.PlaintextHTTPRequest`. The origin check at `csrf.go:288` then compares the browser's `Origin: http://host:8080` against a synthesised `https://host:8080` and fails. ## How to reproduce ``` make docker mkdir -p /tmp/upaas-data docker run -d -p 8080:8080 \ -v /var/run/docker.sock:/var/run/docker.sock \ -v /tmp/upaas-data:/var/lib/upaas \ -e UPAAS_HOST_DATA_DIR=/tmp/upaas-data \ upaas ``` Open `http://127.0.0.1:8080/` in a browser, fill in the setup form, submit. The response is `403 Forbidden - origin invalid` and no admin user is created. Same for `/login` and every other POST. Without a browser: ``` curl -s -c c.jar http://127.0.0.1:8080/setup \ | grep -o 'name="gorilla.csrf.Token" value="[^"]*"' # POST with the token and the header a browser sends: curl -i -b c.jar -X POST http://127.0.0.1:8080/setup \ -H 'Origin: http://127.0.0.1:8080' \ --data-urlencode "gorilla.csrf.Token=<token>" \ --data-urlencode username=admin \ --data-urlencode password=adminadmin \ --data-urlencode password_confirm=adminadmin # -> 403 Forbidden - origin invalid ``` Omitting the `Origin` header instead gives `403 Forbidden - referer not supplied`, because gorilla/csrf also applies its TLS-only strict Referer check. Putting a TLS-terminating reverse proxy in front (nginx, `proxy_pass` to the container over HTTP) makes every one of these requests succeed, which confirms the scheme mismatch is the whole cause. ## What acceptable looks like The README's own `docker run` and `docker compose` recipes work end to end in a browser over `http://host:8080`: setup creates the admin, login works, apps can be created and edited. Either mark plaintext requests for gorilla/csrf (wrap the handler with `csrf.PlaintextHTTPRequest` when the request is not TLS and no `X-Forwarded-Proto: https` is present), or make the trusted origins configurable and document that TLS is mandatory. Whichever way it goes, add a test that drives a POST with an `Origin` header matching a plaintext listener and asserts it is not rejected. Model: opus-5
Author
Collaborator

Fixed in #193: plain-HTTP CSRF now works via a new UPAAS_PLAINTEXT_HTTP setting (default off, so a TLS-terminating reverse proxy is unaffected); the README plain-HTTP recipe sets it. Verified by creating the admin user over plain HTTP.

Model: opus-4-8

Fixed in https://git.eeqj.de/sneak/upaas/pulls/193: plain-HTTP CSRF now works via a new `UPAAS_PLAINTEXT_HTTP` setting (default off, so a TLS-terminating reverse proxy is unaffected); the README plain-HTTP recipe sets it. Verified by creating the admin user over plain HTTP. Model: opus-4-8
sneak closed this issue 2026-09-10 11:17:41 +02:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: sneak/upaas#189