Skip to content

fix(media): sanitize fetch errors before posting to channels#53780

Open
ernestodeoliveira wants to merge 2 commits intoopenclaw:mainfrom
ernestodeoliveira:fix/media-fetch-error-pii-sanitization-rebased
Open

fix(media): sanitize fetch errors before posting to channels#53780
ernestodeoliveira wants to merge 2 commits intoopenclaw:mainfrom
ernestodeoliveira:fix/media-fetch-error-pii-sanitization-rebased

Conversation

@ernestodeoliveira
Copy link
Copy Markdown
Contributor

Summary

When a media fetch operation fails, the raw error message ├ö├ç├ which may contain serialized JSON with private group IDs, phone numbers, user names, and internal system paths ├ö├ç├ was posted to the configured channel as if it were a normal reply. This exposed PII to public-facing channels like Discord and Telegram.

Example of what was leaking:

Media failed: {"group_id":"@g.us","sender":"+905...","name":"John Doe","path":"/home/user/.openclaw/..."}

Details

Added SAFE_MEDIA_FETCH_ERROR_MESSAGE constant and isSafeMediaFetchError() helper to src/media/fetch.ts. When a MediaFetchError is detected at the outbound delivery layer (message-action-params.ts, message-action-runner.ts, deliver.ts), the raw error is replaced with:

├ö├£├í┬┤┬®├à Media fetch failed. The attachment could not be retrieved.

The original error is still logged internally via logVerbose ├ö├ç├ it is only redacted from the outbound channel payload.

Three sanitization boundary points:

  1. message-action-params.ts ├ö├ç├ params resolution for media actions
  2. message-action-runner.ts ├ö├ç├ action execution runner
  3. deliver.ts ├ö├ç├ final delivery layer

Related Issues

Fixes #20279

How to Validate

Trigger a media fetch that fails (invalid URL or timeout). Confirm:

  • Channel receives the generic safe message
  • logVerbose / internal logs still contain the full error

Run unit tests:

pnpm test -- --testPathPattern="fetch|message-action-runner.media"

Note: message-action-runner.media.test.ts requires @anthropic-ai/vertex-sdk which was added to main today (commit 6e20c4baa0). The test will pass once dependencies are installed in CI. The fetch.test.ts suite (12 tests) passes locally and covers the core PII sanitization behavior.

Pre-Merge Checklist

  • Updated relevant documentation and README (if needed)
  • Added/updated tests (if needed)
  • Noted breaking changes (if any)
  • Validated on required platforms/methods:
    • Windows
      • npm run

AI-Assisted Disclosure

  • This PR was built with Claude Code (Anthropic)
  • Fully tested locally ÔÇö tests pass, oxfmt formatting verified
  • Author reviewed and understands all changes
  • Codex review not available locally (not installed)

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Mar 24, 2026

Greptile Summary

This PR fixes a PII leak in media fetch error paths where raw error messages containing group IDs, phone numbers, usernames, and file paths were being forwarded to public-facing channels (Discord, Telegram, etc.). The fix introduces a SAFE_MEDIA_FETCH_ERROR_MESSAGE constant and an isMediaFetchError() helper in src/media/fetch.ts, and applies sanitization at three defensive boundary points.

Key changes:

  • src/media/fetch.ts: New SAFE_MEDIA_FETCH_ERROR_MESSAGE constant ("⚠️ Media failed: unable to fetch attachment.") and isMediaFetchError() that traverses the Error.cause chain to detect MediaFetchError at any nesting depth
  • src/infra/outbound/message-action-params.ts: Catches MediaFetchError from loadWebMedia, logs the original message via logVerbose, then re-throws as a wrapped Error with the safe message
  • src/infra/outbound/message-action-runner.ts: Sanitizes the error in handleBroadcastAction results using isMediaFetchError
  • src/infra/outbound/deliver.ts: Sanitizes the error in emitMessageSent using isMediaFetchError; non-Error thrown values now resolve to "unknown error" instead of String(err) (a deliberate defensive change to avoid leaking stringified non-Error objects)
  • Good unit test coverage in both fetch.test.ts (12 tests covering isMediaFetchError and the safe message) and message-action-runner.media.test.ts (end-to-end PII non-exposure assertion)

