From 5a9d989ad3300dc2166293329700947e69517ac4 Mon Sep 17 00:00:00 2001 From: chenyc-hust Date: Mon, 19 Jan 2026 15:52:52 +0800 Subject: [PATCH] fix: prevent command injection by replacing exec with execFile - Replace child_process.exec with execFile - Pass tshark arguments as an array - Add strict validation for interface input - Preserve existing functionality --- index.js | 173 +++++++++++++++++++++++++++++++++++++------------------ 1 file changed, 118 insertions(+), 55 deletions(-) diff --git a/index.js b/index.js index c8ece68..7310817 100644 --- a/index.js +++ b/index.js @@ -1,10 +1,10 @@ // index.js - WireMCP Server const axios = require('axios'); -const { exec } = require('child_process'); +const { execFile } = require('child_process'); const { promisify } = require('util'); const which = require('which'); const fs = require('fs').promises; -const execAsync = promisify(exec); +const execFileAsync = promisify(execFile); const { McpServer } = require('@modelcontextprotocol/sdk/server/mcp.js'); const { StdioServerTransport } = require('@modelcontextprotocol/sdk/server/stdio.js'); const { z } = require('zod'); @@ -13,6 +13,15 @@ const { z } = require('zod'); const originalConsoleLog = console.log; console.log = (...args) => console.error(...args); +// Validate the network interface name +function validateInterface(interface) { + const validPattern = /^[A-Za-z0-9_.:\- ]+$/; + if (!validPattern.test(interface)) { + throw new Error(`Invalid network interface: ${interface}`); + } + return interface; +} + // Dynamically locate tshark async function findTshark() { try { @@ -27,7 +36,7 @@ async function findTshark() { for (const path of fallbacks) { try { - await execAsync(`${path} -v`); + await execFileAsync(path, ['-v']); console.error(`Found tshark at fallback: ${path}`); return path; } catch (e) { @@ -49,7 +58,7 @@ server.tool( 'capture_packets', 'Capture live traffic and provide raw packet data as JSON for LLM analysis', { - interface: z.string().optional().default('en0').describe('Network interface to capture from (e.g., eth0, en0)'), + interface: z.string().optional().default('en0').regex(/^[A-Za-z0-9_.:\- ]+$/, 'Invalid interface name').describe('Network interface to capture from (e.g., eth0, en0)'), duration: z.number().optional().default(5).describe('Capture duration in seconds'), }, async (args) => { @@ -59,15 +68,26 @@ server.tool( const tempPcap = 'temp_capture.pcap'; console.error(`Capturing packets on ${interface} for ${duration}s`); - await execAsync( - `${tsharkPath} -i ${interface} -w ${tempPcap} -a duration:${duration}`, - { env: { ...process.env, PATH: `${process.env.PATH}:/usr/bin:/usr/local/bin:/opt/homebrew/bin` } } + await execFileAsync( + tsharkPath, ['-i', validateInterface(interface), '-w', tempPcap, '-a', `duration:${duration}`], + {env: { ...process.env, PATH: `${process.env.PATH}:/usr/bin:/usr/local/bin:/opt/homebrew/bin` } } ); - const { stdout, stderr } = await execAsync( - `${tsharkPath} -r "${tempPcap}" -T json -e frame.number -e ip.src -e ip.dst -e tcp.srcport -e tcp.dstport -e tcp.flags -e frame.time -e http.request.method -e http.response.code`, - { env: { ...process.env, PATH: `${process.env.PATH}:/usr/bin:/usr/local/bin:/opt/homebrew/bin` } } - ); + const { stdout, stderr } = await execFileAsync(tsharkPath, [ + '-r', tempPcap, + '-T', 'json', + '-e', 'frame.number', + '-e', 'ip.src', + '-e', 'ip.dst', + '-e', 'tcp.srcport', + '-e', 'tcp.dstport', + '-e', 'tcp.flags', + '-e', 'frame.time', + '-e', 'http.request.method', + '-e', 'http.response.code' + ], { + env: { ...process.env, PATH: `${process.env.PATH}:/usr/bin:/usr/local/bin:/opt/homebrew/bin` } + }); if (stderr) console.error(`tshark stderr: ${stderr}`); let packets = JSON.parse(stdout); @@ -101,7 +121,7 @@ server.tool( 'get_summary_stats', 'Capture live traffic and provide protocol hierarchy statistics for LLM analysis', { - interface: z.string().optional().default('en0').describe('Network interface to capture from (e.g., eth0, en0)'), + interface: z.string().optional().default('en0').regex(/^[A-Za-z0-9_.:\- ]+$/, 'Invalid interface name').describe('Network interface to capture from (e.g., eth0, en0)'), duration: z.number().optional().default(5).describe('Capture duration in seconds'), }, async (args) => { @@ -111,15 +131,20 @@ server.tool( const tempPcap = 'temp_capture.pcap'; console.error(`Capturing summary stats on ${interface} for ${duration}s`); - await execAsync( - `${tsharkPath} -i ${interface} -w ${tempPcap} -a duration:${duration}`, - { env: { ...process.env, PATH: `${process.env.PATH}:/usr/bin:/usr/local/bin:/opt/homebrew/bin` } } - ); - - const { stdout, stderr } = await execAsync( - `${tsharkPath} -r "${tempPcap}" -qz io,phs`, - { env: { ...process.env, PATH: `${process.env.PATH}:/usr/bin:/usr/local/bin:/opt/homebrew/bin` } } - ); + await execFileAsync(tsharkPath, [ + '-i', validateInterface(interface), + '-w', tempPcap, + '-a', `duration:${duration}` + ], { + env: { ...process.env, PATH: `${process.env.PATH}:/usr/bin:/usr/local/bin:/opt/homebrew/bin` } + }); + + const { stdout, stderr } = await execFileAsync(tsharkPath, [ + '-r', tempPcap, + '-qz', 'io,phs' + ], { + env: { ...process.env, PATH: `${process.env.PATH}:/usr/bin:/usr/local/bin:/opt/homebrew/bin` } + }); if (stderr) console.error(`tshark stderr: ${stderr}`); await fs.unlink(tempPcap).catch(err => console.error(`Failed to delete ${tempPcap}: ${err.message}`)); @@ -142,7 +167,7 @@ server.tool( 'get_conversations', 'Capture live traffic and provide TCP/UDP conversation statistics for LLM analysis', { - interface: z.string().optional().default('en0').describe('Network interface to capture from (e.g., eth0, en0)'), + interface: z.string().optional().default('en0').regex(/^[A-Za-z0-9_.:\- ]+$/, 'Invalid interface name').describe('Network interface to capture from (e.g., eth0, en0)'), duration: z.number().optional().default(5).describe('Capture duration in seconds'), }, async (args) => { @@ -152,15 +177,20 @@ server.tool( const tempPcap = 'temp_capture.pcap'; console.error(`Capturing conversations on ${interface} for ${duration}s`); - await execAsync( - `${tsharkPath} -i ${interface} -w ${tempPcap} -a duration:${duration}`, - { env: { ...process.env, PATH: `${process.env.PATH}:/usr/bin:/usr/local/bin:/opt/homebrew/bin` } } - ); - - const { stdout, stderr } = await execAsync( - `${tsharkPath} -r "${tempPcap}" -qz conv,tcp`, - { env: { ...process.env, PATH: `${process.env.PATH}:/usr/bin:/usr/local/bin:/opt/homebrew/bin` } } - ); + await execFileAsync(tsharkPath, [ + '-i', validateInterface(interface), + '-w', tempPcap, + '-a', `duration:${duration}` + ], { + env: { ...process.env, PATH: `${process.env.PATH}:/usr/bin:/usr/local/bin:/opt/homebrew/bin` } + }); + + const { stdout, stderr } = await execFileAsync(tsharkPath, [ + '-r', tempPcap, + '-qz', 'conv,tcp' + ], { + env: { ...process.env, PATH: `${process.env.PATH}:/usr/bin:/usr/local/bin:/opt/homebrew/bin` } + }); if (stderr) console.error(`tshark stderr: ${stderr}`); await fs.unlink(tempPcap).catch(err => console.error(`Failed to delete ${tempPcap}: ${err.message}`)); @@ -183,7 +213,7 @@ server.tool( 'check_threats', 'Capture live traffic and check IPs against URLhaus blacklist', { - interface: z.string().optional().default('en0').describe('Network interface to capture from (e.g., eth0, en0)'), + interface: z.string().optional().default('en0').regex(/^[A-Za-z0-9_.:\- ]+$/, 'Invalid interface name').describe('Network interface to capture from (e.g., eth0, en0)'), duration: z.number().optional().default(5).describe('Capture duration in seconds'), }, async (args) => { @@ -193,15 +223,22 @@ server.tool( const tempPcap = 'temp_capture.pcap'; console.error(`Capturing traffic on ${interface} for ${duration}s to check threats`); - await execAsync( - `${tsharkPath} -i ${interface} -w ${tempPcap} -a duration:${duration}`, - { env: { ...process.env, PATH: `${process.env.PATH}:/usr/bin:/usr/local/bin:/opt/homebrew/bin` } } - ); - - const { stdout } = await execAsync( - `${tsharkPath} -r "${tempPcap}" -T fields -e ip.src -e ip.dst`, - { env: { ...process.env, PATH: `${process.env.PATH}:/usr/bin:/usr/local/bin:/opt/homebrew/bin` } } - ); + await execFileAsync(tsharkPath, [ + '-i', validateInterface(interface), + '-w', tempPcap, + '-a', `duration:${duration}` + ], { + env: { ...process.env, PATH: `${process.env.PATH}:/usr/bin:/usr/local/bin:/opt/homebrew/bin` } + }); + + const { stdout } = await execFileAsync(tsharkPath, [ + '-r', tempPcap, + '-T', 'fields', + '-e', 'ip.src', + '-e', 'ip.dst' + ], { + env: { ...process.env, PATH: `${process.env.PATH}:/usr/bin:/usr/local/bin:/opt/homebrew/bin` } + }); const ips = [...new Set(stdout.split('\n').flatMap(line => line.split('\t')).filter(ip => ip && ip !== 'unknown'))]; console.error(`Captured ${ips.length} unique IPs: ${ips.join(', ')}`); @@ -314,10 +351,22 @@ server.tool( await fs.access(pcapPath); // Extract broad packet data - const { stdout, stderr } = await execAsync( - `${tsharkPath} -r "${pcapPath}" -T json -e frame.number -e ip.src -e ip.dst -e tcp.srcport -e tcp.dstport -e udp.srcport -e udp.dstport -e http.host -e http.request.uri -e frame.protocols`, - { env: { ...process.env, PATH: `${process.env.PATH}:/usr/bin:/usr/local/bin:/opt/homebrew/bin` } } - ); + const { stdout, stderr } = await execFileAsync(tsharkPath, [ + '-r', pcapPath, + '-T', 'json', + '-e', 'frame.number', + '-e', 'ip.src', + '-e', 'ip.dst', + '-e', 'tcp.srcport', + '-e', 'tcp.dstport', + '-e', 'udp.srcport', + '-e', 'udp.dstport', + '-e', 'http.host', + '-e', 'http.request.uri', + '-e', 'frame.protocols' + ], { + env: { ...process.env, PATH: `${process.env.PATH}:/usr/bin:/usr/local/bin:/opt/homebrew/bin` } + }); if (stderr) console.error(`tshark stderr: ${stderr}`); const packets = JSON.parse(stdout); @@ -377,17 +426,31 @@ server.tool( await fs.access(pcapPath); // Extract plaintext credentials - const { stdout: plaintextOut } = await execAsync( - `${tsharkPath} -r "${pcapPath}" -T fields -e http.authbasic -e ftp.request.command -e ftp.request.arg -e telnet.data -e frame.number`, - { env: { ...process.env, PATH: `${process.env.PATH}:/usr/bin:/usr/local/bin:/opt/homebrew/bin` } } - ); + const { stdout: plaintextOut } = await execFileAsync(tsharkPath, [ + '-r', pcapPath, + '-T', 'fields', + '-e', 'http.authbasic', + '-e', 'ftp.request.command', + '-e', 'ftp.request.arg', + '-e', 'telnet.data', + '-e', 'frame.number' + ], { + env: { ...process.env, PATH: `${process.env.PATH}:/usr/bin:/usr/local/bin:/opt/homebrew/bin` } + }); // Extract Kerberos credentials - const { stdout: kerberosOut } = await execAsync( - `${tsharkPath} -r "${pcapPath}" -T fields -e kerberos.CNameString -e kerberos.realm -e kerberos.cipher -e kerberos.type -e kerberos.msg_type -e frame.number`, - { env: { ...process.env, PATH: `${process.env.PATH}:/usr/bin:/usr/local/bin:/opt/homebrew/bin` } } - ); - + const { stdout: kerberosOut } = await execFileAsync(tsharkPath, [ + '-r', pcapPath, + '-T', 'fields', + '-e', 'kerberos.CNameString', + '-e', 'kerberos.realm', + '-e', 'kerberos.cipher', + '-e', 'kerberos.type', + '-e', 'kerberos.msg_type', + '-e', 'frame.number' + ], { + env: { ...process.env, PATH: `${process.env.PATH}:/usr/bin:/usr/local/bin:/opt/homebrew/bin` } + }); const lines = plaintextOut.split('\n').filter(line => line.trim()); const packets = lines.map(line => { const [authBasic, ftpCmd, ftpArg, telnetData, frameNumber] = line.split('\t'); @@ -660,4 +723,4 @@ server.connect(new StdioServerTransport()) .catch(err => { console.error('Failed to start WireMCP:', err); process.exit(1); - }); \ No newline at end of file + });