Skip to content

Commit 2e926c9

Browse files
committed
fix(secrets): fall back to passthrough on unknown scheme
A literal value with a colon before any '/' or space (e.g. a Postgres password 'p@ss:w0rd') was parsed as the scheme 'p@ss', matched no backend, and errored instead of resolving. Now an unclaimed scheme falls through to the passthrough backend; the error only fires when no passthrough exists. This also aligns with 'profile show' masking, which treats the same value as a literal. Regression tests added. Also narrows the .claude/settings.json secrets deny-glob from ./secrets/** to /secrets/** so it protects a root ./secrets/ credentials dir without blocking the internal/secrets/ Go source package.
1 parent 930885e commit 2e926c9

3 files changed

Lines changed: 43 additions & 4 deletions

File tree

.claude/settings.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,14 +36,14 @@
3636
"deny": [
3737
"Read(./.env)",
3838
"Read(./.env.*)",
39-
"Read(./secrets/**)",
39+
"Read(/secrets/**)",
4040
"Read(./config/credentials.json)",
4141
"Read(./**/*.key)",
4242
"Read(./**/*.pem)",
4343
"Read(./**/*.crt)",
4444
"Write(./.env)",
4545
"Write(./.env.*)",
46-
"Write(./secrets/**)",
46+
"Write(/secrets/**)",
4747
"Write(./config/credentials.json)",
4848
"Write(./**/*.key)",
4949
"Write(./**/*.pem)",

internal/secrets/resolver.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,15 @@ func (r *Resolver) Resolve(ref string) (string, error) {
4242
return b.Resolve(ref)
4343
}
4444
}
45+
// No backend claims this scheme. Fall back to the passthrough ("") backend
46+
// rather than erroring: a literal value can legitimately contain a colon
47+
// before any "/" or space (e.g. a Postgres password "p@ss:w0rd"), which
48+
// parseScheme would otherwise read as the unknown scheme "p@ss".
49+
for _, b := range r.backends {
50+
if b.Scheme() == "" {
51+
return b.Resolve(ref)
52+
}
53+
}
4554
return "", &errs.Error{
4655
Op: "secrets.resolve",
4756
Code: errs.CodeUser,

internal/secrets/resolver_test.go

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,40 @@ func TestResolver_EnvMissing_ReturnsErrSecretUnresolved(t *testing.T) {
3838
}
3939
}
4040

41-
func TestResolver_UnknownScheme_ErrorMentionsBackends(t *testing.T) {
41+
// With a passthrough backend present, an unknown scheme falls back to the
42+
// literal value rather than erroring — there's no way to tell "meant the vault
43+
// backend" from "literal value that looks like vault:foo", so passthrough wins.
44+
func TestResolver_UnknownScheme_FallsBackToPassthrough(t *testing.T) {
4245
r := NewResolver(Passthrough{}, Env{})
46+
got, err := r.Resolve("vault:foo")
47+
if err != nil {
48+
t.Fatalf("unexpected error: %v", err)
49+
}
50+
if got != "vault:foo" {
51+
t.Fatalf("got %q; want literal 'vault:foo' via passthrough", got)
52+
}
53+
}
54+
55+
// Without a passthrough backend, an unknown scheme still errors (and the error
56+
// names the available backends).
57+
func TestResolver_UnknownScheme_NoPassthrough_Errors(t *testing.T) {
58+
r := NewResolver(Env{})
4359
_, err := r.Resolve("vault:foo")
4460
if err == nil {
45-
t.Fatal("expected error for unknown scheme")
61+
t.Fatal("expected error for unknown scheme when no passthrough backend")
62+
}
63+
}
64+
65+
// TestResolver_LiteralWithColon is the regression: a literal password
66+
// containing a colon (valid in Postgres) must resolve as-is, not be misread as
67+
// the unknown scheme before the colon.
68+
func TestResolver_LiteralWithColon(t *testing.T) {
69+
r := NewResolver(Env{}, Passthrough{})
70+
got, err := r.Resolve("p@ss:w0rd")
71+
if err != nil {
72+
t.Fatalf("literal with colon errored: %v", err)
73+
}
74+
if got != "p@ss:w0rd" {
75+
t.Fatalf("got %q; want literal 'p@ss:w0rd'", got)
4676
}
4777
}

0 commit comments

Comments
 (0)