blackveil-dns 2.10.8 → 2.10.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -9,7 +9,7 @@ Open-source DNS & email security scanner for Claude, Cursor, VS Code, and MCP cl
9
9
  [![GitHub stars](https://img.shields.io/github/stars/MadaBurns/bv-mcp?style=flat&logo=github)](https://github.com/MadaBurns/bv-mcp/stargazers)
10
10
  [![npm version](https://img.shields.io/npm/v/blackveil-dns)](https://www.npmjs.com/package/blackveil-dns)
11
11
  [![npm downloads](https://img.shields.io/npm/dm/blackveil-dns)](https://www.npmjs.com/package/blackveil-dns)
12
- [![Tests](https://img.shields.io/badge/Tests-2587-brightgreen)](https://github.com/MadaBurns/bv-mcp/actions)
12
+ [![Tests](https://img.shields.io/badge/Tests-2610-brightgreen)](https://github.com/MadaBurns/bv-mcp/actions)
13
13
  [![Coverage](https://img.shields.io/badge/Coverage-~90%25-brightgreen)](https://github.com/MadaBurns/bv-mcp/actions)
14
14
  [![BUSL-1.1 License](https://img.shields.io/badge/License-BUSL--1.1-blue.svg)](LICENSE)
15
15
  [![MCP](https://img.shields.io/badge/MCP-2025--03--26-blue)](https://modelcontextprotocol.io/)
package/dist/index.d.ts CHANGED
@@ -192,7 +192,7 @@ declare function sanitizeDomain(input: string): string;
192
192
  declare function sanitizeInput(input: string, maxLength?: number): string;
193
193
 
194
194
  /** Server version — keep in sync with package.json */
195
- declare const SERVER_VERSION = "2.10.8";
195
+ declare const SERVER_VERSION = "2.10.10";
196
196
 
197
197
  /**
198
198
  * Map of every tool name to its Zod argument schema.
@@ -231,6 +231,11 @@ declare const TOOLS: McpTool[];
231
231
  * Check BIMI records for a domain.
232
232
  * Validates the presence and configuration of BIMI TXT records,
233
233
  * including logo URL format and VMC authority evidence.
234
+ *
235
+ * BIMI `l=` and `a=` tags are extracted from a TXT record at default._bimi.<domain>
236
+ * and are entirely attacker-controlled. We pass safeFetch instead of the raw
237
+ * `fetch` so the destination hostname is validated before any outbound request
238
+ * (H2 fix from the 2026-05-08 security audit).
234
239
  */
235
240
  declare function checkBimi(domain: string, dnsOptions?: QueryDnsOptions): Promise<CheckResult>;
236
241
 
package/dist/index.js CHANGED
@@ -105,7 +105,7 @@ z.object({
105
105
  var REDACTED = "[redacted]";
106
106
  var MAX_LOG_STRING_LENGTH = 256;
107
107
  var MAX_ERROR_STRING_LENGTH = 1024;
108
- var SENSITIVE_KEY_PATTERN = /(^ip$|authorization|mcp-session-id|session|token|api[-_]?key|secret|password|cookie|rawbody)/i;
108
+ var SENSITIVE_KEY_PATTERN = /(^ip$|cf-connecting-ip|authorization|mcp-session-id|session|token|api[-_]?key|secret|password|cookie|rawbody)/i;
109
109
  function isSensitiveKey(key) {
110
110
  return !/^has[A-Z]/.test(key) && SENSITIVE_KEY_PATTERN.test(key);
111
111
  }
@@ -580,6 +580,24 @@ function sanitizeDomain(input) {
580
580
  return "";
581
581
  }
582
582
  }
583
+ function validateOutboundUrl(input) {
584
+ if (!input || typeof input !== "string") {
585
+ return { valid: false, error: "URL is required" };
586
+ }
587
+ let url;
588
+ try {
589
+ url = new URL(input);
590
+ } catch {
591
+ return { valid: false, error: "URL is malformed" };
592
+ }
593
+ if (url.protocol !== "https:") {
594
+ return { valid: false, error: `URL must use https (got "${url.protocol}")` };
595
+ }
596
+ if (url.username || url.password) {
597
+ return { valid: false, error: "URL must not contain userinfo" };
598
+ }
599
+ return validateDomain(url.hostname);
600
+ }
583
601
  function sanitizeInput(input, maxLength = 500) {
584
602
  if (typeof input !== "string") return "";
585
603
  const sanitized = input.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, "");
@@ -587,7 +605,7 @@ function sanitizeInput(input, maxLength = 500) {
587
605
  }
588
606
 
589
607
  // src/lib/server-version.ts
590
- var SERVER_VERSION = "2.10.8";
608
+ var SERVER_VERSION = "2.10.10";
591
609
  var DomainSchema = z.string().min(1).max(253);
592
610
  z.string().regex(/^[0-9a-f]{64}$/);
593
611
  var DkimSelectorSchema = z.string().transform((s) => s.trim().toLowerCase()).pipe(z.string().max(63).regex(/^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/));
@@ -682,7 +700,7 @@ var GetBenchmarkArgs = z.object({
682
700
  format: FormatSchema.optional().describe("Output verbosity. Auto-detected if omitted.")
683
701
  }).passthrough();
684
702
  var GetProviderInsightsArgs = z.object({
685
- provider: z.string().min(1).describe('Provider (e.g., "google workspace").'),
703
+ provider: z.string().min(1).max(200).describe('Provider (e.g., "google workspace").'),
686
704
  profile: BenchmarkProfileSchema.optional().describe('Profile (default "mail_enabled").'),
687
705
  format: FormatSchema.optional().describe("Output verbosity. Auto-detected if omitted.")
688
706
  }).passthrough();
@@ -1143,12 +1161,27 @@ function makeQueryDNS(dnsOptions) {
1143
1161
  };
1144
1162
  }
1145
1163
 
1164
+ // src/lib/safe-fetch.ts
1165
+ function urlOf(input) {
1166
+ if (typeof input === "string") return input;
1167
+ if (input instanceof URL) return input.href;
1168
+ return input.url;
1169
+ }
1170
+ var safeFetch = async (input, init) => {
1171
+ const url = urlOf(input);
1172
+ const validation = validateOutboundUrl(url);
1173
+ if (!validation.valid) {
1174
+ throw new TypeError(`Outbound fetch blocked: ${validation.error ?? "invalid URL"}`);
1175
+ }
1176
+ return fetch(input, init);
1177
+ };
1178
+
1146
1179
  // src/tools/check-bimi.ts
1147
1180
  async function checkBimi(domain, dnsOptions) {
1148
1181
  return checkBIMI(
1149
1182
  domain,
1150
1183
  makeQueryDNS(dnsOptions),
1151
- { timeout: dnsOptions?.timeoutMs ?? 5e3, fetchFn: fetch }
1184
+ { timeout: dnsOptions?.timeoutMs ?? 5e3, fetchFn: safeFetch }
1152
1185
  );
1153
1186
  }
1154
1187
  async function checkCaa(domain, dnsOptions) {
@@ -3171,7 +3204,7 @@ async function fetchWithRedirects(url, timeoutMs) {
3171
3204
  }
3172
3205
  if (!nextUrl.startsWith("https://")) break;
3173
3206
  try {
3174
- response = await fetch(nextUrl, {
3207
+ response = await safeFetch(nextUrl, {
3175
3208
  method: "HEAD",
3176
3209
  redirect: "manual",
3177
3210
  headers: { "User-Agent": SCANNER_USER_AGENT },
@@ -3275,7 +3308,7 @@ async function checkHttpSecurityInner(domain) {
3275
3308
  headers: dualResult.headers
3276
3309
  });
3277
3310
  }
3278
- const response = await fetch(input, init);
3311
+ const response = await safeFetch(input, init);
3279
3312
  capturedHeaders = response.headers;
3280
3313
  return response;
3281
3314
  };