Skip to content

fix(security): mask API keys in log output - #1579

Open
hdot123 wants to merge 1 commit into
looplj:unstablefrom
hdot123:pr/apikey-masking
Open

fix(security): mask API keys in log output#1579
hdot123 wants to merge 1 commit into
looplj:unstablefrom
hdot123:pr/apikey-masking

Conversation

@hdot123

@hdot123 hdot123 commented May 1, 2026

Copy link
Copy Markdown

Problem

API keys are logged in plaintext via slog (e.g. slog.Any("request", request) in llm/httpclient/client.go:187), exposing credentials in application logs.

Fix

Add MarshalLog and LogValue (implementing slog.LogValuer) to AuthConfig so that:

  • MarshalLog() returns the masked key string (sk-xxxx****xxxx) for explicit calls
  • LogValue() ensures automatic masking when AuthConfig is serialized by slog

The masked output preserves struct fields (Type, APIKey, HeaderKey) while masking only the sensitive APIKey value.

Also adds a local maskAPIKey helper function consistent with the existing pattern in internal/server/orchestrator/tester.go.

Scope

Single file: llm/httpclient/model.go

Add MarshalLog and LogValue (slog.LogValuer) to AuthConfig so slog
outputs API keys as sk-xxxx****xxxx instead of plaintext. Prevents
credential leakage in application logs.

Also adds a local maskAPIKey helper function consistent with the
existing pattern in internal/server/orchestrator/tester.go.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request implements API key masking for the AuthConfig struct by adding a LogValue method and a maskAPIKey helper function. Review feedback indicates that the Request struct should also implement LogValuer to ensure masking is applied when the parent object is logged. Additionally, the maskedAuthConfig struct should include JSON tags to maintain consistent field naming in log outputs.

Comment thread llm/httpclient/model.go
Comment on lines +83 to +89
func (a AuthConfig) LogValue() slog.Value {
return slog.AnyValue(maskedAuthConfig{
Type: a.Type,
APIKey: a.MarshalLog(),
HeaderKey: a.HeaderKey,
})
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-high high

The implementation of LogValue on AuthConfig is a good step, but it will not automatically mask the API key when the parent Request object is logged via slog.Any("request", request) (as seen in client.go:187).

Standard slog handlers (and the custom zap bridge in this repo) use reflection to log structs and do not recursively check for LogValuer implementations on nested fields. To fully address the security issue described in the PR, the Request struct should also implement slog.LogValuer to explicitly handle the masking of its Auth field.

Comment thread llm/httpclient/model.go
})
}

type maskedAuthConfig struct{ Type, APIKey, HeaderKey string }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The maskedAuthConfig struct should include JSON tags to ensure that the log output remains consistent with the original AuthConfig field names and behavior (e.g., using snake_case and omitempty). Without these tags, a JSON log handler will output capitalized field names.

type maskedAuthConfig struct {
	Type      string "json:\"type\"" 
	APIKey    string "json:\"api_key\"" 
	HeaderKey string "json:\"header_key,omitempty\"" 
}

@greptile-apps

greptile-apps Bot commented May 1, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds MarshalLog() and LogValue() (slog.LogValuer) to AuthConfig and a local maskAPIKey helper to prevent API keys from appearing in log output. However, LogValue() is only invoked by slog when AuthConfig is the direct value of a log attribute; the primary leakage site (slog.Any("request", request) in client.go:187) logs a *Request, and slog's JSON handler serializes it via json.Marshal, which ignores slog.LogValuer on nested fields — leaving the API key exposed.

  • P1LogValue() does not protect client.go:187; *Request or AuthConfig needs to implement json.Marshaler (or Request needs LogValuer) to cover the nested case.
  • maskedAuthConfig is missing JSON struct tags, so slog's JSON handler will output APIKey/Type/HeaderKey (PascalCase) instead of the original api_key/type/header_key keys.

Confidence Score: 4/5

Not safe to merge as-is — the primary credential-leakage path in client.go:187 is unaddressed.

There is one P1 finding: the LogValue() implementation does not protect the main leakage site (slog.Any("request", request)) because slog's JSON handler uses json.Marshal for nested struct serialisation, which does not respect slog.LogValuer. The PR adds partial protection for direct attribute logging, but the stated goal of masking the key at client.go:187 is not achieved.

llm/httpclient/model.go — LogValue() and maskedAuthConfig both need revision; client.go:187 also needs attention.

