Skip to content

Commit fecef4f

Browse files
rdimitrovclaude
andauthored
fix: don't leak internal error detail in GET /v0/servers 500 response (#1338)
## Summary Fixes the information-disclosure regression introduced by #1335 (item 1 of #1337). `ListServersError` passed the raw `err` into `huma.Error500InternalServerError(...)`. huma serializes extra error args into the response body's `errors` array (`ErrorModel.Errors`, `json:"errors,omitempty"`), so on the **public, unauthenticated** `GET /v0/servers` endpoint a genuine DB failure echoed the internal wrapped error from `internal/database/postgres.go` (`error iterating rows: <pgx error>`) back to the client. pgx/pgconn errors can carry SQLSTATE codes, constraint/table/column names, and internal messages (CWE-209). ## Change - Drop `err` from the 500 call so only the generic message is returned; keep `log.Printf` for the server-side detail — matching the sibling handlers (`servers.go:190/220`, `edit.go`, `status.go`). - Add a regression test (`TestListServersError_realFailureDoesNotLeakDetail`) that marshals the 500 error and asserts the internal detail is absent from the client-visible body. Existing tests only asserted status codes, which is why the leak slipped through. The 499 client-cancelled path is unchanged (the client has already disconnected, so nothing is leaked there). ## Test plan - [x] `go test ./internal/api/handlers/v0/... -run ListServersError` (3/3 pass) - [x] `go build ./...`, `go vet`, `golangci-lint` (no new findings in changed files) ## Note This leaves the non-security items in #1337 (correcting the `superfluous WriteHeader` claim, `statusClientClosed` const reuse, broader cancellation-guard scope) for separate follow-up. Closing #1337 here since the only security-impacting item is addressed; reopen/split if you'd prefer to track the nits independently. Closes #1337 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent ef29673 commit fecef4f

2 files changed

Lines changed: 18 additions & 1 deletion

File tree

internal/api/handlers/v0/list_errors.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,5 +17,8 @@ func ListServersError(ctx context.Context, err error) error {
1717
return huma.NewError(499, "Client closed request", err)
1818
}
1919
log.Printf("list servers failed: %v", err)
20-
return huma.Error500InternalServerError("Failed to get registry list", err)
20+
// Do not pass err here: huma serializes extra error args into the response
21+
// body, which would leak internal (e.g. pgx) error detail to clients. Log it
22+
// server-side only, like the sibling handlers in servers.go.
23+
return huma.Error500InternalServerError("Failed to get registry list")
2124
}

internal/api/handlers/v0/list_errors_test.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package v0_test
22

33
import (
44
"context"
5+
"encoding/json"
56
"errors"
67
"testing"
78

@@ -29,3 +30,16 @@ func TestListServersError_realFailure(t *testing.T) {
2930
require.True(t, errors.As(err, &se))
3031
assert.Equal(t, 500, se.GetStatus())
3132
}
33+
34+
// The 500 response body must not echo the internal error back to the client,
35+
// since it can carry DB/driver detail (CWE-209).
36+
func TestListServersError_realFailureDoesNotLeakDetail(t *testing.T) {
37+
const internal = "database unavailable: dsn=postgres://user:pw@internal-host/db"
38+
err := v0.ListServersError(context.Background(), errors.New(internal))
39+
require.Error(t, err)
40+
41+
body, marshalErr := json.Marshal(err)
42+
require.NoError(t, marshalErr)
43+
assert.NotContains(t, string(body), "database unavailable")
44+
assert.NotContains(t, string(body), "internal-host")
45+
}

0 commit comments

Comments
 (0)