CVE-2024-48964
Description
The package Snyk CLI before 1.1294.0 is vulnerable to Code Injection when scanning an untrusted Gradle project. The vulnerability can be triggered if Snyk test is run inside the untrusted project due to the improper handling of the current working directory name. Snyk recommends only scanning trusted projects.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Snyk CLI before 1.1294.0 vulnerable to code injection via malicious Gradle project directory name, enabling arbitrary command execution.
Vulnerability
CVE-2024-48964 is a code injection vulnerability in the Snyk CLI Gradle plugin affecting versions prior to 1.1294.0. The root cause is improper sanitization of the current working directory name when constructing and executing Gradle commands. An attacker can create a project with a directory name containing shell metacharacters or command sequences, which are then incorporated into the command line without proper escaping [1][2].
Exploitation
The vulnerability can be triggered if Snyk test is executed inside an untrusted Gradle project. No authentication or special privileges beyond file system access are required. By crafting a directory name with malicious payloads (e.g., using backticks or semicolons), an attacker can inject arbitrary commands that are executed by the Snyk CLI process. The fix in commit [2] introduces safer argument handling and wrapper process generation to prevent injection.
Impact
Successful exploitation allows arbitrary command execution with the privileges of the user running Snyk CLI. This can lead to data exfiltration, installation of malware, or lateral movement within the environment. The vulnerability did not affect trusting projects, as the malicious directory name is the trigger.
Mitigation
Snyk has patched this vulnerability in version 1.1294.0 of the CLI. Users are strongly advised to update immediately and to only scan projects from trusted sources. There is no known workaround other than avoiding use of the CLI on untrusted projects.
AI Insight generated on May 20, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
snyk-gradle-pluginnpm | < 4.5.0 | 4.5.0 |
Affected products
5- osv-coords3 versions
< 1.1294.0-r0+ 2 more
- (no CPE)range: < 1.1294.0-r0
- (no CPE)range: < 1.1294.0-r0
- (no CPE)range: < 4.5.0
- Snyk/Snyk Gradle Pluginv5Range: 0
Patches
12f5ee7579f00feat: handle gradle wrapper files for windows (#292)
6 files changed · +82 −32
lib/index.ts+35 −9 modified@@ -396,11 +396,13 @@ function getVersionBuildInfo( export async function getGradleVersion( root: string, command: string, + args?: string[], ): Promise<string> { debugLog('`gradle -v` command run: ' + command); let gradleVersionOutput = '[COULD NOT RUN gradle -v]'; + const completeArgs = args ? args.concat(['-v']) : ['-v']; try { - gradleVersionOutput = await subProcess.execute(command, ['-v'], { + gradleVersionOutput = await subProcess.execute(command, completeArgs, { cwd: root, }); } catch (_) { @@ -409,6 +411,22 @@ export async function getGradleVersion( return gradleVersionOutput; } +export function generateWrapperProcessArgs( + commandPath: string, + args: string[], +): { command: string; args: string[] } { + let parseArgs: string[] = []; + let command = commandPath; + const isWinLocal = /^win/.test(os.platform()); + if (isWinLocal && command !== 'gradle') { + command = 'cmd.exe'; + parseArgs.push('/c'); + parseArgs.push(commandPath); + } + parseArgs = parseArgs.concat(args); + return { command, args: parseArgs }; +} + async function getAllDepsWithPlugin( root: string, targetFile: string, @@ -427,12 +445,15 @@ async function getAllDepsWithPlugin( gradleVersion, ); - const fullCommandText = 'gradle command: ' + command + ' ' + args.join(' '); - debugLog('Executing ' + fullCommandText); + const { command: wrapperedCommand, args: wrapperArgs } = + generateWrapperProcessArgs(command, args); + const fullCommandText = + 'gradle command: ' + wrapperedCommand + ' ' + wrapperArgs.join(' '); + debugLog('Executing ' + fullCommandText); const stdoutText = await subProcess.execute( - command, - args, + wrapperedCommand, + wrapperArgs, { cwd: root }, printIfEcho, ); @@ -467,7 +488,13 @@ async function getAllDeps( snykHttpClient: SnykHttpClient, ): Promise<JsonDepsScriptResult> { const command = getCommand(root, targetFile); - const gradleVersion = await getGradleVersion(root, command); + const { command: wrapperedCommand, args: wrapperArgs } = + generateWrapperProcessArgs(command, []); + const gradleVersion = await getGradleVersion( + root, + wrapperedCommand, + wrapperArgs, + ); if (gradleVersion.match(/Gradle 1/)) { throw new Error('Gradle 1.x is not supported'); } @@ -636,7 +663,6 @@ function toCamelCase(input: string) { function getCommand(root: string, targetFile: string) { const isWinLocal = /^win/.test(os.platform()); // local check, can be stubbed in tests - const quotLocal = isWinLocal ? '"' : "'"; const wrapperScript = isWinLocal ? 'gradlew.bat' : './gradlew'; // try to find a sibling wrapper script first let pathToWrapper = path.resolve( @@ -645,12 +671,12 @@ function getCommand(root: string, targetFile: string) { wrapperScript, ); if (fs.existsSync(pathToWrapper)) { - return quotLocal + pathToWrapper + quotLocal; + return pathToWrapper; } // now try to find a wrapper in the root pathToWrapper = path.resolve(root, wrapperScript); if (fs.existsSync(pathToWrapper)) { - return quotLocal + pathToWrapper + quotLocal; + return pathToWrapper; } return 'gradle'; }
lib/sub-process.ts+3 −3 modified@@ -1,6 +1,6 @@ import * as childProcess from 'child_process'; import debugModule = require('debug'); -import { quoteAll } from 'shescape'; +import { escapeAll } from 'shescape'; const debugLogging = debugModule('snyk-gradle-plugin'); @@ -12,7 +12,7 @@ export function execute( perLineCallback?: (s: string) => Promise<void>, ): Promise<string> { const spawnOptions: childProcess.SpawnOptions = { - shell: true, + shell: false, env: { ...process.env }, }; if (options?.cwd) { @@ -22,7 +22,7 @@ export function execute( spawnOptions.env = { ...process.env, ...options.env }; } - args = quoteAll(args, spawnOptions); + args = escapeAll(args, spawnOptions); // Before spawning an external process, we look if we need to restore the system proxy configuration, // which overides the cli internal proxy configuration.
test/fixtures-with-wrappers/with-lock-file/dep-graph.json+1 −1 modified@@ -144,4 +144,4 @@ } ] } -} +} \ No newline at end of file
test/system/darwin.test.ts+2 −2 modified@@ -34,7 +34,7 @@ test('darwin without wrapper invokes gradle directly', async () => { test('darwin with wrapper invokes wrapper script', async () => { await expect(inspect(rootWithWrapper, 'build.gradle')).rejects.toThrow(); expect(subProcessExecSpy.mock.calls[0][0]).toBe( - "'" + path.join(rootWithWrapper, 'gradlew') + "'", + path.join(rootWithWrapper, 'gradlew'), ); }); @@ -43,6 +43,6 @@ test('darwin with wrapper in root invokes wrapper script', async () => { inspect(subWithWrapper, path.join('app', 'build.gradle')), ).rejects.toThrow(); expect(subProcessExecSpy.mock.calls[0][0]).toBe( - "'" + path.join(subWithWrapper, 'gradlew') + "'", + path.join(subWithWrapper, 'gradlew'), ); });
test/system/plugin.test.ts+16 −1 modified@@ -2,7 +2,8 @@ import * as fs from 'fs'; import * as path from 'path'; import * as depGraphLib from '@snyk/dep-graph'; import { fixtureDir } from '../common'; -import { inspect } from '../../lib'; +import { generateWrapperProcessArgs, inspect } from '../../lib'; +import * as os from 'os'; const rootNoWrapper = fixtureDir('no wrapper'); const withInitScript = fixtureDir('with-init-script'); @@ -212,3 +213,17 @@ test('repeated transitive lines not pruned if verbose graph', async () => { }); expect(result.dependencyGraph?.equals(expected)).toBe(true); }); + +test('generateWrapperProcessArgs should return gradle is wrapper is not used', () => { + const result = generateWrapperProcessArgs('gradle', ['-v']); + expect(result).toEqual({ command: 'gradle', args: ['-v'] }); +}); +test('generateWrapperProcessArgs should return wrapped command is os is windows ', () => { + const platformMock = jest.spyOn(os, 'platform'); + platformMock.mockReturnValue('win32'); + const result = generateWrapperProcessArgs('foo/bar/gradlew.bat', ['-v']); + expect(result).toEqual({ + command: 'cmd.exe', + args: ['/c', 'foo/bar/gradlew.bat', '-v'], + }); +});
test/system/windows.test.ts+25 −16 modified@@ -9,6 +9,7 @@ const rootWithWrapper = fixtureDir('with-wrapper'); const subWithWrapper = fixtureDir('with-wrapper-in-root'); let subProcessExecSpy; let platformMock; +const isWinLocal = /^win/.test(os.platform()); beforeAll(() => { platformMock = jest.spyOn(os, 'platform'); @@ -24,23 +25,31 @@ afterAll(() => { afterEach(() => { jest.clearAllMocks(); }); +if (isWinLocal) { + test('windows with wrapper in root invokes wrapper bat', async () => { + await expect( + inspect(subWithWrapper, path.join('app', 'build.gradle')), + ).rejects.toThrow(); + expect(subProcessExecSpy.mock.calls[0]).toEqual([ + 'cmd.exe', + ['/c', `${subWithWrapper}\\gradlew.bat`, '-v'], + { + cwd: `${subWithWrapper}`, + }, + ]); + }); -test('windows with wrapper in root invokes wrapper bat', async () => { - await expect( - inspect(subWithWrapper, path.join('app', 'build.gradle')), - ).rejects.toThrow(); - expect(subProcessExecSpy.mock.calls[0][0]).toBe( - '"' + path.join(subWithWrapper, 'gradlew.bat') + '"', - ); -}); - -test('windows with wrapper invokes wrapper bat', async () => { - await expect(inspect(rootWithWrapper, 'build.gradle')).rejects.toThrow(); - expect(subProcessExecSpy.mock.calls[0][0]).toBe( - '"' + path.join(rootWithWrapper, 'gradlew.bat') + '"', - ); -}); - + test('windows with wrapper invokes wrapper bat', async () => { + await expect(inspect(rootWithWrapper, 'build.gradle')).rejects.toThrow(); + expect(subProcessExecSpy.mock.calls[0]).toEqual([ + 'cmd.exe', + ['/c', `${rootWithWrapper}\\gradlew.bat`, '-v'], + { + cwd: `${rootWithWrapper}`, + }, + ]); + }); +} test('windows without wrapper invokes gradle directly', async () => { await expect(inspect(rootNoWrapper, 'build.gradle')).rejects.toThrow(); expect(subProcessExecSpy.mock.calls[0][0]).toBe('gradle');
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
3News mentions
0No linked articles in our index yet.