Reject API keys that are not valid header values at construction
Independent review Codex gpt-6-sol high: APPROVED (0 findings)
CI / ci (push) Successful in 1m30s
CI / ci (pull_request) Successful in 1m29s

FromSettings now fails with llm.ErrInvalidKey for a key with a control
character such as CR or LF, for every preset that sends a key, and
openai.New rejects such a key too, so neither defers the failure to the
first request. anthropic.New already rejected it. No error quotes the key.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-04 09:08:46 -04:00
co-authored by Claude Opus 5.5
parent 31322f239e
commit 08be8891da
5 changed files with 42 additions and 2 deletions
+4
View File
@@ -53,6 +53,7 @@ type Config struct {
BaseURL string
// APIKey is sent as "Authorization: Bearer <APIKey>". Empty sends no
// Authorization header, for endpoints such as Ollama that need none.
// It must not have a control character other than tab.
APIKey string
// Model is the model Generate requests. ListModels and Verify do not
// need it.
@@ -102,6 +103,9 @@ func newFromConfig(cfg Config) (*Provider, error) {
if u.User != nil || strings.ContainsAny(cfg.BaseURL, "?#") {
return nil, errors.New("openai: base URL must not carry credentials, a query or a fragment")
}
if !validHeaderValue(cfg.APIKey) {
return nil, errors.New("openai: API key has a control character")
}
// No error from New quotes caller-supplied text or a name derived from
// it: any such text may equal or contain the API key, and redaction
// cannot catch a fragment or a case-changed copy of it.
+14
View File
@@ -628,6 +628,20 @@ func TestNewRejectsBadHeaderWithoutEchoingValue(t *testing.T) {
assertNoSecret(t, err)
}
func TestNewRejectsKeyWithControlCharacter(t *testing.T) {
for _, key := range []string{testKey + "\n", testKey + "\r\nX-Injected: 1", "\x00" + testKey, testKey + "\x7f"} {
_, err := New(Config{BaseURL: "https://example.com/v1", APIKey: key, StructuredOutput: JSONSchema})
if err == nil {
t.Errorf("New(%q) = nil error", key)
continue
}
assertNoSecret(t, err)
if strings.Contains(err.Error()+fmt.Sprintf("%#v", err), key) {
t.Errorf("error leaks the key: %v", err)
}
}
}
// TestNewConfigErrorsNeverLeakKey covers every New error that names
// caller-supplied text, each with the API key embedded in that text.
func TestNewConfigErrorsNeverLeakKey(t *testing.T) {