Redact spoilers in MCP write confirmations; harden SSRF name check - #306
cameronapak wants to merge 1 commit into
Conversation
…name check MCP write tools (add_node, add_subtree, move_nodes, mirror_node, import_opml) echoed the destination parent's raw text in their confirmation strings, leaking ||spoiler|| plaintext the read tools redact on egress. Route those interpolations through redactSpoilers. Also strip a trailing dot from the unfurl hostname so an FQDN like foo.internal. can't slip the internal-name checks in the SSRF guard. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Bugbot is not enabled for your account, so this pull request was not reviewed. Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs. |
📝 WalkthroughWalkthroughMCP write-tool confirmations now redact spoiler text in destination-parent descriptions. The SSRF unfurl guard strips trailing hostname dots before internal-name checks, with tests covering both hardening changes and a patch changeset documenting them. ChangesMCP spoiler redaction
Unfurl hostname canonicalization
Estimated code review effort: 2 (Simple) | ~10 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 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: 1
🤖 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 `@worker/unfurl-core.ts`:
- Around line 75-78: Update the hostname normalization in isAllowedUnfurlTarget
to remove the entire trailing-dot run, not just one dot, before SSRF hostname
checks; preserve IPv6 bracket stripping and existing checks. Add a regression
test covering double trailing dots such as db.internal.., and document the
normalization policy in the relevant docs or ADR if this behavior is
intentional.
🪄 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: c3774563-5627-4416-8d2e-cf4358745049
📒 Files selected for processing (5)
.changeset/spoiler-redact-mcp-writes.mdworker/mcp-tools.tsworker/mcp.test.tsworker/unfurl-core.tsworker/unfurl.test.ts
| const host = u.hostname | ||
| .toLowerCase() | ||
| .replace(/^\[|]$/g, "") // strip IPv6 brackets | ||
| .replace(/\.$/, ""); // strip a trailing dot so "foo.internal." can't slip the name checks |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "Locate worker/unfurl-core.ts and related files"
fd -a 'unfurl-core\.ts$|AGENTS\.md$|README\.md$' . | sed 's#^\./##'
echo
echo "Inspect worker/unfurl-core.ts outline and relevant section"
if [ -f worker/unfurl-core.ts ]; then
wc -l worker/unfurl-core.ts
ast-grep outline worker/unfurl-core.ts || true
sed -n '1,180p' worker/unfurl-core.ts | cat -n
fi
echo
echo "Search for internal suffix checks and SSRF/fetch/use path"
rg -n --hidden --glob '!node_modules' --glob '!dist' --glob '!build' '(endsWith\([^)]*\.internal|startsWith\([^)]*\.internal|\.internal|Hostname|hostname|fetch\(|Request|URL\()' .Repository: cameronapak/dotflowy
Length of output: 38247
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "Inspect worker/unfurl.ts redirect revalidation"
sed -n '1,120p' worker/unfurl.ts | cat -n
echo
echo "Inspect worker/unfurl.test.ts guard tests"
sed -n '1,130p' worker/unfurl.test.ts | cat -n
echo
echo "Programmatic check: current replacement behavior vs internal suffix predicates"
node - <<'JS'
const cases = ["db.internal.", "db.internal..", "db.internal...", "foo.internal..bar", "localhost.."];
for (const raw of cases) {
const host = raw.toLowerCase().replace(/^\[|]$/g, "").replace(/\.$/, "");
const outcome = host && (host === "localhost" || host.endsWith(".localhost") || host.endsWith(".local") || host.endsWith(".internal"))
? "blocked" : "allowed";
console.log(`${raw} -> ${host!} -> ${outcome}`);
}
JSRepository: cameronapak/dotflowy
Length of output: 12065
Normalize all trailing dots before the SSRF hostname checks.
.replace(/\.$/, "") leaves db.internal.. / db.internal... as db.internal. / db.internal.., which avoid the *.internal block. Strip the entire trailing-dot run at the isAllowedUnfurlTarget boundary, and add a regression test for the double-dot case plus docs/ADR guidance if this was intended as policy.
Proposed fix
- .replace(/\.$/, ""); // strip a trailing dot so "foo.internal." can't slip the name checks
+ .replace(/\.+$/, ""); // strip all trailing dots before hostname checks📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const host = u.hostname | |
| .toLowerCase() | |
| .replace(/^\[|]$/g, "") // strip IPv6 brackets | |
| .replace(/\.$/, ""); // strip a trailing dot so "foo.internal." can't slip the name checks | |
| const host = u.hostname | |
| .toLowerCase() | |
| .replace(/^\[|]$/g, "") // strip IPv6 brackets | |
| .replace(/\.+$/, ""); // strip all trailing dots before hostname checks |
🤖 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 `@worker/unfurl-core.ts` around lines 75 - 78, Update the hostname
normalization in isAllowedUnfurlTarget to remove the entire trailing-dot run,
not just one dot, before SSRF hostname checks; preserve IPv6 bracket stripping
and existing checks. Add a regression test covering double trailing dots such as
db.internal.., and document the normalization policy in the relevant docs or ADR
if this behavior is intentional.
Summary
A security review of the Worker surface turned up two egress gaps: MCP write-tool confirmations leaked spoiler plaintext the read tools redact, and the unfurl SSRF guard could be slipped by a trailing-dot FQDN. Both are now closed.
Changes
add_node,add_subtree,move_nodes,mirror_node,import_opml) redact||spoiler||runs in the destination parent's echoed text, matching the read tools' egress redaction.foo.internal.can no longer bypass the.internal/.local/.localhostname checks.Test plan
bun test worker/mcp.test.ts worker/unfurl.test.ts— 58 pass (added a confirmation-redaction regression + a trailing-dot FQDN case)bun run typecheck:worker,bun run typecheck:test,bun run lint— cleanSummary by CodeRabbit