@burtthecoder/mcp-dnstwist 1.0.3 → 1.0.5

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
@@ -38,7 +38,7 @@ npx -y @smithery/cli install @burtthecoder/mcp-dnstwist --client claude
38
38
 
39
39
  2. Install the server globally via npm:
40
40
  ```bash
41
- npm install -g mcp-dnstwist
41
+ npm install -g @burtthecoder/mcp-dnstwist
42
42
  ```
43
43
 
44
44
  3. Add to your Claude Desktop configuration file:
package/build/index.js CHANGED
@@ -2,32 +2,16 @@
2
2
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
3
3
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
4
  import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError, } from '@modelcontextprotocol/sdk/types.js';
5
- import { exec } from 'child_process';
5
+ import { execFile } from 'child_process';
6
6
  import { promisify } from 'util';
7
- const execAsync = promisify(exec);
7
+ import { validateFuzzArgs } from './validate.js';
8
+ // execFile runs docker directly with an argument array. No shell is involved,
9
+ // so tool input can never be interpreted as shell syntax (CWE-78, issue #13).
10
+ const execFileAsync = promisify(execFile);
8
11
  const DOCKER_IMAGE = 'elceef/dnstwist';
9
- function isFuzzDomainArgs(args) {
10
- if (!args || typeof args !== 'object')
11
- return false;
12
- const a = args;
13
- return typeof a.domain === 'string' &&
14
- (a.nameservers === undefined || typeof a.nameservers === 'string') &&
15
- (a.threads === undefined || typeof a.threads === 'number') &&
16
- (a.format === undefined || ['json', 'csv', 'list'].includes(a.format)) &&
17
- (a.registered_only === undefined || typeof a.registered_only === 'boolean') &&
18
- (a.mxcheck === undefined || typeof a.mxcheck === 'boolean') &&
19
- (a.ssdeep === undefined || typeof a.ssdeep === 'boolean') &&
20
- (a.banners === undefined || typeof a.banners === 'boolean');
21
- }
22
12
  class DnstwistServer {
23
13
  constructor() {
24
- this.server = new Server({
25
- name: 'dnstwist-server',
26
- version: '0.1.0',
27
- capabilities: {
28
- tools: {}
29
- }
30
- });
14
+ this.server = new Server({ name: 'dnstwist-server', version: '0.1.0' }, { capabilities: { tools: {} } });
31
15
  this.setupToolHandlers();
32
16
  this.server.onerror = (error) => console.error('[MCP Error]', error);
33
17
  process.on('SIGINT', async () => {
@@ -40,10 +24,10 @@ class DnstwistServer {
40
24
  process.exit(1);
41
25
  });
42
26
  }
43
- async execCommand(command) {
44
- console.error('Executing command:', command);
27
+ async execCommand(file, args) {
28
+ console.error('Executing:', file, JSON.stringify(args));
45
29
  try {
46
- const result = await execAsync(command, {
30
+ const result = await execFileAsync(file, args, {
47
31
  maxBuffer: 10 * 1024 * 1024
48
32
  });
49
33
  console.error('Command output:', result.stdout);
@@ -60,19 +44,19 @@ class DnstwistServer {
60
44
  try {
61
45
  console.error('Checking Docker...');
62
46
  try {
63
- await this.execCommand('docker --version');
47
+ await this.execCommand('docker', ['--version']);
64
48
  }
65
49
  catch (error) {
66
50
  throw new Error('Docker is not installed or not running. Please install Docker and try again.');
67
51
  }
68
52
  console.error('Checking if dnstwist image exists...');
69
53
  try {
70
- await this.execCommand(`docker image inspect ${DOCKER_IMAGE}`);
54
+ await this.execCommand('docker', ['image', 'inspect', DOCKER_IMAGE]);
71
55
  console.error('Dnstwist image found');
72
56
  }
73
57
  catch (error) {
74
58
  console.error('Dnstwist image not found, pulling...');
75
- await this.execCommand(`docker pull ${DOCKER_IMAGE}`);
59
+ await this.execCommand('docker', ['pull', DOCKER_IMAGE]);
76
60
  console.error('Dnstwist image pulled successfully');
77
61
  }
78
62
  }
@@ -144,10 +128,7 @@ class DnstwistServer {
144
128
  if (request.params.name !== 'fuzz_domain') {
145
129
  throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${request.params.name}`);
146
130
  }
147
- if (!isFuzzDomainArgs(request.params.arguments)) {
148
- throw new McpError(ErrorCode.InvalidParams, 'Invalid arguments for fuzz_domain');
149
- }
150
- const { domain, nameservers = '1.1.1.1', threads = 50, format = 'json', registered_only = true, mxcheck = true, ssdeep = false, banners = true } = request.params.arguments;
131
+ const { domain, nameservers, threads, format, registered_only, mxcheck, ssdeep, banners } = validateFuzzArgs(request.params.arguments);
151
132
  // Build command arguments
152
133
  const args = [
153
134
  domain,
@@ -166,7 +147,11 @@ class DnstwistServer {
166
147
  if (banners)
167
148
  args.push('-b');
168
149
  // Run dnstwist in Docker
169
- const { stdout, stderr } = await this.execCommand(`docker run --rm ${DOCKER_IMAGE} ${args.join(' ')}`);
150
+ const { stdout } = await this.execCommand('docker', ['run', '--rm', DOCKER_IMAGE, ...args]);
151
+ // csv and list output are returned as dnstwist printed them.
152
+ if (format !== 'json') {
153
+ return { content: [{ type: 'text', text: stdout }] };
154
+ }
170
155
  let results;
171
156
  try {
172
157
  results = JSON.parse(stdout);
@@ -0,0 +1,70 @@
1
+ import { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js';
2
+ import { isIP } from 'net';
3
+ import { domainToASCII } from 'url';
4
+ // A hostname: dot-separated labels of letters, digits and inner hyphens, ending
5
+ // in an alphabetic or punycode TLD. It can't start with "-", so it can't be
6
+ // mistaken for a dnstwist option either.
7
+ const HOSTNAME = /^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+(?:[a-z]{2,63}|xn--[a-z0-9-]{1,59})$/i;
8
+ const FORMATS = ['json', 'csv', 'list'];
9
+ const MAX_NAMESERVERS = 10;
10
+ function invalid(message) {
11
+ throw new McpError(ErrorCode.InvalidParams, `Invalid arguments for fuzz_domain: ${message}`);
12
+ }
13
+ /** Lowercase ASCII (punycode) form of a hostname, or null if it isn't one. */
14
+ function asciiHostname(value) {
15
+ const trimmed = value.trim();
16
+ // domainToASCII silently drops inner tabs and newlines ("a.com\nb" becomes
17
+ // "a.comb"), so reject whitespace and control characters first.
18
+ if (/[\s\x00-\x1f\x7f]/.test(trimmed))
19
+ return null;
20
+ // Handles Unicode (IDN) names; returns '' for invalid input.
21
+ const ascii = domainToASCII(trimmed.replace(/\.$/, ''));
22
+ return HOSTNAME.test(ascii) ? ascii : null;
23
+ }
24
+ function optionalBoolean(a, key, fallback) {
25
+ if (a[key] === undefined)
26
+ return fallback;
27
+ if (typeof a[key] !== 'boolean')
28
+ invalid(`${key} must be a boolean`);
29
+ return a[key];
30
+ }
31
+ /**
32
+ * Validate and normalise fuzz_domain arguments. Everything returned is safe to
33
+ * pass to dnstwist as a separate argv entry: no value can start with "-".
34
+ */
35
+ export function validateFuzzArgs(args) {
36
+ if (!args || typeof args !== 'object')
37
+ invalid('arguments must be an object');
38
+ const a = args;
39
+ const domain = typeof a.domain === 'string' ? asciiHostname(a.domain) : null;
40
+ if (!domain)
41
+ invalid('domain must be a domain name such as example.com');
42
+ const rawNameservers = a.nameservers === undefined ? '1.1.1.1' : a.nameservers;
43
+ if (typeof rawNameservers !== 'string')
44
+ invalid('nameservers must be a string');
45
+ const servers = rawNameservers.split(',').map((ns) => {
46
+ const trimmed = ns.trim();
47
+ return isIP(trimmed) !== 0 ? trimmed : asciiHostname(trimmed);
48
+ });
49
+ if (servers.length > MAX_NAMESERVERS || servers.some((ns) => !ns)) {
50
+ invalid(`nameservers must be up to ${MAX_NAMESERVERS} comma-separated IP addresses or hostnames`);
51
+ }
52
+ const threads = a.threads === undefined ? 50 : a.threads;
53
+ if (typeof threads !== 'number' || !Number.isInteger(threads) || threads < 1 || threads > 100) {
54
+ invalid('threads must be an integer from 1 to 100');
55
+ }
56
+ const format = a.format === undefined ? 'json' : a.format;
57
+ if (!FORMATS.includes(format)) {
58
+ invalid(`format must be one of ${FORMATS.join(', ')}`);
59
+ }
60
+ return {
61
+ domain: domain,
62
+ nameservers: servers.join(','),
63
+ threads: threads,
64
+ format: format,
65
+ registered_only: optionalBoolean(a, 'registered_only', true),
66
+ mxcheck: optionalBoolean(a, 'mxcheck', true),
67
+ ssdeep: optionalBoolean(a, 'ssdeep', false),
68
+ banners: optionalBoolean(a, 'banners', true),
69
+ };
70
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@burtthecoder/mcp-dnstwist",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
4
4
  "description": "MCP server for dnstwist - DNS fuzzing to detect typosquatting, phishing and corporate espionage",
5
5
  "main": "build/index.js",
6
6
  "type": "module",
@@ -14,18 +14,19 @@
14
14
  ],
15
15
  "scripts": {
16
16
  "build": "tsc && chmod +x build/index.js",
17
- "prepublishOnly": "npm run build"
17
+ "prepublishOnly": "npm run build",
18
+ "test": "npm run build && node --test tests/*.test.mjs"
18
19
  },
19
20
  "dependencies": {
20
- "@modelcontextprotocol/sdk": "^0.4.0"
21
+ "@modelcontextprotocol/sdk": "^1.32.1"
21
22
  },
22
23
  "devDependencies": {
23
- "@types/node": "^20.0.0",
24
- "typescript": "^5.0.0"
24
+ "@types/node": "^20.19.43",
25
+ "typescript": "^5.9.3"
25
26
  },
26
27
  "repository": {
27
28
  "type": "git",
28
- "url": "git+https://github.com/burtthecoder/mcp-dnstwist.git"
29
+ "url": "git+https://github.com/w0h1v/mcp-dnstwist.git"
29
30
  },
30
31
  "keywords": [
31
32
  "mcp",
@@ -41,6 +42,6 @@
41
42
  "author": "burtmacklin",
42
43
  "license": "MIT",
43
44
  "engines": {
44
- "node": ">=18"
45
+ "node": ">=20"
45
46
  }
46
47
  }