Use with caution. Address findings before production.
Scanned 5/12/2026, 3:36:03 PMยทCached resultยทDeep Scanยท91 rulesยทView source โยทHow we decide โ
AIVSS Score
Medium
Severity Breakdown
0
critical
1
high
27
medium
1
low
MCP Server Information
Findings
This package carries a C-grade security rating with 27 medium-severity issues that pose meaningful risks, primarily centered on resource exhaustion vulnerabilities (15 findings) and ANSI escape injection attacks (8 findings). The safety score of 66/100 indicates notable gaps in security posture, though the absence of critical or high-severity findings suggests the risks are manageable with proper usage controls. You should implement strict input validation and resource limits if you deploy this package in production environments.
AIPer-finding remediation generated by bedrock-claude-haiku-4-5 โ 29 of 29 findings. Click any finding to read.
No known CVEs found for this package or its dependencies.
Scan Details
Done
Sign in to save scan history and re-scan automatically on new commits.
Building your own MCP server?
Same rules, same LLM judges, same grade. Private scans stay isolated to your account and never appear in the public registry. Required for code your team hasnโt shipped yet.
29 of 29 findings
29 findings
MCP server auto-generates tools from OpenAPI spec that call Brilliant Directories API v2 endpoints using BD_API_KEY/BD_SITE_URL environment variables read at startup, without consulting per-request caller identity or authorization context.
Evidence
| 1 | #!/usr/bin/env node |
| 2 | |
| 3 | /** |
| 4 | * Brilliant Directories MCP Server (npm package โ stdio transport) |
RemediationAI
The problem is that the MCP server reads BD_API_KEY and BD_SITE_URL at startup and uses them for all tool calls, without checking the identity or authorization context of each request. Fix this by implementing a per-request authorization middleware that validates the caller's identity (via MCP request metadata or a custom auth header) and maps it to a specific BD API key and site URL before executing any tool. This ensures each caller only accesses BD resources they are authorized for, preventing privilege escalation. Verify the fix by adding a test that attempts to call a tool with a different caller identity and confirms the request is rejected or uses the correct scoped credentials.
LLM consensus
User-controlled value printed to terminal without ANSI escape sanitization. Malicious input can inject cursor-control sequences, rewrite earlier output, or hide shell commands from the operator.
Evidence
| 3894 | res.on("end", () => { |
| 3895 | if (config.debug) { |
| 3896 | console.error(`[debug] <- ${res.statusCode} ${method} ${fullUrl.href}`); |
| 3897 | console.error(`[debug] body: ${data.length > 500 ? data.slice(0, 500) + "...[truncated]" : data}`); |
| 3898 | } |
| 3899 | const retryAfter = res.headers && res.headers["retry-after"]; |
| 3900 | try { |
RemediationAI
The problem is that `fullUrl.href`, `method`, and `data` are user-controlled values printed directly to stderr without ANSI escape sanitization, allowing an attacker to inject cursor-control sequences that rewrite terminal output or hide commands. Replace the console.error calls with a sanitization function such as `stripAnsi()` from the `strip-ansi` npm package, or manually remove ANSI escape sequences using a regex like `/\x1b\[[0-9;]*m/g`. This prevents terminal injection attacks. Verify by passing a URL with embedded ANSI codes (e.g., `\x1b[2J` to clear screen) and confirming the debug output shows the literal escape sequence, not the control effect.
LLM consensus
User-controlled value printed to terminal without ANSI escape sanitization. Malicious input can inject cursor-control sequences, rewrite earlier output, or hide shell commands from the operator.
Evidence
| 1691 | if (typeof v === "string" && v.includes("?")) { |
| 1692 | const cleaned = sanitizeSingleImageUrl(v); |
| 1693 | if (cleaned !== v) { |
| 1694 | console.error(`[sanitize] ${toolName}: stripped query-string from ${field}`); |
| 1695 | args[field] = cleaned; |
| 1696 | } |
| 1697 | } |
RemediationAI
The problem is that `toolName` and `field` are user-controlled values printed to stderr without ANSI escape sanitization. Replace the console.error call with `console.error(`[sanitize] ${stripAnsi(toolName)}: stripped query-string from ${stripAnsi(field)}`)` using the `strip-ansi` package or a custom sanitization function. This prevents ANSI injection attacks that could manipulate terminal display. Verify by injecting ANSI codes into toolName or field and confirming they appear as literal text in the output.
LLM consensus
User-controlled value printed to terminal without ANSI escape sanitization. Malicious input can inject cursor-control sequences, rewrite earlier output, or hide shell commands from the operator.
Evidence
| 4186 | if (result.status >= 200 && result.status < 300 && result.body?.status === "success") { |
| 4187 | console.log(`OK - credentials verified against ${config.apiUrl}`); |
| 4188 | if (result.body.data) { |
| 4189 | console.log(JSON.stringify(result.body.data, null, 2)); |
| 4190 | } |
| 4191 | process.exit(0); |
| 4192 | } else { |
RemediationAI
The problem is that `result.body.data` is user-controlled API response data printed directly via `JSON.stringify()` without ANSI escape sanitization. Replace `console.log(JSON.stringify(result.body.data, null, 2))` with `console.log(stripAnsi(JSON.stringify(result.body.data, null, 2)))` using the `strip-ansi` package. This prevents malicious API responses from injecting terminal control sequences. Verify by mocking the API to return a response containing ANSI escape codes and confirming they are displayed as literal text.
LLM consensus
User-controlled value printed to terminal without ANSI escape sanitization. Malicious input can inject cursor-control sequences, rewrite earlier output, or hide shell commands from the operator.
Evidence
| 5618 | shuttingDown = true; |
| 5619 | const inflight = IN_FLIGHT_REQUESTS.size; |
| 5620 | if (inflight > 0) { |
| 5621 | console.error(`[${signal}] Draining ${inflight} in-flight request(s)...`); |
| 5622 | // 5000ms drain window on SIGTERM/SIGINT. Enough for most BD calls to |
| 5623 | // complete cleanly (BD median response is <500ms). Hard abort after |
| 5624 | // 5s so the user's MCP client doesn't hang waiting for us to exit. |
RemediationAI
The problem is that `signal` and `inflight` (a number) are printed to stderr without sanitizing `signal`, which could be user-influenced in some contexts. Replace `console.error(`[${signal}] Draining ${inflight} in-flight request(s)...`)` with `console.error(`[${stripAnsi(String(signal))}] Draining ${inflight} in-flight request(s)...`)` using the `strip-ansi` package. This prevents ANSI injection if signal is ever attacker-controlled. Verify by confirming the output displays the signal name as plain text.
LLM consensus
User-controlled value printed to terminal without ANSI escape sanitization. Malicious input can inject cursor-control sequences, rewrite earlier output, or hide shell commands from the operator.
Evidence
| 4191 | process.exit(0); |
| 4192 | } else { |
| 4193 | console.error(`FAIL - HTTP ${result.status}`); |
| 4194 | console.error(typeof result.body === "string" ? result.body : JSON.stringify(result.body, null, 2)); |
| 4195 | process.exit(2); |
| 4196 | } |
| 4197 | } catch (err) { |
RemediationAI
The problem is that `result.body` (a user-controlled API response) is printed directly via `JSON.stringify()` without ANSI escape sanitization. Replace `console.error(typeof result.body === "string" ? result.body : JSON.stringify(result.body, null, 2))` with `console.error(stripAnsi(typeof result.body === "string" ? result.body : JSON.stringify(result.body, null, 2)))` using the `strip-ansi` package. This prevents malicious API responses from injecting terminal control sequences. Verify by mocking an error response with embedded ANSI codes and confirming they appear as literal text.
LLM consensus
User-controlled value printed to terminal without ANSI escape sanitization. Malicious input can inject cursor-control sequences, rewrite earlier output, or hide shell commands from the operator.
Evidence
| 1680 | const orig = args.post_image; |
| 1681 | const cleaned = orig.split(",").map(sanitizeSingleImageUrl).filter(Boolean).join(","); |
| 1682 | if (cleaned !== orig) { |
| 1683 | console.error(`[sanitize] ${toolName}: stripped query-string or whitespace from post_image CSV`); |
| 1684 | args.post_image = cleaned; |
| 1685 | } |
| 1686 | return; |
RemediationAI
The problem is that `toolName` is printed to stderr without ANSI escape sanitization. Replace `console.error(`[sanitize] ${toolName}: stripped query-string or whitespace from post_image CSV`)` with `console.error(`[sanitize] ${stripAnsi(toolName)}: stripped query-string or whitespace from post_image CSV`)` using the `strip-ansi` package. This prevents ANSI injection attacks. Verify by passing a toolName with embedded ANSI codes and confirming they appear as literal text in the output.
LLM consensus
User-controlled value printed to terminal without ANSI escape sanitization. Malicious input can inject cursor-control sequences, rewrite earlier output, or hide shell commands from the operator.
Evidence
| 4075 | console.log("OK"); |
| 4076 | } else { |
| 4077 | console.log("FAILED"); |
| 4078 | console.log(` HTTP ${result.status}: ${JSON.stringify(result.body)}`); |
| 4079 | console.log(""); |
| 4080 | console.log("Setup will still write the config, but your AI agent won't be able to call the API until this is fixed."); |
| 4081 | if (autoYes || nonInteractive) { |
RemediationAI
The problem is that `result.body` (user-controlled API response) is printed directly via `JSON.stringify()` without ANSI escape sanitization. Replace `console.log(` HTTP ${result.status}: ${JSON.stringify(result.body)}`)` with `console.log(` HTTP ${result.status}: ${stripAnsi(JSON.stringify(result.body))}`)` using the `strip-ansi` package. This prevents malicious API responses from injecting terminal control sequences. Verify by mocking an error response with embedded ANSI codes and confirming they appear as literal text.
LLM consensus
User-controlled value printed to terminal without ANSI escape sanitization. Malicious input can inject cursor-control sequences, rewrite earlier output, or hide shell commands from the operator.
Evidence
| 3885 | : ""; |
| 3886 | console.error(`[debug] -> ${method} ${fullUrl.href}`); |
| 3887 | console.error(`[debug] headers: ${JSON.stringify(safeHeaders)}`); |
| 3888 | if (safeBody) console.error(`[debug] body: ${safeBody}`); |
| 3889 | } |
| 3890 | |
| 3891 | const req = transport.request(options, (res) => { |
RemediationAI
The problem is that `fullUrl.href` and `safeBody` are printed to stderr without ANSI escape sanitization. Replace the console.error calls with `console.error(`[debug] -> ${stripAnsi(method)} ${stripAnsi(fullUrl.href)}`)` and `if (safeBody) console.error(`[debug] body: ${stripAnsi(safeBody)}`)` using the `strip-ansi` package. This prevents ANSI injection attacks. Verify by passing a URL with embedded ANSI codes and confirming they appear as literal text in the debug output.
LLM consensus
Network / IO / subprocess call without an explicit timeout. A malicious or hung upstream (HTTP host, socket peer, child process) can pin threads, exhaust connection/process pools, and make the MCP server unresponsive. Always pass a bounded timeout. v2 extends v1 with subprocess coverage (R03 from the legacy readiness audit).
Evidence
| 1922 | if (cached && (Date.now() - cached.fetchedAt) < POST_TYPES_TTL_MS) return cached.website_id; |
| 1923 | try { |
| 1924 | const url = new URL(`/api/v2/site_info/get`, `https://${domain}`); |
| 1925 | const resp = await fetch(url.toString(), { method: "GET", headers: { "X-Api-Key": apiKey, Accept: "application/json" } }); |
| 1926 | if (!resp.ok) return null; |
| 1927 | const body = await resp.json().catch(() => null); |
| 1928 | const msg = body && body.message; |
RemediationAI
The problem is that the fetch call to `/api/v2/site_info/get` has no timeout, allowing a hung upstream server to block indefinitely. Add a `signal` parameter using `AbortController` with a timeout: `const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 5000); const resp = await fetch(url.toString(), { method: "GET", headers: {...}, signal: controller.signal }); clearTimeout(timeout);`. This ensures the request fails fast if the upstream server is unresponsive. Verify by mocking a slow server and confirming the request aborts after 5 seconds.
LLM consensus
Network / IO / subprocess call without an explicit timeout. A malicious or hung upstream (HTTP host, socket peer, child process) can pin threads, exhaust connection/process pools, and make the MCP server unresponsive. Always pass a bounded timeout. v2 extends v1 with subprocess coverage (R03 from the legacy readiness audit).
Evidence
| 1954 | url.searchParams.append("property_operator[]", "="); |
| 1955 | url.searchParams.append("property_operator[]", "="); |
| 1956 | url.searchParams.set("limit", "100"); |
| 1957 | const resp = await fetch(url.toString(), { method: "GET", headers: { "X-Api-Key": apiKey, Accept: "application/json" } }); |
| 1958 | if (!resp.ok) return {}; |
| 1959 | const body = await resp.json().catch(() => null); |
| 1960 | const rows = (body && Array.isArray(body.message)) ? body.message : []; |
RemediationAI
The problem is that the fetch call to query properties has no timeout, allowing a hung upstream server to block indefinitely. Add a `signal` parameter using `AbortController` with a timeout: `const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 5000); const resp = await fetch(url.toString(), { method: "GET", headers: {...}, signal: controller.signal }); clearTimeout(timeout);`. This ensures the request fails fast if the upstream server is unresponsive. Verify by mocking a slow server and confirming the request aborts after 5 seconds.
LLM consensus
Network / IO / subprocess call without an explicit timeout. A malicious or hung upstream (HTTP host, socket peer, child process) can pin threads, exhaust connection/process pools, and make the MCP server unresponsive. Always pass a bounded timeout. v2 extends v1 with subprocess coverage (R03 from the legacy readiness audit).
Evidence
| 2075 | url.searchParams.append("property_operator[]", "="); |
| 2076 | url.searchParams.append("property_operator[]", "="); |
| 2077 | url.searchParams.set("limit", "100"); |
| 2078 | const resp = await fetch(url.toString(), { method: "GET", headers: { "X-Api-Key": apiKey, Accept: "application/json" } }); |
| 2079 | let body = null; |
| 2080 | try { body = await resp.json(); } catch {} |
| 2081 | if (!body) return { probed_ok: false, deleted, reason: `probe returned non-JSON (HTTP ${resp.status})` }; |
RemediationAI
The problem is that the fetch call to query properties has no timeout, allowing a hung upstream server to block indefinitely. Add a `signal` parameter using `AbortController` with a timeout: `const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 5000); const resp = await fetch(url.toString(), { method: "GET", headers: {...}, signal: controller.signal }); clearTimeout(timeout);`. This ensures the request fails fast if the upstream server is unresponsive. Verify by mocking a slow server and confirming the request aborts after 5 seconds.
LLM consensus
Network / IO / subprocess call without an explicit timeout. A malicious or hung upstream (HTTP host, socket peer, child process) can pin threads, exhaust connection/process pools, and make the MCP server unresponsive. Always pass a bounded timeout. v2 extends v1 with subprocess coverage (R03 from the legacy readiness audit).
Evidence
| 2201 | const form = new URLSearchParams(); |
| 2202 | form.set("redirect_id", redirectId); |
| 2203 | form.set("new_filename", newFilename); |
| 2204 | const resp = await fetch(url.toString(), { |
| 2205 | method: "PUT", |
| 2206 | headers: { "X-Api-Key": apiKey, Accept: "application/json", "Content-Type": "application/x-www-form-urlencoded" }, |
| 2207 | body: form.toString(), |
RemediationAI
The problem is that the fetch call to PUT a redirect has no timeout, allowing a hung upstream server to block indefinitely. Add a `signal` parameter using `AbortController` with a timeout: `const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 5000); const resp = await fetch(url.toString(), { method: "PUT", headers: {...}, body: form, signal: controller.signal }); clearTimeout(timeout);`. This ensures the request fails fast if the upstream server is unresponsive. Verify by mocking a slow server and confirming the request aborts after 5 seconds.
LLM consensus
Network / IO / subprocess call without an explicit timeout. A malicious or hung upstream (HTTP host, socket peer, child process) can pin threads, exhaust connection/process pools, and make the MCP server unresponsive. Always pass a bounded timeout. v2 extends v1 with subprocess coverage (R03 from the legacy readiness audit).
Evidence
| 2035 | form.set("meta_id", String(r.meta_id)); |
| 2036 | form.set("database", "list_seo"); |
| 2037 | form.set("database_id", String(seoId)); |
| 2038 | const dr = await fetch(delUrl.toString(), { |
| 2039 | method: "DELETE", |
| 2040 | headers: { |
| 2041 | "X-Api-Key": apiKey, |
RemediationAI
The problem is that the fetch call to DELETE a record has no timeout, allowing a hung upstream server to block indefinitely. Add a `signal` parameter using `AbortController` with a timeout: `const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 5000); const dr = await fetch(delUrl.toString(), { method: "DELETE", headers: {...}, signal: controller.signal }); clearTimeout(timeout);`. This ensures the request fails fast if the upstream server is unresponsive. Verify by mocking a slow server and confirming the request aborts after 5 seconds.
LLM consensus
Network / IO / subprocess call without an explicit timeout. A malicious or hung upstream (HTTP host, socket peer, child process) can pin threads, exhaust connection/process pools, and make the MCP server unresponsive. Always pass a bounded timeout. v2 extends v1 with subprocess coverage (R03 from the legacy readiness audit).
Evidence
| 2097 | form.set("meta_id", String(r.meta_id)); |
| 2098 | form.set("database", String(database)); |
| 2099 | form.set("database_id", String(databaseId)); |
| 2100 | const dr = await fetch(delUrl.toString(), { |
| 2101 | method: "DELETE", |
| 2102 | headers: { "X-Api-Key": apiKey, Accept: "application/json", "Content-Type": "application/x-www-form-urlencoded" }, |
| 2103 | body: form.toString(), |
RemediationAI
The problem is that the fetch call to DELETE a record has no timeout, allowing a hung upstream server to block indefinitely. Add a `signal` parameter using `AbortController` with a timeout: `const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 5000); const dr = await fetch(delUrl.toString(), { method: "DELETE", headers: {...}, signal: controller.signal }); clearTimeout(timeout);`. This ensures the request fails fast if the upstream server is unresponsive. Verify by mocking a slow server and confirming the request aborts after 5 seconds.
LLM consensus
Network / IO / subprocess call without an explicit timeout. A malicious or hung upstream (HTTP host, socket peer, child process) can pin threads, exhaust connection/process pools, and make the MCP server unresponsive. Always pass a bounded timeout. v2 extends v1 with subprocess coverage (R03 from the legacy readiness audit).
Evidence
| 4368 | }; |
| 4369 | // If any BD fetches failed outright (network / non-200), surface it so the |
| 4370 | // agent knows some values are fallbacks, not live site data. A missing row |
| 4371 | // on a successful fetch (slot just isn't set) is NOT a failure - fallback |
| 4372 | // silently applies; that's the documented design. |
| 4373 | if (failedSlots.length > 0) { |
| 4374 | kit._warnings = [ |
RemediationAI
The problem is that one or more fetch calls in this section have no timeout, allowing a hung upstream server to block indefinitely. Add a `signal` parameter using `AbortController` with a timeout to all fetch calls: `const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 5000); const resp = await fetch(url, { ..., signal: controller.signal }); clearTimeout(timeout);`. This ensures requests fail fast if the upstream server is unresponsive. Verify by mocking a slow server and confirming the request aborts after 5 seconds.
LLM consensus
Network / IO / subprocess call without an explicit timeout. A malicious or hung upstream (HTTP host, socket peer, child process) can pin threads, exhaust connection/process pools, and make the MCP server unresponsive. Always pass a bounded timeout. v2 extends v1 with subprocess coverage (R03 from the legacy readiness audit).
Evidence
| 2002 | async function _readSeoTypeForId(domain, apiKey, seoId) { |
| 2003 | try { |
| 2004 | const url = new URL(`/api/v2/list_seo/get/${encodeURIComponent(String(seoId))}`, `https://${domain}`); |
| 2005 | const resp = await fetch(url.toString(), { method: "GET", headers: { "X-Api-Key": apiKey, Accept: "application/json" } }); |
| 2006 | if (!resp.ok) return null; |
| 2007 | const body = await resp.json().catch(() => null); |
| 2008 | const row = body && body.message && (Array.isArray(body.message) ? body.message[0] : body.message); |
RemediationAI
The problem is that the fetch call to `/api/v2/list_seo/get/{seoId}` has no timeout, allowing a hung upstream server to block indefinitely. Add a `signal` parameter using `AbortController` with a timeout: `const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 5000); const resp = await fetch(url.toString(), { method: "GET", headers: {...}, signal: controller.signal }); clearTimeout(timeout);`. This ensures the request fails fast if the upstream server is unresponsive. Verify by mocking a slow server and confirming the request aborts after 5 seconds.
LLM consensus
Network / IO / subprocess call without an explicit timeout. A malicious or hung upstream (HTTP host, socket peer, child process) can pin threads, exhaust connection/process pools, and make the MCP server unresponsive. Always pass a bounded timeout. v2 extends v1 with subprocess coverage (R03 from the legacy readiness audit).
Evidence
| 2181 | form.set("new_filename", newFilename); |
| 2182 | form.set("type", "custom"); |
| 2183 | form.set("db_id", String(seoId)); |
| 2184 | const resp = await fetch(url.toString(), { |
| 2185 | method: "POST", |
| 2186 | headers: { "X-Api-Key": apiKey, Accept: "application/json", "Content-Type": "application/x-www-form-urlencoded" }, |
| 2187 | body: form.toString(), |
RemediationAI
The problem is that the fetch call to POST a new record has no timeout, allowing a hung upstream server to block indefinitely. Add a `signal` parameter using `AbortController` with a timeout: `const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 5000); const resp = await fetch(url.toString(), { method: "POST", headers: {...}, body: form, signal: controller.signal }); clearTimeout(timeout);`. This ensures the request fails fast if the upstream server is unresponsive. Verify by mocking a slow server and confirming the request aborts after 5 seconds.
LLM consensus
Network / IO / subprocess call without an explicit timeout. A malicious or hung upstream (HTTP host, socket peer, child process) can pin threads, exhaust connection/process pools, and make the MCP server unresponsive. Always pass a bounded timeout. v2 extends v1 with subprocess coverage (R03 from the legacy readiness audit).
Evidence
| 2022 | url.searchParams.append("property_operator[]", "="); |
| 2023 | url.searchParams.append("property_operator[]", "="); |
| 2024 | url.searchParams.set("limit", "100"); |
| 2025 | const resp = await fetch(url.toString(), { method: "GET", headers: { "X-Api-Key": apiKey, Accept: "application/json" } }); |
| 2026 | if (!resp.ok) return { probed_ok: false, deleted, reason: `probe HTTP ${resp.status}` }; |
| 2027 | const body = await resp.json().catch(() => null); |
| 2028 | if (!body || body.status !== "success") return { probed_ok: false, del |
RemediationAI
The problem is that the fetch call to query properties has no timeout, allowing a hung upstream server to block indefinitely. Add a `signal` parameter using `AbortController` with a timeout: `const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 5000); const resp = await fetch(url.toString(), { method: "GET", headers: {...}, signal: controller.signal }); clearTimeout(timeout);`. This ensures the request fails fast if the upstream server is unresponsive. Verify by mocking a slow server and confirming the request aborts after 5 seconds.
LLM consensus
Network / IO / subprocess call without an explicit timeout. A malicious or hung upstream (HTTP host, socket peer, child process) can pin threads, exhaust connection/process pools, and make the MCP server unresponsive. Always pass a bounded timeout. v2 extends v1 with subprocess coverage (R03 from the legacy readiness audit).
Evidence
| 2218 | const url = new URL(`/api/v2/redirect_301/delete`, `https://${domain}`); |
| 2219 | const form = new URLSearchParams(); |
| 2220 | form.set("redirect_id", existing.redirect_id); |
| 2221 | const resp = await fetch(url.toString(), { |
| 2222 | method: "DELETE", |
| 2223 | headers: { "X-Api-Key": apiKey, Accept: "application/json", "Content-Type": "application/x-www-form-urlencoded" }, |
| 2224 | body: form.toString(), |
RemediationAI
The problem is that the fetch call to DELETE a redirect has no timeout, allowing a hung upstream server to block indefinitely. Add a `signal` parameter using `AbortController` with a timeout: `const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 5000); const resp = await fetch(url.toString(), { method: "DELETE", headers: {...}, body: form, signal: controller.signal }); clearTimeout(timeout);`. This ensures the request fails fast if the upstream server is unresponsive. Verify by mocking a slow server and confirming the request aborts after 5 seconds.
LLM consensus
Network / IO / subprocess call without an explicit timeout. A malicious or hung upstream (HTTP host, socket peer, child process) can pin threads, exhaust connection/process pools, and make the MCP server unresponsive. Always pass a bounded timeout. v2 extends v1 with subprocess coverage (R03 from the legacy readiness audit).
Evidence
| 1980 | url.searchParams.append("property_operator[]", "="); |
| 1981 | url.searchParams.append("property_operator[]", "="); |
| 1982 | url.searchParams.set("limit", "100"); |
| 1983 | const resp = await fetch(url.toString(), { method: "GET", headers: { "X-Api-Key": apiKey, Accept: "application/json" } }); |
| 1984 | if (!resp.ok) return null; |
| 1985 | const body = await resp.json().catch(() => null); |
| 1986 | const rows = (body && Array.isArray(body.message)) ? body.message : []; |
RemediationAI
The problem is that the fetch call to query properties has no timeout, allowing a hung upstream server to block indefinitely. Add a `signal` parameter using `AbortController` with a timeout: `const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 5000); const resp = await fetch(url.toString(), { method: "GET", headers: {...}, signal: controller.signal }); clearTimeout(timeout);`. This ensures the request fails fast if the upstream server is unresponsive. Verify by mocking a slow server and confirming the request aborts after 5 seconds.
LLM consensus
Network / IO / subprocess call without an explicit timeout. A malicious or hung upstream (HTTP host, socket peer, child process) can pin threads, exhaust connection/process pools, and make the MCP server unresponsive. Always pass a bounded timeout. v2 extends v1 with subprocess coverage (R03 from the legacy readiness audit).
Evidence
| 1143 | **Exceptions to the HTML-allowed rules:** |
| 1144 | |
| 1145 | - **Email body โ no `<style>` blocks.** Outlook strips them. Use inline `style=""` attributes only. See **Rule: Email template recipe** for full email-client constraints. |
| 1146 | - **Widget exception:** `widget_data`, `widget_style`, `widget_javascript` are exempt from all listed patterns. Widgets legitimately need JS and scoped CSS, and anyone with API permission to write widgets already has admin capability. Warn (but do NOT block) if widget_javascript contai |
RemediationAI
The problem is that the documentation does not show evidence of a timeout being applied to network calls. Review all fetch() calls in index.js and add explicit timeouts using AbortController with a 5-second limit to every network request. This prevents hung upstream servers from blocking the MCP server indefinitely. Verify by running the server against a slow/unresponsive upstream and confirming all requests abort after 5 seconds.
Network / IO / subprocess call without an explicit timeout. A malicious or hung upstream (HTTP host, socket peer, child process) can pin threads, exhaust connection/process pools, and make the MCP server unresponsive. Always pass a bounded timeout. v2 extends v1 with subprocess coverage (R03 from the legacy readiness audit).
Evidence
| 1907 | try { |
| 1908 | const url = new URL(`/api/v2/data_categories/get`, `https://${domain}`); |
| 1909 | url.searchParams.set("limit", "100"); |
| 1910 | const resp = await fetch(url.toString(), { method: "GET", headers: { "X-Api-Key": apiKey, Accept: "application/json" } }); |
| 1911 | if (!resp.ok) return null; |
| 1912 | const body = await resp.json().catch(() => null); |
| 1913 | const rows = (body && Array.isArray(body.message)) ? body.message : []; |
RemediationAI
The problem is that the fetch call to `/api/v2/data_categories/get` has no timeout, allowing a hung upstream server to block indefinitely. Add a `signal` parameter using `AbortController` with a timeout: `const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 5000); const resp = await fetch(url.toString(), { method: "GET", headers: {...}, signal: controller.signal }); clearTimeout(timeout);`. This ensures the request fails fast if the upstream server is unresponsive. Verify by mocking a slow server and confirming the request aborts after 5 seconds.
LLM consensus
Network / IO / subprocess call without an explicit timeout. A malicious or hung upstream (HTTP host, socket peer, child process) can pin threads, exhaust connection/process pools, and make the MCP server unresponsive. Always pass a bounded timeout. v2 extends v1 with subprocess coverage (R03 from the legacy readiness audit).
Evidence
| 2156 | url.searchParams.append("property_value[]", oldFilename); |
| 2157 | url.searchParams.append("property_operator[]", "="); |
| 2158 | url.searchParams.set("limit", "1"); |
| 2159 | const resp = await fetch(url.toString(), { method: "GET", headers: { "X-Api-Key": apiKey, Accept: "application/json" } }); |
| 2160 | if (!resp.ok) return null; |
| 2161 | const body = await resp.json().catch(() => null); |
| 2162 | const rows = (body && Array.isArray(body.message)) ? body.message : []; |
RemediationAI
The problem is that the fetch call to query properties by filename has no timeout, allowing a hung upstream server to block indefinitely. Add a `signal` parameter using `AbortController` with a timeout: `const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 5000); const resp = await fetch(url.toString(), { method: "GET", headers: {...}, signal: controller.signal }); clearTimeout(timeout);`. This ensures the request fails fast if the upstream server is unresponsive. Verify by mocking a slow server and confirming the request aborts after 5 seconds.
LLM consensus
MCP manifest declares tools but no authentication field is present (none of: auth, authorization, bearer, oauth, mtls, apiKey, api_key, basic, token, authToken). Absence is a weak signal โ confirm whether the server relies on network-layer or host-level auth, or declare the real mechanism explicitly so reviewers can audit it.
Evidence
| 1 | # Official Brilliant Directories MCP Server โ Setup Guide |
| 2 | |
| 3 | [](https://www.npmjs.com/package/brilliant-directories-mcp) |
| 4 | [](LICENSE) |
| 5 | [](https://modelcontextprotocol.io) |
| 6 | |
| 7 | Universal AI integration for your BD site. Give any AI agent full access to y |
RemediationAI
The problem is that the README does not declare an authentication mechanism for the MCP server, making it unclear how callers are authenticated and authorized. Add an explicit `authentication` field to the MCP manifest (or a new section in README.md) that documents the auth method, such as: `"authentication": { "type": "apiKey", "description": "Requires BD_API_KEY and BD_SITE_URL environment variables" }`. This allows reviewers to audit the security model. Verify by confirming the manifest or README clearly states how authentication is enforced.
Brilliant Directories MCP server auto-generates tools from OpenAPI spec and may emit notifications/tools/list_changed, but the source does not show per-tool version/etag/digest fields in the tools/list response to detect tool content changes.
Evidence
| 1 | #!/usr/bin/env node |
| 2 | |
| 3 | /** |
| 4 | * Brilliant Directories MCP Server (npm package โ stdio transport) |
RemediationAI
The problem is that the tools/list response does not include per-tool version, etag, or digest fields, making it impossible for clients to detect when tool definitions have changed. Add a `version` or `digest` field to each tool in the tools/list response (e.g., computed as a hash of the tool's schema), and increment it whenever the OpenAPI spec is reloaded. This allows clients to detect tool content changes. Verify by reloading the OpenAPI spec and confirming the digest changes in the tools/list response.
LLM consensus
Time-of-check-to-time-of-use race. Code calls `os.path.exists` / `fs.existsSync` to check a path, then `open` / `readFileSync` / `unlink` on the same name within a few lines โ without a lock or atomic-open. An attacker who can race the filesystem (symlink, file replacement) between the check and the use gets the action applied to a different target. Replace the check-then-use pattern with the action's own error handling: try the open and catch FileNotFoundError / ENOENT. For atomic creation use
Evidence
| 1 | #!/usr/bin/env node |
| 2 | |
| 3 | /** |
| 4 | * Brilliant Directories MCP Server (npm package โ stdio transport) |
| 5 | * |
| 6 | * Exposes all BD API v2 endpoints as MCP tools. Reads the OpenAPI spec and |
| 7 | * auto-generates tool definitions. Runs as a child process launched by the |
| 8 | * user's MCP-capable AI client (Claude Desktop / Cursor / Claude Code). |
| 9 | * |
| 10 | * Usage: |
| 11 | * brilliant-directories-mcp --api-key YOUR_KEY --url https://your-site.com |
| 12 | * |
| 13 | * Or via env vars: |
| 14 | * BD_API_KEY=YOUR_KEY BD_SITE_URL=https://your-site.com brilli |
RemediationAI
The problem is that the code may check if a file exists with `fs.existsSync()` and then open/read/delete it without a lock, allowing an attacker to race a symlink or file replacement between the check and the use. Replace the check-then-use pattern with direct error handling: instead of `if (fs.existsSync(path)) { fs.readFileSync(path) }`, use `try { fs.readFileSync(path) } catch (e) { if (e.code !== 'ENOENT') throw e; }`. This eliminates the race window. Verify by creating a test that attempts to race a symlink replacement and confirming the operation either succeeds atomically or fails safely.
LLM consensus
MCP tool file registers a tool, performs a destructive sink (fs.unlink / shutil.rmtree / DROP TABLE / DELETE FROM / TRUNCATE / UPDATE ... SET / HTTP DELETE|PUT|PATCH / subprocess / exec / spawn), and emits no audit event anywhere in the file. Without an audit event, an investigator cannot answer "who deleted record X on day Y?" โ the irreversible action leaves no trail. Closes the OWASP MCP Top 10:2025 MCP08 (Lack of Audit and Telemetry) gap. Distinct from MCP-201 (no confirmation) and MCP-283
Evidence
| 1 | #!/usr/bin/env node |
| 2 | |
| 3 | /** |
| 4 | * Brilliant Directories MCP Server (npm package โ stdio transport) |
| 5 | * |
| 6 | * Exposes all BD API v2 endpoints as MCP tools. Reads the OpenAPI spec and |
| 7 | * auto-generates tool definitions. Runs as a child process launched by the |
| 8 | * user's MCP-capable AI client (Claude Desktop / Cursor / Claude Code). |
| 9 | * |
| 10 | * Usage: |
| 11 | * brilliant-directories-mcp --api-key YOUR_KEY --url https://your-site.com |
| 12 | * |
| 13 | * Or via env vars: |
| 14 | * BD_API_KEY=YOUR_KEY BD_SITE_URL=https://your-site.com brilli |
RemediationAI
The problem is that destructive operations (DELETE, PUT, PATCH, unlink) are performed without emitting any audit event, leaving no trail for incident response. Add audit logging to every destructive sink: before executing the operation, call an audit function like `auditLog({ action: 'DELETE', tool: toolName, target: recordId, timestamp: Date.now(), caller: requestContext.userId })` and write it to a secure audit log file or external service. This enables investigators to answer "who deleted record X on day Y?". Verify by performing a destructive operation and confirming an audit entry is written with the caller identity, action, and timestamp.
LLM consensus
Silent error swallowing detected. An except clause that does pass or ... discards the exception with no log, no metric, and no trace. This blinds incident response and hides real failures.
Evidence
| 2077 | url.searchParams.set("limit", "100"); |
| 2078 | const resp = await fetch(url.toString(), { method: "GET", headers: { "X-Api-Key": apiKey, Accept: "application/json" } }); |
| 2079 | let body = null; |
| 2080 | try { body = await resp.json(); } catch {} |
| 2081 | if (!body) return { probed_ok: false, deleted, reason: `probe returned non-JSON (HTTP ${resp.status})` }; |
| 2082 | // BD returns HTTP 400 + status:error on legitimate empty results โ that's |
| 2083 | // a clean "no orphans" not a probe failure. |
RemediationAI
The problem is that the `try { body = await resp.json(); } catch {}` silently swallows JSON parsing errors with no log, metric, or trace, blinding incident response. Replace with `try { body = await resp.json(); } catch (e) { console.error(`[error] Failed to parse JSON response: ${e.message}`); }` to log the error. This ensures failures are visible for debugging and monitoring. Verify by passing invalid JSON in a response and confirming an error message is logged to stderr.
LLM consensus