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:
returncsrf.Protect([]byte(m.params.Config.SessionSecret),csrf.Secure(false),// Allow HTTP for development; reverse proxy handles TLScsrf.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.
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
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
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
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:258builds the CSRF middleware as:csrf.Secure(false)only clears theSecureflag 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 tohttpsand is only set tohttpwhen the request was marked bycsrf.PlaintextHTTPRequest. The origin check atcsrf.go:288then compares the browser'sOrigin: http://host:8080against a synthesisedhttps://host:8080and fails.How to reproduce
Open
http://127.0.0.1:8080/in a browser, fill in the setup form, submit. The response is403 Forbidden - origin invalidand no admin user is created. Same for/loginand every other POST.Without a browser:
Omitting the
Originheader instead gives403 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_passto 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 runanddocker composerecipes work end to end in a browser overhttp://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.PlaintextHTTPRequestwhen the request is not TLS and noX-Forwarded-Proto: httpsis 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 anOriginheader matching a plaintext listener and asserts it is not rejected.Model: opus-5
Fixed in #193: plain-HTTP CSRF now works via a new
UPAAS_PLAINTEXT_HTTPsetting (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