Skip to content

fix: resolve issue #19 security gaps and logging discrepancies#154

Open
stakeswky wants to merge 1 commit intoTinyAGI:mainfrom
stakeswky:fix/issue-19-security-discrepancies
Open

fix: resolve issue #19 security gaps and logging discrepancies#154
stakeswky wants to merge 1 commit intoTinyAGI:mainfrom
stakeswky:fix/issue-19-security-discrepancies

Conversation

@stakeswky
Copy link

Summary

Fixes key security and discrepancy items in #19:

  • remove plaintext message content snippets from logs (queue processor + channel clients)
  • keep Chromium sandbox enabled by default for WhatsApp
  • allow sandbox disable only via explicit opt-in env (WHATSAPP_DISABLE_SANDBOX=true) with warning log
  • add tests for both privacy-safe logging helper and WhatsApp launch args behavior

Details

1) Sensitive message logging

Previous code logged message.substring(0, 50) from user content in multiple places.
This change introduces messageSizeSummary() and logs only message length metadata, e.g. 42 characters, avoiding plaintext snippets at rest in log files.

Touched files:

  • src/queue-processor.ts
  • src/channels/discord-client.ts
  • src/channels/telegram-client.ts
  • src/channels/whatsapp-client.ts
  • src/lib/privacy.ts (new)

2) WhatsApp Chromium sandbox

Previous launch args always disabled sandbox (--no-sandbox, --disable-setuid-sandbox).
Now sandbox remains enabled by default. Sandbox disable is opt-in via WHATSAPP_DISABLE_SANDBOX=true for environments that require it.

Touched files:

  • src/channels/whatsapp-client.ts
  • src/lib/whatsapp-launch.ts (new)

3) Tests

Added Node built-in test coverage:

  • src/test/privacy.test.ts
  • src/test/whatsapp-launch.test.ts
  • package.json test script: npm run build:main && node --test dist/test/**/*.test.js

Validation

  • npm run build:main
  • npm run test ✅ (4 tests passing)

Notes

Issue #19 also discusses command injection and allowlist concerns.
On current main, invocation already uses spawn(command, args) (no shell interpolation), and pairing-based sender gating is present in all channel clients. This PR focuses on the remaining concrete security/privacy deltas from that report (message-at-rest leakage and sandbox defaults).

@jlia0
Copy link
Collaborator

jlia0 commented Mar 6, 2026

@greptileai review this pr

@greptile-apps
Copy link

greptile-apps bot commented Mar 6, 2026

Greptile Summary

This PR addresses two concrete security gaps from issue #19: it replaces plaintext message content snippets in log calls with a privacy-safe messageSizeSummary() helper across all channel clients and the queue processor, and it re-enables the Chromium sandbox for the WhatsApp client by default (opt-out via WHATSAPP_DISABLE_SANDBOX=true). Both new modules (privacy.ts, whatsapp-launch.ts) are covered by Node built-in tests.

Key issues found:

  • Privacy gap remains in emitEvent: The log() call in queue-processor.ts was correctly updated, but the immediately adjacent emitEvent('message_received', ...) call still passes rawMessage.substring(0, 120) as an event field. Any downstream consumer of that event (e.g. websocket clients, monitoring hooks) still receives raw message content, undermining the stated goal of eliminating plaintext snippets from the system.
  • Breaking change for Docker deployments: Re-enabling the Chromium sandbox by default means Chromium will fail to launch in Docker containers that lack SYS_ADMIN capability or a permissive seccomp profile — the most common production scenario. Users previously unaffected will find the WhatsApp client broken on upgrade until they set WHATSAPP_DISABLE_SANDBOX=true. This migration requirement is not documented in the PR.
  • TypeScript type inconsistency in messageSizeSummary: The parameter is typed as string but the body applies a || '' null-guard, which implies null/undefined inputs are expected. The type should be string | null | undefined to match the implementation, and a corresponding test case should be added.

Confidence Score: 3/5

  • Requires fixes before merging — an unresolved privacy gap in emitEvent and a breaking change for Docker deployments need to be addressed.
  • The privacy-safe logging refactor is well-structured and the sandbox opt-out mechanism is the right security direction, but there are two meaningful issues: (1) the emitEvent call adjacent to the fixed log line still leaks message content, directly contradicting the PR's core goal, and (2) enabling the sandbox by default is a breaking change for Docker-based deployments that wasn't flagged as such or documented. These are not critical production crashes on all platforms, but they represent incomplete or undocumented behavior changes that should be resolved before merging.
  • src/queue-processor.ts (emitEvent privacy gap) and src/lib/whatsapp-launch.ts (Docker breaking change) need the most attention.

Important Files Changed

