From 82c975885db008fbd5a44f142819023ff1a69607 Mon Sep 17 00:00:00 2001 From: Lakshman Patel Date: Fri, 17 Jul 2026 08:25:36 +0530 Subject: [PATCH 1/3] fix(eyrie): Bedrock secret masking, streaming context cancel, HTTP header injection fix, SQLite perms SECURITY FIXES: - Change secretAccessKey from string to []byte to prevent accidental logging - Add String() method to BedrockClient that masks secrets - Add context cancellation for Bedrock streaming EventStreamReader BUG FIXES: - Fix storage/sqlite.go chmod failure on :memory: databases SECURITY HARDENING: - Extend control character rejection in custom headers from \r\n to all chars < 0x20 - Add circuit breaker field to Router for future use --- client/adapters/bedrock.go | 36 ++++++++++++++++++++++++++++++------ client/client.go | 3 ++- router/router.go | 17 +++++++++-------- storage/sqlite.go | 9 +++++++++ 4 files changed, 50 insertions(+), 15 deletions(-) diff --git a/client/adapters/bedrock.go b/client/adapters/bedrock.go index 70ab7ac..d5a3099 100644 --- a/client/adapters/bedrock.go +++ b/client/adapters/bedrock.go @@ -28,7 +28,7 @@ const ( type BedrockClient struct { accessKeyID string - secretAccessKey string + secretAccessKey []byte sessionToken string region string httpClient *http.Client @@ -37,12 +37,22 @@ type BedrockClient struct { guardrails *core.Guardrails } +// String returns a safe string representation of the Bedrock client. +// Sensitive fields (secret access key) are masked to prevent accidental exposure in logs. +func (c *BedrockClient) String() string { + masked := "****" + if len(c.secretAccessKey) > 4 { + masked = string(c.secretAccessKey[:4]) + "****" + } + return fmt.Sprintf("BedrockClient{access_key_id=%s, region=%s, secret_access_key=%s}", c.accessKeyID, c.region, masked) +} + var _ core.Provider = (*BedrockClient)(nil) func NewBedrockClient(accessKeyID, secretAccessKey, sessionToken, region string) *BedrockClient { return &BedrockClient{ accessKeyID: accessKeyID, - secretAccessKey: secretAccessKey, + secretAccessKey: []byte(secretAccessKey), sessionToken: sessionToken, region: region, httpClient: core.NewPooledHTTPClient(core.DefaultTimeout), @@ -163,7 +173,21 @@ func (c *BedrockClient) StreamChat(ctx context.Context, messages []core.EyrieMes // Bedrock uses Amazon EventStream format. Parse it. reader := newEventStreamReader(resp.Body) for { - event, err := reader.ReadEvent() + event, err := func() (*eventStreamFrame, error) { + var result *eventStreamFrame + var readErr error + done := make(chan struct{}) + go func() { + result, readErr = reader.ReadEvent() + close(done) + }() + select { + case <-done: + return result, readErr + case <-streamCtx.Done(): + return nil, streamCtx.Err() + } + }() if err != nil { if err != io.EOF { select { @@ -235,7 +259,7 @@ func (c *BedrockClient) StreamChat(ctx context.Context, messages []core.EyrieMes } func (c *BedrockClient) Ping(ctx context.Context) error { - if c.region == "" || c.accessKeyID == "" || c.secretAccessKey == "" { + if c.region == "" || c.accessKeyID == "" || len(c.secretAccessKey) == 0 { return fmt.Errorf("eyrie: bedrock credentials are incomplete") } req, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("https://bedrock.%s.amazonaws.com/foundation-models", c.region), nil) @@ -432,7 +456,7 @@ func (es *eventStreamReader) ReadEvent() (*eventStreamFrame, error) { } func (c *BedrockClient) sign(req *http.Request, body []byte, now time.Time) error { - if c.region == "" || c.accessKeyID == "" || c.secretAccessKey == "" { + if c.region == "" || c.accessKeyID == "" || len(c.secretAccessKey) == 0 { return fmt.Errorf("eyrie: bedrock credentials are incomplete") } service := "bedrock" @@ -464,7 +488,7 @@ func (c *BedrockClient) sign(req *http.Request, body []byte, now time.Time) erro scope, sha256Hex([]byte(canonicalRequest)), }, "\n") - signingKey := awsSigningKey(c.secretAccessKey, dateStamp, c.region, service) + signingKey := awsSigningKey(string(c.secretAccessKey), dateStamp, c.region, service) signature := hex.EncodeToString(hmacSHA256(signingKey, stringToSign)) req.Header.Set("Authorization", fmt.Sprintf("AWS4-HMAC-SHA256 Credential=%s/%s, SignedHeaders=%s, Signature=%s", c.accessKeyID, scope, signedHeaders, signature)) return nil diff --git a/client/client.go b/client/client.go index b862153..d8b8043 100644 --- a/client/client.go +++ b/client/client.go @@ -112,7 +112,8 @@ func ParseCustomHeaders() map[string]string { name := strings.TrimSpace(line[:idx]) value := strings.TrimSpace(line[idx+1:]) // Reject header names/values containing control characters to prevent injection. - if strings.ContainsAny(name, "\r\n") || strings.ContainsAny(value, "\r\n") { + if strings.IndexFunc(name, func(r rune) bool { return r < 0x20 }) >= 0 || + strings.IndexFunc(value, func(r rune) bool { return r < 0x20 }) >= 0 { continue } result[name] = value diff --git a/router/router.go b/router/router.go index 84404e7..b27fb4f 100644 --- a/router/router.go +++ b/router/router.go @@ -30,14 +30,15 @@ func (e RouteEntry) cost() int { } type Router struct { - entries []RouteEntry - fallback []client.Provider - totalWeight int - defaultRetry RetryConfig - strategy Strategy - stratState *strategyState - mu sync.RWMutex - stats map[string]*atomic.Int64 + entries []RouteEntry + fallback []client.Provider + totalWeight int + defaultRetry RetryConfig + strategy Strategy + stratState *strategyState + breakers map[string]*CircuitBreaker + mu sync.RWMutex + stats map[string]*atomic.Int64 } // Option configures a Router at construction time. diff --git a/storage/sqlite.go b/storage/sqlite.go index dc96192..ac50901 100644 --- a/storage/sqlite.go +++ b/storage/sqlite.go @@ -5,6 +5,7 @@ import ( "database/sql" "encoding/json" "fmt" + "os" "strings" "time" @@ -54,6 +55,14 @@ func Open(path string, opts ...Option) (*SQLiteStore, error) { _ = db.Close() return nil, err } + // Restrict SQLite database file permissions to owner-only read/write. + // Skip for in-memory databases (path == ":memory:") which have no real file. + if path != ":memory:" { + if err := os.Chmod(path, 0o600); err != nil { + _ = db.Close() + return nil, fmt.Errorf("storage: chmod %s: %w", path, err) + } + } return s, nil } From 22ae3d116111748caa18ee44d96f5b2043f444ae Mon Sep 17 00:00:00 2001 From: Lakshman Patel Date: Fri, 17 Jul 2026 12:39:42 +0530 Subject: [PATCH 2/3] fix: gofmt formatting --- router/router.go | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/router/router.go b/router/router.go index b27fb4f..2ea3601 100644 --- a/router/router.go +++ b/router/router.go @@ -30,15 +30,15 @@ func (e RouteEntry) cost() int { } type Router struct { - entries []RouteEntry - fallback []client.Provider - totalWeight int - defaultRetry RetryConfig - strategy Strategy - stratState *strategyState - breakers map[string]*CircuitBreaker - mu sync.RWMutex - stats map[string]*atomic.Int64 + entries []RouteEntry + fallback []client.Provider + totalWeight int + defaultRetry RetryConfig + strategy Strategy + stratState *strategyState + breakers map[string]*CircuitBreaker + mu sync.RWMutex + stats map[string]*atomic.Int64 } // Option configures a Router at construction time. From 37526c2af5d708349d7676e7fdc91876883c55ef Mon Sep 17 00:00:00 2001 From: Lakshman Patel Date: Fri, 17 Jul 2026 12:44:36 +0530 Subject: [PATCH 3/3] fix: remove unused breakers field --- router/router.go | 1 - 1 file changed, 1 deletion(-) diff --git a/router/router.go b/router/router.go index 2ea3601..84404e7 100644 --- a/router/router.go +++ b/router/router.go @@ -36,7 +36,6 @@ type Router struct { defaultRetry RetryConfig strategy Strategy stratState *strategyState - breakers map[string]*CircuitBreaker mu sync.RWMutex stats map[string]*atomic.Int64 }