fix(security): close secret-scrubbing bypass paths in all MCP write routes#922
fix(security): close secret-scrubbing bypass paths in all MCP write routes#922Srinath279 wants to merge 2 commits into
Conversation
…outes ## Problem `stripPrivateData` was only applied inside the observe pipeline (`src/hooks/prompt-submit.ts`). Seven write paths that accept content directly via MCP tools bypassed that pipeline entirely, so secrets passed through those routes were stored in plaintext: - `mem::remember` — explicit memory saves - `mem::lesson-save` — lessons (content + context fields) - `mem::slot-create` — slot initial content - `mem::slot-append` — appended text - `mem::slot-replace` — replaced content - `mem::sketch-create` — sketch/action title and description - `mem::team-share` — shared item at the audience-widening boundary - `mem::compress` — LLM summary output (model can echo seen secrets) - `mem::import` — imported dumps bypass capture entirely ## Changes ### src/functions/privacy.ts - Add regex for PEM private key blocks (`-----BEGIN * PRIVATE KEY-----`) - Add regex for DB connection URLs with embedded credentials (`postgres://`, `mysql://`, `mongodb+srv://`, `redis://`, etc.) - Export new `scrubRecord<T>` — a recursive `T→T` walker that applies `stripPrivateData` to every string in an arbitrary nested object/array. Use this where the shape is unknown (imports, LLM output, team shares); call `stripPrivateData` directly for known string fields. ### src/functions/remember.ts - Call `stripPrivateData(data.content)` before the Jaccard similarity check so dedup compares and stores the already-scrubbed form. ### src/functions/lessons.ts - Scrub `content` and `context` before fingerprinting, so dedup keys match the scrubbed form. Applies to both manual saves and output from `mem::crystallize`. ### src/functions/slots.ts - Scrub content in `slot-create`, text in `slot-append`, and content in `slot-replace`. ### src/functions/sketches.ts - Scrub `title` and `description` in both `sketch-create` and the inline action-create path. ### src/functions/team.ts - Apply `scrubRecord` at the share boundary before writing the `TeamSharedItem`. Defense-in-depth: catches rows written before a newer pattern was added and shares widen audience from one user to the team. ### src/functions/compress.ts - Apply `scrubRecord` to the LLM-parsed compression output before it is spread into `CompressedObservation`. An LLM can echo a secret from its context window into a generated summary. ### src/functions/export-import.ts - Apply `scrubRecord` to all content-bearing collections (sessions, memories, summaries, lessons, insights, semanticMemories, crystals, sketches, observations) before any row is written during import. Imports arrive from outside the capture pipeline and may have been created by an older install with fewer patterns. ### test/scrubbing-bypass.test.ts (new) - Unit tests for all seven bypass paths: remember, lesson-save, slot-create, slot-append, slot-replace, sketch-create, team-share, compress (LLM output), and import. - Fixtures cover every secret family: Anthropic key, GitHub PAT, DB URL with embedded password, and PEM private key block. ### pnpm-workspace.yaml (new) - `allowBuilds` for esbuild, onnxruntime-node, protobufjs, sharp so `pnpm build` and `pnpm start` are not blocked by the ERR_PNPM_IGNORED_BUILDS check on developer machines.
|
Someone is attempting to deploy a commit to the rohitg00's projects Team on Vercel. A member of the Team first needs to authorize it. |
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (2)
✅ Files skipped from review due to trivial changes (1)
🚧 Files skipped from review as they are similar to previous changes (1)
📝 WalkthroughWalkthroughAdds recursive record scrubbing and new secret-detection patterns, applies scrubbing across memory operations and import/export boundaries, updates architecture docs and workspace build config, and adds comprehensive tests validating redaction across workflows. ChangesPrivacy Scrubbing Enhancement
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (1)
src/functions/slots.ts (1)
322-322: 💤 Low valueConsider assigning to a local variable instead of mutating the input parameter.
While the current approach works correctly, mutating
data.contentdirectly is stylistically inconsistent with the patterns used elsewhere in this file (slot-create and slot-append assign to local constants). Using a local variable would make the code more consistent and easier to reason about.♻️ Optional refactor for consistency
const label = validateLabel(data?.label); if (!label) return { success: false, error: "label required" }; if (typeof data?.content !== "string") return { success: false, error: "content required (string)" }; - data.content = stripPrivateData(data.content); + const content = stripPrivateData(data.content); return withKeyedLock(`slot:${label}`, async () => { const { slot, scope } = await readSlot(kv, label); if (!slot) return { success: false, error: "slot not found (use mem::slot-create first)" }; if (slot.readOnly) return { success: false, error: "slot is read-only" }; - if (data.content.length > slot.sizeLimit) { + if (content.length > slot.sizeLimit) { return { success: false, - error: `content exceeds sizeLimit (${data.content.length} > ${slot.sizeLimit})`, + error: `content exceeds sizeLimit (${content.length} > ${slot.sizeLimit})`, sizeLimit: slot.sizeLimit, }; } - const updated: MemorySlot = { ...slot, content: data.content, updatedAt: nowIso() }; + const updated: MemorySlot = { ...slot, content, updatedAt: nowIso() }; await kv.set(scopeKv(scope), label, updated); await recordAudit(kv, "slot_replace", "mem::slot-replace", [label], { scope, before: slot.content.length, - after: data.content.length, + after: content.length, }); - return { success: true, slot: updated, size: data.content.length }; + return { success: true, slot: updated, size: content.length }; });🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/functions/slots.ts` at line 322, Replace the in-place mutation of the input parameter by assigning the sanitized value to a local variable: instead of mutating data.content with stripPrivateData, create a local const (e.g., const content = stripPrivateData(data.content)) and use that local variable for subsequent logic; update any references in the surrounding function (slots.ts, functions that follow this line) to read from the new local variable rather than data.content to keep the style consistent with slot-create/slot-append.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@docs/E2E_ARCHITECTURE.md`:
- Around line 686-699: The Session model lacks the parentSessionId used by the
subagent flow; add an optional parentSessionId?: string (UUID v4) to the Session
interface to reflect that subagents link back to a parent session, and update
any other occurrences of the Session declaration (the later duplicate) and any
documentation/examples that reference Session to include this optional field so
the contract in Section 7 and Section 5 stays consistent.
- Around line 455-459: The table row for Scope Key `mem:obs:{sessionId}`
contains the cell text RawObservation | CompressedObservation[] which breaks the
markdown table; update that cell to escape the pipe (e.g., replace the vertical
bar with \| or the HTML entity &`#124`;) so it stays a single column (locate the
row with `mem:obs:{sessionId}` and adjust the cell content).
- Around line 100-106: The diagram contains a truncated reference
"index-persist.."—replace it with the full filename used in Section 4.4, e.g.
"index-persistence.ts", so the entry reads "agent-sdk │ │ IndexPersistence
(disk) ←─── index-persistence.ts" and ensure the spelling matches the
actual source file name referenced elsewhere.
---
Nitpick comments:
In `@src/functions/slots.ts`:
- Line 322: Replace the in-place mutation of the input parameter by assigning
the sanitized value to a local variable: instead of mutating data.content with
stripPrivateData, create a local const (e.g., const content =
stripPrivateData(data.content)) and use that local variable for subsequent
logic; update any references in the surrounding function (slots.ts, functions
that follow this line) to read from the new local variable rather than
data.content to keep the style consistent with slot-create/slot-append.
🪄 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: defaults
Review profile: CHILL
Plan: Pro
Run ID: 2ff725c5-7bd4-49de-821c-4549c61dc0cf
📒 Files selected for processing (11)
docs/E2E_ARCHITECTURE.mdpnpm-workspace.yamlsrc/functions/compress.tssrc/functions/export-import.tssrc/functions/lessons.tssrc/functions/privacy.tssrc/functions/remember.tssrc/functions/sketches.tssrc/functions/slots.tssrc/functions/team.tstest/scrubbing-bypass.test.ts
- Fix truncated reference 'index-persist..' → 'index-persistence.ts' in architecture diagram - Escape pipe in table cell for RawObservation | CompressedObservation[] - Add parentSessionId field to Session interface to align with subagent contract - Refactor slot-replace to use local const instead of mutating input parameter Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Problem
stripPrivateDatawas wired into the observe pipeline only (hooks/prompt-submit.ts).Seven MCP write tools that accept content directly bypassed that pipeline entirely,
meaning secrets submitted through those routes were stored in plaintext:
Solution
Apply stripPrivateData (or the new scrubRecord for untyped payloads) at each
write site before data touches KV storage.
New patterns in privacy.ts
New utility: scrubRecord
A recursive T→T walker (objects, arrays, strings). Lets callers scrub arbitrarily-shaped
payloads (LLM output, import blobs, team-share content) without knowing their schema.
Tests
New test/scrubbing-bypass.test.ts covers every bypass path.
Bonus: pnpm-workspace.yaml
Adds allowBuilds for esbuild, onnxruntime-node, protobufjs, sharp — fixes
ERR_PNPM_IGNORED_BUILDS on fresh checkouts.
Test plan
Summary by CodeRabbit
Documentation
Bug Fixes
Tests
Chores