Filename Overview
src/queue-processor.ts Log call updated to use messageSizeSummary, but the adjacent emitEvent call on line 74 still passes rawMessage.substring(0, 120), leaving a privacy gap in event payloads.
src/lib/whatsapp-launch.ts New helper cleanly separates sandbox args; however, enabling sandbox by default is a breaking change for Docker deployments, which require --no-sandbox or elevated kernel capabilities to launch Chromium.
src/lib/privacy.ts Simple, correct privacy helper; TypeScript parameter type (string) is inconsistent with the defensive (text
src/channels/whatsapp-client.ts Log call updated correctly; sandbox flag delegation to buildWhatsAppChromiumArgs is clean; sandbox-disable warning is appropriate.
src/channels/discord-client.ts Log call correctly updated to messageSizeSummary; no other issues in the changed lines.
src/channels/telegram-client.ts Log call correctly updated to messageSizeSummary; no other issues in the changed lines.
src/test/privacy.test.ts Covers empty string, singular, and plural cases; missing coverage for null/undefined inputs given the defensive implementation.
src/test/whatsapp-launch.test.ts Tests correctly verify sandbox flag presence/absence for both code paths; good coverage of the new helper.

Sequence Diagram

sequenceDiagram
    participant C as Channel Client<br/>(Discord/Telegram/WhatsApp)
    participant P as privacy.ts<br/>messageSizeSummary()
    participant L as logging.ts<br/>log() / emitEvent()
    participant Q as queue-processor.ts

    Note over C: Incoming message received
    C->>P: messageSizeSummary(messageText)
    P-->>C: "42 characters"
    C->>L: log(INFO, "Message from X: 42 characters")
    C->>Q: POST /api/message (full message body)

    Note over Q: Queue processing
    Q->>P: messageSizeSummary(rawMessage)
    P-->>Q: "42 characters"
    Q->>L: log(INFO, "Processing [...]: 42 characters")
    Q->>L: emitEvent('message_received',<br/>{ message: rawMessage.substring(0,120) })
    Note over L: ⚠️ Raw content still in event payload
Loading

Last reviewed commit: 127af2c

log('INFO', `Processing [${isInternal ? 'internal' : channel}] ${isInternal ? `@${dbMsg.from_agent}→@${dbMsg.agent}` : `from ${sender}`}: ${rawMessage.substring(0, 50)}...`);
log('INFO', `Processing [${isInternal ? 'internal' : channel}] ${isInternal ? `@${dbMsg.from_agent}→@${dbMsg.agent}` : `from ${sender}`}: ${messageSizeSummary(rawMessage)}`);
if (!isInternal) {
emitEvent('message_received', { channel, sender, message: rawMessage.substring(0, 120), messageId });
Copy link

Choose a reason for hiding this comment

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

emitEvent still passes raw message content

The log() call on the line above was correctly updated to use messageSizeSummary, but the adjacent emitEvent call still passes rawMessage.substring(0, 120) — a literal plaintext snippet of the message body — as part of its payload.

Since emitEvent is imported from ./lib/logging alongside log, any downstream consumer of message_received events (websocket clients, event log files, monitoring hooks, etc.) will still receive unredacted message content. This directly contradicts the PR's goal of eliminating plaintext message content snippets from the system.

Suggested change
emitEvent('message_received', { channel, sender, message: rawMessage.substring(0, 120), messageId });
emitEvent('message_received', { channel, sender, message: messageSizeSummary(rawMessage), messageId });

Comment on lines +10 to +12
if (!disableSandbox) {
return baseArgs;
}
Copy link

Choose a reason for hiding this comment

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

Sandbox-enabled default will break Chromium in Docker without opt-in

Returning baseArgs (no sandbox flags) when disableSandbox === false means Chromium launches with its kernel-level sandbox fully active. This is the correct security posture on bare-metal hosts, but it is a breaking change for the most common production deployment scenario — Docker containers.

Docker containers do not have the SYS_ADMIN capability or a permissive seccomp profile by default, both of which are required for Chromium's namespace sandbox to initialize. Without them, Puppeteer will fail to launch Chromium entirely, typically with an error like:

Failed to launch the browser process!
[...] FATAL:zygote_host_impl_linux.cc] No usable sandbox!

Previously, --no-sandbox was always included, so the WhatsApp client worked out of the box in Docker. After this change, existing Docker-based deployments will fail to start until operators explicitly set WHATSAPP_DISABLE_SANDBOX=true, which is not called out as a migration step anywhere in the PR or README.

Consider documenting the Docker requirement prominently (e.g. in the README or startup warning log), or detecting the environment at runtime and emitting a clearer error when the sandbox fails to initialize.

Comment on lines +1 to +2
export function messageSizeSummary(text: string): string {
const length = (text || '').length;
Copy link

Choose a reason for hiding this comment

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

TypeScript signature doesn't match the defensive implementation

The function is typed as accepting string, but the body immediately applies (text || ''), which only has effect when text is null, undefined, or an empty string. Since TypeScript already guarantees a string at the call site, the || '' guard is unreachable under strict type-checking — yet it signals the author's expectation that callers may actually pass null/undefined.

Either tighten the type to match the guard:

Suggested change
export function messageSizeSummary(text: string): string {
const length = (text || '').length;
export function messageSizeSummary(text: string | null | undefined): string {
const length = (text || '').length;

Or drop the defensive guard if callers are already typed as string. Additionally, the test suite does not cover a null/undefined input — if the broader intent is to be safe against those, a test case should be added to privacy.test.ts.

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.

2 participants