Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion packages/gatekeeper-cloudflare/src/cloudflare.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,10 @@ export default {
} else if (relPath === "/oauth") {
const error = url.searchParams.get("error");
if (error) {
return new Response(`${error}: ${url.searchParams.get("error_description")}`);
// Security fix: Set Content-Type to text/plain to prevent XSS via reflected parameters
return new Response(`${error}: ${url.searchParams.get("error_description")}`, {
headers: { "Content-Type": "text/plain; charset=utf-8" },
});
}
const state = url.searchParams.get("state");
if (!state) return new Response("Error: no 'state' provided");
Expand Down
21 changes: 21 additions & 0 deletions packages/mcp-shared/src/endpoint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,13 @@ const BLOCKED_HOST_PATTERNS = [
// spellings of the same address: `http://2130706433/` and `http://0x7f000001/` are both 127.0.0.1,
// and `[::ffff:127.0.0.1]` is its IPv4-mapped IPv6 form. Each becomes dotted-quad.
function normalizeHost(hostname: string): string {
// Security fix: Strip IPv6 zone IDs and handle IPv4-compatible IPv6 to prevent SSRF bypass
hostname = hostname.replace(/(%25|%)[^\]]+\]$/, "]");
const compat = /^\[::([^\]]+)\]$/i.exec(hostname);
if (compat && compat[1].includes(".")) {
hostname = compat[1];
}

// `URL` rewrites an IPv4-mapped IPv6 address into hex groups, so `[::ffff:127.0.0.1]` arrives as
// `[::ffff:7f00:1]` and the dotted-quad spelling is never what we see.
const mapped = /^\[::ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})\]$/i.exec(hostname);
Expand All @@ -41,6 +48,20 @@ function normalizeHost(hostname: string): string {
return [high >>> 8, high & 0xff, low >>> 8, low & 0xff].join(".");
}

// Security fix: Parse each octet individually to handle mixed/hex/octal dotted notation
const octets = hostname.split(".");
if (octets.length === 4) {
const parsedOctets = octets.map(octet => {
if (/^0[xX][0-9a-fA-F]+$/.test(octet)) return parseInt(octet, 16);
if (/^0[0-7]+$/.test(octet)) return parseInt(octet, 8);
if (/^(0|[1-9][0-9]*)$/.test(octet)) return parseInt(octet, 10);
return NaN;
});
if (parsedOctets.every(o => Number.isInteger(o) && o >= 0 && o <= 255)) {
return parsedOctets.join(".");
}
}

// A bare integer (decimal, hex, or octal) is a valid IPv4 address to most resolvers.
const asInteger = /^(?:0[xX][0-9a-fA-F]+|0[0-7]*|[1-9][0-9]*)$/.test(hostname)
? Number(hostname)
Expand Down
7 changes: 6 additions & 1 deletion packages/mcp-shared/src/tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,9 +159,14 @@ function defuseFences(text: string): string {
// and headings are neutralized, the text is capped, and the rest is block-quoted.
function quoteUntrusted(text: string, max: number): string {
const cleaned = defuseFences(text)
// Security fix: strip HTML tags and bidirectional control chars to prevent injection/spoofing
.replace(/<[^>]*>/g, "")
.replace(/[\u200E-\u200F\u202A-\u202E\u2066-\u2069\u061C]/g, "")
// Repeated, since one strip leaves `##` as `#` -- still a heading, at heading weight, in the
// prompt the approver reads.
// prompt the approver reads. Also strip horizontal rules and strikethrough.
.replace(/^[ \t]*[#>]+[ \t]*/gm, "")
.replace(/^[ \t]*(?:-{3,}|\*{3,})[ \t]*/gm, "")
.replace(/~~/g, "")
.trim();
const clipped = cleaned.length > max ? `${cleaned.slice(0, max)}\u2026` : cleaned;
return clipped.split("\n").map(line => `> ${line}`).join("\n");
Expand Down
Loading