Minor note: The safe message in code ("⚠️ Media failed: unable to fetch attachment.") differs slightly from the example in the PR description ("⚠️ Media fetch failed. The attachment could not be retrieved.") — the code string is the authoritative one and is fine.

Confidence Score: 5/5

  • This PR is safe to merge — it addresses a real PII leak with a well-structured, defensively layered fix and adequate test coverage.
  • The security fix is correct: three sanitization boundaries are applied, isMediaFetchError() correctly traverses cause chains, the safe message contains no PII patterns, and the original error is intentionally preserved in the cause chain for internal logging only. The one P2 observation (.toContain vs .toBe in the test) is non-blocking. No critical logic errors or data-loss risks are present.
  • No files require special attention
Prompt To Fix All With AI
This is a comment left during a code review.
Path: src/infra/outbound/message-action-runner.media.test.ts
Line: 532

Comment:
**Strengthen assertion to `toBe` for exact match**

The test uses `.toContain(SAFE_MEDIA_FETCH_ERROR_MESSAGE)` to verify the sanitized error message. Since `SAFE_MEDIA_FETCH_ERROR_MESSAGE` is the entire expected string, a `.toBe` assertion is more precise and will catch any regression where PII is prepended or appended to the safe message (which `.toContain` would silently miss).

```suggestion
      expect(errorText).toBe(SAFE_MEDIA_FETCH_ERROR_MESSAGE);
```

How can I resolve this? If you propose a fix, please make it concise.

Reviews (1): Last reviewed commit: "fix(media): remove fragile string-match ..." | Re-trigger Greptile

expect(errorText).not.toContain("Secret Group");
expect(errorText).not.toContain("/home/user");
// Safe message should be used
expect(errorText).toContain(SAFE_MEDIA_FETCH_ERROR_MESSAGE);
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 Strengthen assertion to toBe for exact match

The test uses .toContain(SAFE_MEDIA_FETCH_ERROR_MESSAGE) to verify the sanitized error message. Since SAFE_MEDIA_FETCH_ERROR_MESSAGE is the entire expected string, a .toBe assertion is more precise and will catch any regression where PII is prepended or appended to the safe message (which .toContain would silently miss).

Suggested change
expect(errorText).toContain(SAFE_MEDIA_FETCH_ERROR_MESSAGE);
expect(errorText).toBe(SAFE_MEDIA_FETCH_ERROR_MESSAGE);
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/infra/outbound/message-action-runner.media.test.ts
Line: 532

Comment:
**Strengthen assertion to `toBe` for exact match**

The test uses `.toContain(SAFE_MEDIA_FETCH_ERROR_MESSAGE)` to verify the sanitized error message. Since `SAFE_MEDIA_FETCH_ERROR_MESSAGE` is the entire expected string, a `.toBe` assertion is more precise and will catch any regression where PII is prepended or appended to the safe message (which `.toContain` would silently miss).

```suggestion
      expect(errorText).toBe(SAFE_MEDIA_FETCH_ERROR_MESSAGE);
```

How can I resolve this? If you propose a fix, please make it concise.

@ernestodeoliveira ernestodeoliveira force-pushed the fix/media-fetch-error-pii-sanitization-rebased branch from 4dba470 to f6c329a Compare March 24, 2026 20:15
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4dba4702cc

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

@@ -757,10 +758,12 @@ async function deliverOutboundPayloadsCore(
messageId: lastMessageId,
});
} catch (err) {
const rawError = err instanceof Error ? err.message : "unknown error";
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve non-Error text in outbound failure events

The new fallback err instanceof Error ? err.message : "unknown error" drops actionable diagnostics when a plugin/adapter throws a string or plain object, so callers now get unknown error instead of the real failure text. Before this commit, non-Error throws were surfaced via String(err), which kept these failures debuggable; this regression affects non-media failures too and is unrelated to the media-sanitization goal. Keep the media-specific redaction, but use String(err) for non-Error values (and mirror the same fix in message-action-runner.ts).

Useful? React with 👍 / 👎.

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Critical Privacy Leak: Internal metadata and PII exposed in "Media failed" error messages

1 participant