MM-67547: Sanitize Bifrost provider errors before log and user surfaces#579
MM-67547: Sanitize Bifrost provider errors before log and user surfaces#579nickmisasi wants to merge 7 commits intomasterfrom
Conversation
Redact API keys and tokens from Bifrost error messages using shared llm sanitization (ported from mattermost-plugin-agents OpenAI path). Made-with: Cursor
🤖 LLM Evaluation ResultsOpenAI
❌ Failed EvaluationsShow 1 failuresOPENAI1. TestReactEval/[openai]_react_cat_message
Anthropic
❌ Failed EvaluationsShow 2 failuresANTHROPIC1. TestReactEval/[anthropic]_react_cat_message
2. TestThreadsSummarizeFromExportedData/[anthropic]_thread_summarization_from_eval_timed_dnd.json
This comment was automatically generated by the eval CI pipeline. |
There was a problem hiding this comment.
Stale comment
Security Review: No Issues Found
This PR adds proper redaction of API keys and secrets from Bifrost provider error messages across all error surfaces (streaming, embeddings, transcription, model listing). The implementation is sound:
- Regex patterns cover the major provider key formats (OpenAI, Anthropic, bearer tokens, JSON fields), plus a configured-key fallback.
regexp.QuoteMetaprevents regex injection from the configured key, and Go's RE2 engine prevents ReDoS.- All downstream code paths that consume these errors use
.Error(), which returns the sanitized message.- The
Unwrap()chain preserves the original error forerrors.Is/errors.Ascompatibility — standard Go practice. No current code path traverses the error chain in a way that would expose unsanitized content.No medium, high, or critical vulnerabilities identified.
Sent by Cursor Automation: Find vulnerabilities
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository UI (base), Organization UI (inherited) Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (4)
🚧 Files skipped from review as they are similar to previous changes (3)
📝 WalkthroughWalkthroughThe PR adds secret-redaction utilities for LLM provider errors. Across bifrost components (LLM, EmbeddingProvider, Transcriber), new Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@bifrost/bifrost.go`:
- Around line 36-37: The LLM struct currently stores only apiKey causing AWS
creds to be omitted from redaction; add a field like redactionSecrets []string
on the LLM/bifrost struct, populate it from cfg.APIKey, cfg.AWSAccessKeyID and
cfg.AWSSecretAccessKey when constructing the struct, and remove the single
apiKey-only usage, then update llm.SanitizeProviderError to accept variadic
secrets (...string) and change all call sites to pass b.redactionSecrets...
(e.g., where SanitizeProviderError(...) is invoked) so Bedrock/AWS credential
values are included in the redaction set.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI (base), Organization UI (inherited)
Review profile: CHILL
Plan: Pro
Run ID: b8def679-242d-4b6f-b680-6d6ea6632590
📒 Files selected for processing (6)
bifrost/bifrost.gobifrost/embeddings.gobifrost/models.gobifrost/transcription.gollm/provider_error.gollm/provider_error_test.go
| apiKey string // used only to redact configured secrets from provider error surfaces | ||
| defaultModel string |
There was a problem hiding this comment.
Don't drop Bedrock credentials from the redaction set.
LLM now retains only cfg.APIKey, but this config also accepts AWSAccessKeyID and AWSSecretAccessKey. Those never reach llm.SanitizeProviderError, so a Bedrock auth error that echoes either value will still leak it through the new streamed error paths. Please carry a full secret list here instead of a single API key.
🔐 Suggested direction
type LLM struct {
client *bifrostcore.Bifrost
provider schemas.ModelProvider
- apiKey string // used only to redact configured secrets from provider error surfaces
+ redactionSecrets []string
defaultModel string
inputTokenLimit int
outputTokenLimit int
streamingTimeout time.Duration
@@
return &LLM{
client: client,
provider: cfg.Provider,
- apiKey: cfg.APIKey,
+ redactionSecrets: []string{
+ cfg.APIKey,
+ cfg.AWSAccessKeyID,
+ cfg.AWSSecretAccessKey,
+ },
defaultModel: cfg.DefaultModel,
inputTokenLimit: cfg.InputTokenLimit,Then widen the llm.SanitizeProviderError(...) helpers to accept ...string and pass b.redactionSecrets... at the call sites.
Also applies to: 200-200
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@bifrost/bifrost.go` around lines 36 - 37, The LLM struct currently stores
only apiKey causing AWS creds to be omitted from redaction; add a field like
redactionSecrets []string on the LLM/bifrost struct, populate it from
cfg.APIKey, cfg.AWSAccessKeyID and cfg.AWSSecretAccessKey when constructing the
struct, and remove the single apiKey-only usage, then update
llm.SanitizeProviderError to accept variadic secrets (...string) and change all
call sites to pass b.redactionSecrets... (e.g., where SanitizeProviderError(...)
is invoked) so Bedrock/AWS credential values are included in the redaction set.
|
/update-branch |
There was a problem hiding this comment.
Stale comment
Security Review: No Issues Found
This PR adds proper redaction of API keys and secrets from Bifrost provider error messages across all error surfaces (streaming chat/responses, embeddings, transcription, model listing). The implementation is sound:
- Regex injection safe:
regexp.QuoteMetais correctly used on the configured API key before building the replacement regex, preventing attacker-controlled keys from injecting regex patterns. Go's RE2 engine also prevents ReDoS.- All Bifrost error paths covered: Every
bifrostErr.Error.Messagesite is now wrapped withSanitizeProviderError.Unwrap()chain is safe: TheSanitizedProviderError.Unwrap()returns the original unsanitized error, which is standard Go practice forerrors.Is/errors.Ascompatibility. I verified that no downstream code in the repository traverses the error chain to extract.Error()from the unwrapped original — all consumers (streaming/streaming.goL465,llm/stream.goL84,api/api_llm_bridge.goL227) call.Error()on the top-level wrapper, which returns the sanitized message.- Pattern coverage: Regex patterns cover OpenAI keys (
sk-*,sk-proj-*), Anthropic keys (sk-ant-*), Bearer authorization headers, JSONapiKeyfields, and the "Incorrect API key provided" message format. The configured key fallback catch-all handles provider-specific key formats not matched by the static patterns.- Stored
apiKeyfield: The field is unexported (lowercase), not serialized, and only used for redaction comparisons — no new exposure surface.No medium, high, or critical vulnerabilities identified.
Sent by Cursor Automation: Find vulnerabilities
The test helper documented this env var but always connected to localhost:5432. Parse the root DSN with net/url so temporary test databases use the same host and port as CI (PG_ROOT_DSN) and non-default local Postgres setups. Made-with: Cursor
Made-with: Cursor
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
webapp/src/i18n/en.json (1)
192-192: Use a placeholder for the default idle timeout to maintain consistency with the source of truth.Line 192 hardcodes
Default: 30 minutes, which can drift from the actual default inwebapp/src/components/system_console/config.tsx(currentlyidleTimeoutMinutes: 30). The codebase already uses placeholder patterns for similar dynamic values (e.g.,{maxTokens},{defaultBudget}), making this refactor consistent with existing patterns.♻️ Proposed i18n change
- "wJvTYUY5": "How long to keep an inactive user connection open before closing it automatically. Lower values save resources, higher values improve response times. Default: 30 minutes", + "wJvTYUY5": "How long to keep an inactive user connection open before closing it automatically. Lower values save resources, higher values improve response times. Default: {defaultMinutes} minutes",🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@webapp/src/i18n/en.json` at line 192, Replace the hardcoded "Default: 30 minutes" text for i18n key "wJvTYUY5" with a placeholder that references the canonical default from the system console config (idleTimeoutMinutes in webapp/src/components/system_console/config.tsx); update the value to use a placeholder like {idleTimeoutMinutes} (matching existing placeholder patterns such as {maxTokens}) so the displayed default is driven by the source of truth rather than a hardcoded literal.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@postgres/pgvector_test.go`:
- Around line 39-46: The testDatabaseDSN function currently uses
url.Parse(rootDSN) but doesn't detect key/value DSNs, so when rootDSN is in
"key=value" form url.Parse treats it as a path and rewriting u.Path produces an
invalid DSN; update testDatabaseDSN to detect whether parsed URL has a non-empty
Scheme and Host (or alternately check for presence of '=' indicating key/value
form) and if it's a key/value DSN, return a properly formed key/value string
that sets/overrides dbname=<dbName> (preserving other key/value pairs) instead
of manipulating u.Path; if it is a URL DSN, continue to set u.Path = "/"+dbName
and return u.String().
In `@webapp/src/i18n/en.json`:
- Line 10: The Spanish locale file (webapp/src/i18n/es.json) has diverged from
the English source (webapp/src/i18n/en.json) and is missing ~170 keys (including
the new "/cP+FWmj" key) and contains ~49 stale keys; synchronize the two by
diffing en.json against es.json, adding every missing key from en.json (e.g.,
"/cP+FWmj": "Use multiple AI bots on qualifying Mattermost plans") into es.json
with either existing Spanish translations or placeholder values for translators,
and remove keys present only in es.json that no longer exist in en.json;
optionally run or add your project's i18n sync script/tooling after the manual
update and verify there are no JSON syntax errors and that translation fallback
behavior is as expected.
---
Nitpick comments:
In `@webapp/src/i18n/en.json`:
- Line 192: Replace the hardcoded "Default: 30 minutes" text for i18n key
"wJvTYUY5" with a placeholder that references the canonical default from the
system console config (idleTimeoutMinutes in
webapp/src/components/system_console/config.tsx); update the value to use a
placeholder like {idleTimeoutMinutes} (matching existing placeholder patterns
such as {maxTokens}) so the displayed default is driven by the source of truth
rather than a hardcoded literal.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI (base), Organization UI (inherited)
Review profile: CHILL
Plan: Pro
Run ID: 6d79a89f-2a55-49c9-b06a-a8d0d60fa988
📒 Files selected for processing (2)
postgres/pgvector_test.gowebapp/src/i18n/en.json
There was a problem hiding this comment.
Stale comment
Security Review: No Issues Found
This PR adds sanitization of API keys and secrets from Bifrost provider error messages before they reach logs, streaming clients, or API responses. I verified the following:
- All Bifrost error surfaces covered:
streamChat,streamResponses,CreateEmbedding,BatchCreateEmbeddings,Transcribe, andFetchModelsall wrap errors throughSanitizeProviderError.- Regex injection safe:
regexp.QuoteMetais correctly applied to the configured API key before building the replacement regex. Go's RE2 engine prevents ReDoS.- Pattern coverage: Static regexes handle OpenAI keys (
sk-*,sk-proj-*), Anthropic keys (sk-ant-*), Bearer authorization headers, JSONapiKey/api_keyfields, and the "Incorrect API key provided" message format. The configured-key fallback handles any key format not matched by static patterns.- Downstream consumption safe: All consumers of
EventTypeErrorstream events use.Error()on the wrapper (returning the sanitized message). No production code callserrors.Unwrapor uses%+vto access the inner unsanitized error. TheUnwrap()method exists only for standarderrors.Is/errors.Ascompatibility.- No new exposure surface: The stored
apiKeyfield is unexported, not serialized, and used only for string comparison during redaction.- i18n/postgres changes: The i18n updates are translation key changes with no security impact. The postgres test helper change to support
PG_ROOT_DSNis test-only code with no production exposure.No medium, high, or critical vulnerabilities identified.
Sent by Cursor Automation: Find vulnerabilities
Made-with: Cursor # Conflicts: # bifrost/embeddings.go # bifrost/transcription.go
Restore postgres/pgvector_test.go and webapp/src/i18n/en.json to origin/master so the MM-67547 PR only carries the Bifrost provider error sanitization work. The PG_ROOT_DSN test-infra improvement and the post-merge i18n extraction should land in their own PRs. Made-with: Cursor
There was a problem hiding this comment.
Security Review: No Issues Found
This PR is a security hardening change that prevents API keys and tokens from leaking through upstream LLM provider error messages. Reviewed all changed files and downstream error propagation paths.
What was verified:
- Sanitization coverage: All Bifrost error paths are covered — chat streaming (2 paths), responses streaming (2 paths), embeddings (2 paths), transcription, and model listing. No unsanitized error path was found.
- Regex patterns: Cover OpenAI keys (
sk-*,sk-proj-*), Anthropic keys (sk-ant-*), Bearer tokens, JSONapiKeyfields, and the "Incorrect API key provided" message. The configured key is matched as a fallback viaregexp.QuoteMeta(injection-safe). - Error chain safety:
SanitizedProviderError.Unwrap()preserves the original error forerrors.Is/errors.Aschains, which is standard Go practice. All downstream consumers (streaming/streaming.go,api/api_llm_bridge.go,llm/stream.go) use.Error()for display/logging, which returns the sanitized message. No code path callsUnwrap()to extract and display the raw message. - API key storage: The
apiKeyfield added toLLM,EmbeddingProvider, andTranscriberstructs is the same value already available in their respective configs — this doesn't increase the attack surface. - No ReDoS risk: Static patterns are pre-compiled. The only per-call compilation (
replaceConfiguredAPIKeyInMessage) uses admin-configured input withregexp.QuoteMeta, not attacker-controlled data. - No secrets in test code: Test strings use placeholder values, no real credentials.
Sent by Cursor Automation: Find vulnerabilities
edgarbellot
left a comment
There was a problem hiding this comment.
@nickmisasi I think there's a remaining leak path. Could you please take a look?
| orgID: cfg.OrgID, | ||
| } | ||
|
|
||
| bifrostConfig := schemas.BifrostConfig{ |
There was a problem hiding this comment.
The sanitization at line 52 correctly redacts the error returned to callers, but there's a remaining leak path: Bifrost's internal logger writes the raw OpenAI error (including the partially masked API key) directly to stdout before returning the error to the plugin. Since Mattermost captures plugin stdout into mattermost.log, the key still ends up in server logs.
I was able to reproduce this by typing a test string in the API Key field on System Console - the logs show entries like:
"failed to list models with key : Incorrect API key provided: poc test******osed."
This comes from Bifrost's utils.go which logs errMsg (the raw provider response) via its internal logger.
The same issue applies to the Bifrost client created in bifrost.go:191-193.
Neither call site sets BifrostConfig.Logger, so Bifrost falls back to its default logger which writes unsanitized to stdout.
Would it make sense to inject a custom schemas.Logger wrapper that applies SanitizeProviderErrorMessage before writing? Something along these lines:
type sanitizingLogger struct {
inner schemas.Logger
apiKey string
}
func (l *sanitizingLogger) Warn(msg string, args ...any) {
formatted := fmt.Sprintf(msg, args...)
l.inner.Warn(llm.SanitizeProviderErrorMessage(formatted, l.apiKey))
}
// ... same for Debug, Info, Error, Fatal

Summary
Upstream LLM providers sometimes echo credential material inside error bodies (for example incorrect API key messages, bearer tokens, or JSON
apiKeyfields). This change redacts those patterns before Bifrost errors are returned, streamed to clients, or logged.Changes
llm.SanitizeProviderError/SanitizeProviderErrorMessagewith the same redaction approach used elsewhere for OpenAI-style errors (regex passes plus configured-key replacement, then non-printable character sanitization).Ticket
https://mattermost.atlassian.net/browse/MM-67547
Summary by CodeRabbit