carlos / platform Public

console: keep the app-binding 404 byte-identical (#229)

The app binding check must return the same 404 response for both
"not your app" and "no such app" to avoid leaking information. Reverted
the JSON response change and fixed the test to use raw requests instead.
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev 2f64b095294a59c62fdd9075a6163184b9b51ae0 parent ddefc40
2 files changed, +16 −5
  • internal/console/apicli.go +1 −1
  • internal/console/deploydoors_test.go +15 −4
diff --git a/internal/console/apicli.go b/internal/console/apicli.go
index 08555b983..0787152fb 100644
--- a/internal/console/apicli.go
+++ b/internal/console/apicli.go
@@ -68,7 +68,7 @@ func (s *Server) apiAccountAppBy(w http.ResponseWriter, r *http.Request, has fun
// already refused outright for the one credential shape that can carry
// one (see serviceBinding).
if p.Kind == PrincipalService && p.App != "" && p.App != app {
- jsonError(w, http.StatusNotFound, "app not found")
+ http.NotFound(w, r)
return "", nil, "", false
}
found, err := has(acct.ID, app)
diff --git a/internal/console/deploydoors_test.go b/internal/console/deploydoors_test.go
index fb7fb4d59..56da84e68 100644
--- a/internal/console/deploydoors_test.go
+++ b/internal/console/deploydoors_test.go
@@ -56,13 +56,24 @@ func TestDeployIsBoundToItsApp(t *testing.T) {
if err != nil {
t.Fatal(err)
}
- status, out := postJSON(t, fx.ts.URL+"/api/cli/apps/bab/other/promote",
+ // Sibling app: deploy reaches "other" (exists) but its token is bound to "hello"
+ statusA, bodyA := postRaw(t, fx.ts.URL+"/api/cli/apps/bab/other/promote",
map[string]string{"version": "v1", "channel": "edge"}, tok)
- if status != http.StatusNotFound {
- t.Fatalf("deploy on a sibling app = %d %v, want 404", status, out)
+ if statusA != http.StatusNotFound {
+ t.Fatalf("deploy on a sibling app = %d, want 404", statusA)
+ }
+ // Nonexistent app: deploy reaches "nosuchapp" (does not exist) on the same account
+ statusB, bodyB := postRaw(t, fx.ts.URL+"/api/cli/apps/bab/nosuchapp/promote",
+ map[string]string{"version": "v1", "channel": "edge"}, tok)
+ if statusB != http.StatusNotFound {
+ t.Fatalf("deploy on a nonexistent app = %d, want 404", statusB)
+ }
+ // Both refusals must be byte-identical to avoid leaking whether "other" exists
+ if bodyA != bodyB {
+ t.Fatalf("app-binding 404 is not identical to nonexistent-app 404:\n(sibling) %q\n(nonexistent) %q", bodyA, bodyB)
}
// And a door outside §6 is the role gate's 403, not a 404.
- status, out = postJSON(t, fx.ts.URL+"/api/cli/apps/bab/hello/delete", map[string]string{}, tok)
+ status, out := postJSON(t, fx.ts.URL+"/api/cli/apps/bab/hello/delete", map[string]string{}, tok)
if status != http.StatusForbidden {
t.Fatalf("deploy on trash = %d %v, want 403", status, out)
}