Security Review

  • Credential leakage (unfixed)llm/httpclient/client.go:187: slog.Any("request", request) still exposes the raw API key. The new LogValue() on AuthConfig is bypassed when the struct is nested inside *Request and serialised by json.Marshal. The leakage path described in the PR description remains open at debug log level.

Important Files Changed

Filename Overview
llm/httpclient/model.go Adds MarshalLog(), LogValue(), maskedAuthConfig, and maskAPIKey to AuthConfig — but LogValue() only fires when AuthConfig is a direct slog attribute, not when nested inside *Request (the primary leakage site in client.go:187); maskedAuthConfig also lacks json struct tags.

Sequence Diagram

sequenceDiagram
    participant Caller
    participant slog
    participant JSONHandler
    participant json_Marshal as json.Marshal

    Note over Caller,json_Marshal: Current path — LogValue is NOT invoked
    Caller->>slog: slog.Any("request", ptr to Request)
    slog->>slog: AnyValue → KindAny, not LogValuer
    slog->>JSONHandler: handle KindAny value
    JSONHandler->>json_Marshal: json.Marshal(Request struct)
    json_Marshal-->>JSONHandler: api_key field serialised via struct tags — key exposed

    Note over Caller,json_Marshal: Direct attribute path — LogValue IS invoked
    Caller->>slog: slog.Any("auth", AuthConfig value)
    slog->>slog: AnyValue → KindLogValuer
    slog->>slog: Resolve calls LogValue()
    slog-->>JSONHandler: maskedAuthConfig with APIKey masked
    JSONHandler-->>Caller: masked output
Loading

Reviews (1): Last reviewed commit: "fix(security): mask API keys in log outp..." | Re-trigger Greptile

Comment thread llm/httpclient/model.go
Comment on lines +83 to +89
func (a AuthConfig) LogValue() slog.Value {
return slog.AnyValue(maskedAuthConfig{
Type: a.Type,
APIKey: a.MarshalLog(),
HeaderKey: a.HeaderKey,
})
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 security LogValue() does not protect the primary leakage path

The PR's stated goal is to fix slog.Any("request", request) in client.go:187, but LogValue() is not invoked there. When slog's handler encounters a KindAny value it cannot resolve as a LogValuer at the attribute level, it falls back to json.Marshal (for JSONHandler) or fmt.Sprintf (for TextHandler). Neither serializer is aware of slog.LogValuer, so *Request is serialized field-by-field via struct tags, and Auth *AuthConfig ends up as {"api_key":"sk-actual-key"} in the log — unchanged.

LogValue() is only invoked when AuthConfig/*AuthConfig is the direct value of a slog attribute (e.g., slog.Any("auth", config)), not when it is a nested field inside another struct.

To protect client.go:187, one of the following is needed:

  • Have Request implement slog.LogValuer and return a safe representation.
  • Have AuthConfig implement json.Marshaler so that json.Marshal masks the key when traversing *Request.
  • Replace slog.Any("request", request) with individual safe attribute calls.

Comment thread llm/httpclient/model.go
})
}

type maskedAuthConfig struct{ Type, APIKey, HeaderKey string }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 maskedAuthConfig missing JSON struct tags

maskedAuthConfig has no json struct tags, so when slog's JSON handler serializes it (via json.Marshal), the output uses Go's exported field names — Type, APIKey, HeaderKey — instead of the snake_case tags (type, api_key, header_key) used by the original AuthConfig. Any log-parsing tooling or dashboards that match on the original key names will not find the masked values.

Suggested change
type maskedAuthConfig struct{ Type, APIKey, HeaderKey string }
type maskedAuthConfig struct {
Type string `json:"type"`
APIKey string `json:"api_key"`
HeaderKey string `json:"header_key,omitempty"`
}

Comment thread llm/httpclient/model.go
Comment on lines +148 to +155
// maskAPIKey returns a masked version of the API key for display.
// Shows first 4 and last 4 characters with **** in between.
func maskAPIKey(key string) string {
if len(key) <= 8 {
return "****"
}
return key[:4] + "****" + key[len(key)-4:]
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Duplicate maskAPIKey helper

An identical maskAPIKey function already exists in internal/server/orchestrator/tester.go (lines 549–556). Duplicating it here means any future change to the masking logic must be applied in two places. Consider extracting it to a shared internal utility package.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant