Root cause
Injection & unsafe execution introduced
Add JAR decompilation support with mandatory class name
How AI contributed: Direct introductionPublished Apr 8, 2026 in
idachev/mcp-javadc
OS command injection (CWE-78) via the jarFilePath argument of the decompile-from-jar tool: DecompilerService.decompileFromJar() in index.js shells out with template-interpolated user input - execPromise with template jar tf "${jarFilePath}" | grep ".class$" and cd "${tempDir}" && jar xf "${jarFilePath}" - jarFilePath is only wrapped in double quotes, so an attacker-supplied value containing a double-quote character (e.g. "; <command>; ") breaks out and executes arbitrary OS commands on the server. Remote/unauthenticated via the MCP tool interface (advisory: 'HTTP Interface' component, 'argument jarFilePath', exploit public), affects up to 1.2.4; the vulnerable lines are still present at HEAD.
Root cause
Add JAR decompilation support with mandatory class name
Fix status
No minimum fix commit has been established for this finding.
Fix status unresolved
Case facts
@@ -6,14 +6,28 @@ import { decompile } from '@run-slicer/cfr'; import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';+import * as os from 'os';+import { promisify } from 'util';+import { exec } from 'child_process'; const SERVER_NAME = 'javadc';-const PACKAGE_VERSION = '1.1.5';+const PACKAGE_VERSION = '1.2.0'; class DecompilerService { async decompileFromPath(classFilePath) { try { await fs.access(classFilePath);++ // Check if the file is a JAR+ const isJar = classFilePath.toLowerCase().endsWith('.jar');++ if (isJar) {+ throw new Error(+ 'JAR files must be decompiled using decompileFromJar with className parameter'+ );+ }++ // Regular class file decompilation const classData = await fs.readFile(classFilePath); const internalName = this.getInternalNameFromPath(classFilePath); @@ -39,6 +53,85 @@ class DecompilerService { } } + async decompileFromJar(jarFilePath, className) {+ if (!className) {+ throw new Error('Class name must be specified for JAR decompilation');+ }++ // Create a temporary directory for extraction+ const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'javadc-'));+ const execPromise = promisify(exec);++ try {+ // Extract the list of class files in the JAR+ const { stdout } = await execPromise(`jar tf "${jarFilePath}" | grep ".class$"`);+ const classFiles = stdout.trim().split('\n');++ if (classFiles.length === 0) {+ throw new Error('No class files found in the JAR file');+ }++ // Extract all class files - switch to current directory then extract+ await execPromise(`cd "${tempDir}" && jar xf "${jarFilePath}"`);++ // Determine which class to decompile+ let targetClassFile = null;+ let internalName = null;++ // Convert package.class notation to internal format (with /)+ internalName = className.replace(/\./g, '/');+ targetClassFile = internalName + '.class';++ // Check if the class exists in the JAR+ const classExists = classFiles.some(cf => cf.trim() === targetClassFile);+ if (!classExists) {+ throw new Error(`Class '${className}' not found in JAR file`);+ }++ const extractedClassPath = path.join(tempDir, targetClassFile);++ // Read the class data+ const classData = await fs.readFile(extractedClassPath);++ // Decompile the class+ const decompiled = await decompile(internalName, {+ source: async name => {+ if (name === internalName) {+ return classData;+ }++ // Handle other class references from the JAR+ const otherClassFile = name + '.class';+ const otherClassPath = path.join(tempDir, otherClassFile);++ try {+ return await fs.readFile(otherClassPath);+ } catch {+ if (name.startsWith('java/lang/')) {+ return Buffer.from([]);@@ -7,6 +7,7 @@ import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js' const FIXTURES_DIR = path.join(process.cwd(), 'test', 'fixtures'); const TEST_CLASS_PATH = path.join(FIXTURES_DIR, 'SampleClass.class');+const TEST_JAR_PATH = path.join(FIXTURES_DIR, 'SampleClass.jar'); async function runTests() { console.log('Starting MCP Java Decompiler tests with MCP client...');@@ -32,20 +33,22 @@ async function runTests() { assert(toolsResponse && toolsResponse.tools, 'Expected tools array in response'); assert(Array.isArray(toolsResponse.tools), 'Expected tools to be an array');- assert(toolsResponse.tools.length === 2, 'Expected 2 tools to be listed');+ assert(toolsResponse.tools.length === 3, 'Expected 3 tools to be listed'); const toolNames = toolsResponse.tools.map(tool => tool.name); assert(toolNames.includes('decompile-from-path'), 'Expected decompile-from-path tool'); assert(toolNames.includes('decompile-from-package'), 'Expected decompile-from-package tool');+ assert(toolNames.includes('decompile-from-jar'), 'Expected decompile-from-jar tool'); console.log('✓ Successfully listed tools:', toolNames); console.log('\nTest 2: Testing decompile-from-path tool...'); try { await fs.access(TEST_CLASS_PATH);+ await fs.access(TEST_JAR_PATH); } catch (e) {- console.log('Test class file not found, running create-test-fixtures...');+ console.log('Test fixtures not found, running create-test-fixtures...'); const { createFixtures } = await import('./create-test-fixtures.js'); await createFixtures(); }@@ -94,7 +97,52 @@ async function runTests() { console.log('✓ Expected error when decompiling from package:', error.message); } - console.log('\nTest 4: Testing error handling for invalid path...');+ console.log('\nTest 4: Testing decompile-from-jar tool...');+ + try {+ const decompileJarResponse = await client.callTool({+ name: 'decompile-from-jar',+ arguments: {+ jarFilePath: TEST_JAR_PATH,+ className: 'SampleClass'+ },+ });+ + console.log('Decompile jar response received:', + decompileJarResponse ? 'Success' : 'Error');+ + assert(decompileJarResponse && decompileJarResponse.content, + 'Expected content in response');+ + const jarText = decompileJarResponse.content[0]?.text || '';+ assert(jarText.includes('class SampleClass'), 'Expected decompiled class in result');+ assert(jarText.includes('void printMessage()'), 'Expected method in decompiled class');+ console.log('✓ Successfully decompiled from JAR with explicit class name');+ + // Test for missing className (should error)+ const decompileJarMissingClassResponse = await client.callTool({+ name: 'decompile-from-jar',+ arguments: {+ jarFilePath: TEST_JAR_PATH+ },+ });+ + console.log('Decompile jar (missing className) response received:', + decompileJarMissingClassResponse ? 'Success' : 'Error');+ + assert(decompileJarMissingClassResponse && decompileJarMissingClassResponse.content, + 'Expected content in response');+ + const missingClassText = decompileJarMissingClassResponse.content[0]?.text || '';+ assert(missingClassText.includes('Error:'), 'Expected error message for missing className');+ console.log('✓ Successfully returned error for missing className');+ + } catch (error) {+ console.error('Failed to decompile from JAR:', error);+ throw error;+ }+ + console.log('\nTest 5: Testing error handling for invalid path...'); const invalidPathResponse = await client.callTool({ name: 'decompile-from-path',@@ -4,6 +4,7 @@ import assert from 'assert'; const FIXTURES_DIR = path.join(process.cwd(), 'test', 'fixtures'); const TEST_CLASS_PATH = path.join(FIXTURES_DIR, 'SampleClass.class');+const TEST_JAR_PATH = path.join(FIXTURES_DIR, 'SampleClass.jar'); import { decompilerService } from '../index.js'; @@ -13,8 +14,9 @@ async function runTests() { try { try { await fs.access(TEST_CLASS_PATH);+ await fs.access(TEST_JAR_PATH); } catch (e) {- console.log('Test class file not found, running create-test-fixtures...');+ console.log('Test fixtures not found, running create-test-fixtures...'); const { createFixtures } = await import('./create-test-fixtures.js'); await createFixtures(); }@@ -38,7 +40,23 @@ async function runTests() { console.log('✓ Expected error when decompiling from package:', error.message); } - console.log('\nTest 3: Testing error handling...');+ console.log('\nTest 3: Testing decompileFromJar...');+ + try {+ // Now we need to explicitly pass the class name for JAR decompilation+ const decompiled = await decompilerService.decompileFromJar(TEST_JAR_PATH, 'SampleClass');+ + assert(decompiled && typeof decompiled === 'string', 'Expected decompiled result to be a string');+ assert(decompiled.includes('class SampleClass'), 'Expected decompiled class in result');+ assert(decompiled.includes('void printMessage()'), 'Expected method in decompiled class');+ + console.log('✓ Successfully decompiled class from JAR file');+ } catch (error) {+ console.error('Failed to decompile from JAR:', error);+ throw error;+ }+ + console.log('\nTest 4: Testing error handling...'); try { await decompilerService.decompileFromPath('/path/to/nonexistent/file.class');