You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Two further pre-existing routing divergences between the equal-peer implementations, surfaced during the merge-gate review of #43 (both set aside there as out of scope):
TS denies exec anywhere in the path.ts/src/proxy.ts (~line 35) rejects any path containing an /exec segment, so e.g. GET /images/exec is denied in TypeScript but allowed (passthrough) in Go and Rust.
Go's matchEndpoint accepts subpaths of exact endpoints.POST /containers/create/extra reaches routeCreate in Go, while Rust and TypeScript exact-match the endpoint and fall through to default-deny.
Impact
Low severity. Neither is exploitable by itself (gates still apply to whatever is routed), but which requests reach the daemon — and which gate chain they go through — depends on the implementation language, which the equal-peers rule exists to prevent. Behaviour should converge; in both cases the stricter behaviour looks correct (deny POST /containers/create/extra everywhere; scope TS's exec check to the positions Go/Rust check).
Proposed solution
Decide the canonical row for each case, converge all three implementations, and pin each row with same-named unit tests in all three languages plus integration checks — the #24/#43 pattern. The Quint routing model should gain the two rows as well so the table stays the source of truth.
Bundling with the empty-name divergence fix: possible if a maintainer prefers one PR; filed separately because the empty-name case has a clear one-line fix while these two need a canonical-behaviour decision first.
A third case in this family, confirmed during the #51 merge-gate review by running the Go router directly:
DELETE /containers/<name>/exec — Go's exec check (matchEndpoint(path, "containers", "exec")) matches exec only in the name position, so this path takes the lifecycle branch and is allowed and forwarded. Rust (path.contains("/exec") after the /containers/ prefix check) and TypeScript (path.includes("/exec")) both deny it.
Related: POST /containers/<name>/exec is denied in Go too, but by default-deny rather than the exec check — the three implementations agree on the status for that one, for different reasons.
Observed outputs (Go router, built from main):
DELETE /containers/mycontainer/exec -> Allow (forwarded)
POST /containers/mycontainer/exec -> Deny "endpoint ... is not allowed"
DELETE /containers/exec -> Deny "exec is not allowed"
So when converging the exec rule, the decision isn't only about TS being too broad — Go is too narrow. spec/README.md's Modeling Notes (post-#51) document this divergence and point here. Note deploy/test.sh's POST /containers/*/exec -> 403 check passes for different reasons per language today.
Decided design (2026-10-08). Probe of all three routers on main:
Request
Go
Rust
TS
DELETE /containers/mycontainer/exec
Allow
Deny
Deny
POST /containers/exec-runner/start
Allow
Deny
Deny
DELETE /containers/exec-runner
Allow
Deny
Deny
GET /containers/exec-runner/json
Allow
Deny
Deny
GET /images/exec
Allow
Allow
Deny
GET /exec/abc/json
Allow
Allow
Deny
POST /containers/create/extra
CreateContainer
Deny
Deny
POST /images/create/extra
Allow
Deny
Deny
New case: Rust (contains("/exec")) and TS (includes("/exec")) are substring checks. A container whose name starts with exec (exec-runner, executor) therefore cannot be started, removed or inspected through them, although the daemon allows such names.
Rule, matching whole path segments (after the version strip):
Exec is denied when the segments are containers/<name>/exec, for any method, or when the first segment is exec, for every method including GET. Exec inspect leaks command lines: container inspect lists ExecIDs, and GET /exec/<id>/json then shows the full arguments, including inline secrets (verified live). /containers/exec stays a reserved name.
Exact endpoints:POST /containers/create and POST /images/create match only with exactly two segments. Anything longer falls to the default deny.
Spec rows first (spec/router.qnt), then a shared test table in all three languages plus integration checks.
Activity
abienkowski commentedon Oct 7, 2026
A third case in this family, confirmed during the #51 merge-gate review by running the Go router directly:
DELETE /containers/<name>/exec— Go's exec check (matchEndpoint(path, "containers", "exec")) matchesexeconly in the name position, so this path takes the lifecycle branch and is allowed and forwarded. Rust (path.contains("/exec")after the/containers/prefix check) and TypeScript (path.includes("/exec")) both deny it.POST /containers/<name>/execis denied in Go too, but by default-deny rather than the exec check — the three implementations agree on the status for that one, for different reasons.Observed outputs (Go router, built from main):
So when converging the exec rule, the decision isn't only about TS being too broad — Go is too narrow.
spec/README.md's Modeling Notes (post-#51) document this divergence and point here. Notedeploy/test.sh'sPOST /containers/*/exec -> 403check passes for different reasons per language today.abienkowski commentedon Oct 8, 2026
Decided design (2026-10-08). Probe of all three routers on
main:DELETE /containers/mycontainer/execPOST /containers/exec-runner/startDELETE /containers/exec-runnerGET /containers/exec-runner/jsonGET /images/execGET /exec/abc/jsonPOST /containers/create/extraPOST /images/create/extraNew case: Rust (
contains("/exec")) and TS (includes("/exec")) are substring checks. A container whose name starts withexec(exec-runner,executor) therefore cannot be started, removed or inspected through them, although the daemon allows such names.Rule, matching whole path segments (after the version strip):
containers/<name>/exec, for any method, or when the first segment isexec, for every method including GET. Exec inspect leaks command lines: container inspect listsExecIDs, andGET /exec/<id>/jsonthen shows the full arguments, including inline secrets (verified live)./containers/execstays a reserved name.POST /containers/createandPOST /images/creatematch only with exactly two segments. Anything longer falls to the default deny.spec/router.qnt), then a shared test table in all three languages plus integration checks.The per-socket exec feature is split out to #65.
spec: clarify the #49 history note (#49)
refactor: parenthesise the Go exec check; exact README exec row (#49)
fix: match exec and create endpoints on whole path segments (#49) (#67)