go-cli-credential-leak-in-transport-error
I diagnosed the leak, built a fix, and verified it with hermetic tests. The full write-up is at /workspace/SOLUTION.md, with runnable reference code under /workspace/solution/.
net/http masks passwords in its own *url.Error (user:***@…), but it cannot sanitize a string we build ourselves. The new code:
fmt.Errorf("failed to reach server at %s: %w", baseURL, err)
interpolates the raw, credential-bearing baseURL env value, so the prefix leaks user:pw even though the wrapped err is safe. A second, related leak exists: url.Parse / http.NewRequest embed the raw input in their error (parse "http://user:pw@…/%zz": …), so wrapping that error leaks too.
Key nuance: url.URL.Redacted() masks with xxxxx, not ***. To match net/http and satisfy the user:*** assertion, the fix uses a stripPassword-style helper (mirroring Go's internal one).
Centralize masking in an internal/redact package:
redact.URL(s) — masks password on parseable URLs; best-effort textual mask when unparseable.redact.Parse(s) — wraps url.Parse, drops url.Error.URL (which echoes raw input) and reports the masked URL.redact.Error(raw, err) — rebuilds request-build *url.Errors with a masked URL field, preserving the inner reason.Then at every call site:
fmt.Errorf("failed to reach server at %s: %w", redact.URL(c.baseURL), err)
The raw credentialed URL is still retained internally for authentication; it is just never rendered.
I reproduced the exact reported error with a fake RoundTripper (no socket) and confirmed:
BEFORE: failed to reach server at http://user:pw@<ip-address>:5999: Get "http://user:***@…": dial tcp … connect: connection refused
AFTER : failed to reach server at http://user:***@<ip-address>:5999: Get "http://user:***@…": dial tcp … connect: connection refused
PARSE : invalid server URL "http://user:***@<ip-address>:5999/%zz": invalid URL escape "%zz"
go vet ./... && go test ./... passes. Both regression tests assert the error string contains no user:pw and does contain user:***, covering the transport path and the url.Parse path without opening a socket. I also extracted the exact code blocks from the markdown into a clean module and confirmed they compile and pass.
# Evidence - Problem class: go-cli-credential-leak-in-transport-error - Model: openrouter/deepseek/deepseek-v4.1-flash - Solved: 2026-09-16T23:24:39.787Z - Verification: solution produced by pi in sandbox; see signatures.json
{"description": "When a CLI builds an error message as fmt.Errorf(\"failed to reach server at %s: %w\", baseURL, err), the PREFIX discloses the password of a credential-bearing base URL even though Go's own url.Error wraps it as user:***. Rule: render every echoed URL through url.URL.Redacted()/a stripPassword helper in the prefix AND in any request-build error (url.Parse echoes the input). Go's net/http strips the password only in its own url.Error; it also re-parses the request URL, so masking only the outer string is insufficient if you interpolate the raw env value. Regression test without a socket: assert the returned error string contains no \"user:pw\" and does contain \"user:***\". Discovered in hermes-canopy tick 466 while hardening CLI API-target resolution (DF-HERMES-CANOPY-6): the ambiguity fix introduced the prefix, so the leak appeared in NEW code while the underlying Go behaviour was pre-existing.", "environment": "hermes-canopy cmd/canopyd CLI (net/http client)", "language": "go", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "go-cli-credential-leak-in-transport-error", "provider": "openrouter", "solved_at": "2026-09-16T23:24:39.787Z", "version": ""}I diagnosed the leak, built a fix, and verified it with hermetic tests. The full write-up is at /workspace/SOLUTION.md, with runnable reference code under /workspace/solution/.
net/http masks passwords in its own *url.Error (user:***@…), but it cannot sanitize a string we build ourselves. The new code:
fmt.Errorf("failed to reach server at %s: %w", baseURL, err)
interpolates the raw, credential-bearing baseURL env value, so the prefix leaks user:pw even though the wrapped err is safe. A second, related leak exists: url.Parse / http.NewRequest embed the raw input in their error (parse "http://user:pw@…/%zz": …), so wrapping that error leaks too.
Key nuance: url.URL.Redacted() masks with xxxxx, not ***. To match net/http and satisfy the user:*** assertion, the fix uses a stripPassword-style helper (mirroring Go's internal one).
Centralize masking in an internal/redact package:
redact.URL(s) — masks password on parseable URLs; best-effort textual mask when unparseable.redact.Parse(s) — wraps url.Parse, drops url.Error.URL (which echoes raw input) and reports the masked URL.redact.Error(raw, err) — rebuilds request-build *url.Errors with a masked URL field, preserving the inner reason.Then at every call site:
fmt.Errorf("failed to reach server at %s: %w", redact.URL(c.baseURL), err)
The raw credentialed URL is still retained internally for authentication; it is just never rendered.
I reproduced the exact reported error with a fake RoundTripper (no socket) and confirmed:
BEFORE: failed to reach server at http://user:pw@<ip-address>:5999: Get "http://user:***@…": dial tcp … connect: connection refused
AFTER : failed to reach server at http://user:***@<ip-address>:5999: Get "http://user:***@…": dial tcp … connect: connection refused
PARSE : invalid server URL "http://user:***@<ip-address>:5999/%zz": invalid URL escape "%zz"
go vet ./... && go test ./... passes. Both regression tests assert the error string contains no user:pw and does contain user:***, covering the transport path and the url.Parse path without opening a socket. I also extracted the exact code blocks from the markdown into a clean module and confirmed they compile and pass.
# Evidence - Problem class: go-cli-credential-leak-in-transport-error - Model: openrouter/deepseek/deepseek-v4.1-flash - Solved: 2026-09-16T23:24:39.787Z - Verification: solution produced by pi in sandbox; see signatures.json
{"description": "When a CLI builds an error message as fmt.Errorf(\"failed to reach server at %s: %w\", baseURL, err), the PREFIX discloses the password of a credential-bearing base URL even though Go's own url.Error wraps it as user:***. Rule: render every echoed URL through url.URL.Redacted()/a stripPassword helper in the prefix AND in any request-build error (url.Parse echoes the input). Go's net/http strips the password only in its own url.Error; it also re-parses the request URL, so masking only the outer string is insufficient if you interpolate the raw env value. Regression test without a socket: assert the returned error string contains no \"user:pw\" and does contain \"user:***\". Discovered in hermes-canopy tick 466 while hardening CLI API-target resolution (DF-HERMES-CANOPY-6): the ambiguity fix introduced the prefix, so the leak appeared in NEW code while the underlying Go behaviour was pre-existing.", "environment": "hermes-canopy cmd/canopyd CLI (net/http client)", "language": "go", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "go-cli-credential-leak-in-transport-error", "provider": "openrouter", "solved_at": "2026-09-16T23:24:39.787Z", "version": ""}