spec: login POST routes by next; a login POST to an ungated route is 404
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
1 file changed,
+22
−4
docs/superpowers/specs/2026-09-07-password-gate-design.md+22 −4
diff --git a/docs/superpowers/specs/2026-09-07-password-gate-design.md b/docs/superpowers/specs/2026-09-07-password-gate-design.md| index 5e2743681..3b62df343 100644 |
| --- a/docs/superpowers/specs/2026-09-07-password-gate-design.md |
| +++ b/docs/superpowers/specs/2026-09-07-password-gate-design.md |
| @@ -159,10 +159,28 @@ redirect after a successful POST, or the too-many-attempts page. |
| ### 4.2 The reserved path |
| `POST /.carlos/gate` on a gated host is the login form's action. Fields: |
| -`password`, `next`. On any other host or method the path is ordinary and |
| -reaches the app. `next` is accepted only if it starts with a single `/` |
| -(not `//`, not a scheme), else it becomes `/`; that is the entire |
| -open-redirect surface and it is closed at the door. |
| +`password`, `next`. `next` is accepted only if it starts with a single `/` |
| +(not `//`, not a scheme, no control bytes), else it becomes `/`; that is |
| +the entire open-redirect surface and it is closed at the door. |
| + |
| +**The edge routes a login POST by `next`, not by its own path** (review |
| +finding, 2026-09-07). A hostname that routes by path prefix resolves each |
| +request's first segment to a target route; `/.carlos/gate` has no such |
| +segment and would fall through to the front host's own route — so a gated |
| +prefix target could never be logged into, and the password would be |
| +proxied to whatever app fronts the hostname. So, for exactly `POST |
| +/.carlos/gate`, the edge parses the form (bounded, 4 KiB) and resolves the |
| +upstream against the path in `next`, which is the path the visitor was |
| +refused on. On a host with no prefix map this resolves to the same route |
| +the request path would have; on a prefix host it resolves to the target |
| +the visitor was trying to reach, whose hash judges the password and whose |
| +host signs the cookie (§4.3). |
| + |
| +**A login POST to an ungated route is a 404, never proxied.** The page is |
| +only ever rendered by a gated route, so a POST that reaches an ungated one |
| +is a stray or a probe; forwarding a form field named `password` to an app |
| +that never asked for it is the one outcome to rule out. On every other |
| +method the path is ordinary and reaches the app. |
| Outcomes: |