fix: harden read-only target diagnostics

This commit is contained in:
proxyctl coordinator
2026-07-17 10:42:48 +08:00
parent 0fbaa71e37
commit a5360028ca
8 changed files with 385 additions and 33 deletions
+17 -1
View File
@@ -535,7 +535,7 @@ func (a *Adapter) readSettings(ctx context.Context) (settings, error) {
if result.HTTPAuthUser, err = a.getString(ctx, httpSchema, "authentication-user"); err != nil {
return settings{}, err
}
if result.HTTPAuthPassword, err = a.getString(ctx, httpSchema, "authentication-password"); err != nil {
if result.HTTPAuthPassword, err = a.getSensitiveString(ctx, httpSchema, "authentication-password"); err != nil {
return settings{}, err
}
if result.HTTPSHost, err = a.getString(ctx, httpsSchema, "host"); err != nil {
@@ -560,6 +560,22 @@ func (a *Adapter) getString(ctx context.Context, schema, key string) (string, er
}
return parseString(raw)
}
// getSensitiveString deliberately suppresses both command error detail and raw
// output. A password value is secret even when malformed, and command runners
// are not trusted to keep that value out of their errors.
func (a *Adapter) getSensitiveString(ctx context.Context, schema, key string) (string, error) {
raw, err := a.run(ctx, "get", schema, key)
if err != nil {
return "", safeRedaction(fmt.Sprintf("get %s %s failed; sensitive output suppressed", schema, key), err)
}
value, err := parseString(strings.TrimSpace(raw))
if err != nil {
return "", errors.New("parse " + schema + " " + key + " failed; sensitive output suppressed")
}
return value, nil
}
func (a *Adapter) getInt(ctx context.Context, schema, key string) (int, error) {
raw, err := a.get(ctx, schema, key)
if err != nil {
+43
View File
@@ -108,6 +108,49 @@ func active() *profile.Profile {
return &profile.Profile{Name: "office", Mode: profile.AdvancedMode, HTTPURL: "http://proxy.example:8080", HTTPSURL: "http://secure.example:8443", AllURL: "socks5://socks.example:1080", Bypass: []string{"localhost", ".example.com", "10.0.0.0/8", "2001:db8::1"}}
}
func TestDiagnosticsSuppressSensitiveGSettingsOutputAndRunnerErrors(t *testing.T) {
const secret = "GNOME-DIAGNOSTIC-SECRET-7f3a"
for _, test := range []struct {
name string
value string
fail bool
}{
{name: "malformed value", value: "unterminated-" + secret},
{name: "runner error", value: secret, fail: true},
} {
t.Run(test.name, func(t *testing.T) {
r := newRunner()
key := "org.gnome.system.proxy.http authentication-password"
r.values[key] = test.value
if test.fail {
r.fail = func(c gnome.Command) error {
if reflect.DeepEqual(c.Args, []string{"get", "org.gnome.system.proxy.http", "authentication-password"}) {
return errors.New("runner exposed " + secret)
}
return nil
}
}
a := adapter(r, &memoryState{})
checks := a.DiagnosticChecks(context.Background(), active())
for _, check := range checks {
if strings.Contains(check.Evidence, secret) {
t.Fatalf("diagnostic evidence exposed password: %#v", checks)
}
}
result := a.SetDesired(context.Background(), true, active())
operationError := ""
if result.Err != nil {
operationError = result.Err.Error()
}
for _, evidence := range []string{a.Availability(context.Background()).Evidence, a.Inspect(context.Background(), active()).Evidence, operationError} {
if strings.Contains(evidence, secret) {
t.Fatalf("Target evidence or loggable operation error exposed password: %q", evidence)
}
}
})
}
}
func TestDiagnosticChecksCoverCommandSchemasSessionAndAuthorityWithoutMutation(t *testing.T) {
r, state := newRunner(), &memoryState{}
a := adapter(r, state)