Skip to content

Redact spoilers in MCP write confirmations; harden SSRF name check - #306

Open
cameronapak wants to merge 1 commit into
mainfrom
claude/security-review-dbba89
Open

cameronapak wants to merge 1 commit into
mainfrom
claude/security-review-dbba89

Conversation

@cameronapak

@cameronapak cameronapak commented Jul 22, 2026

Copy link
Copy Markdown
Owner

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

  1. MCP write confirmations (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.
  2. The unfurl SSRF guard strips a trailing dot from the hostname, so an FQDN like foo.internal. can no longer bypass the .internal/.local/.localhost name 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 — clean

Summary by CodeRabbit

  • Bug Fixes
    • Write-tool confirmation messages now redact spoiler content when displaying destination and parent-node details.
    • Improved protection against unsafe URL fetching by correctly handling hostnames with trailing dots.
  • Security
    • Internal and localhost-style hostnames using fully qualified trailing-dot forms are now blocked consistently.
  • Tests
    • Added coverage for spoiler redaction in write confirmations and trailing-dot hostname validation.

…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>
@cursor

cursor Bot commented Jul 22, 2026

Copy link
Copy Markdown

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.

@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

MCP 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.

Changes

MCP spoiler redaction

Layer / File(s) Summary
Redact MCP write confirmations
worker/mcp-tools.ts, worker/mcp.test.ts, .changeset/*
add_node, add_subtree, move_nodes, mirror_node, and import_opml redact spoiler content in destination-parent confirmation text; a test verifies the rendered output.

Unfurl hostname canonicalization

Layer / File(s) Summary
Canonicalize unfurl hostnames
worker/unfurl-core.ts, worker/unfurl.test.ts
Hostname normalization removes trailing dots before internal-hostname checks, with tests for internal FQDN forms.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the two main changes: spoiler redaction in MCP write confirmations and a hardened SSRF hostname check.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/security-review-dbba89

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

📥 Commits

Reviewing files that changed from the base of the PR and between 5a08f8f and 58ce7e1.

📒 Files selected for processing (5)
  • .changeset/spoiler-redact-mcp-writes.md
  • worker/mcp-tools.ts
  • worker/mcp.test.ts
  • worker/unfurl-core.ts
  • worker/unfurl.test.ts

Comment thread worker/unfurl-core.ts
Comment on lines +75 to +78
const host = u.hostname
.toLowerCase()
.replace(/^\[|]$/g, "") // strip IPv6 brackets
.replace(/\.$/, ""); // strip a trailing dot so "foo.internal." can't slip the name checks

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 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}`);
}
JS

Repository: 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.

Suggested change
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.

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.

1 participant