You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Location:security/jsLoader.ts:1074 (createSpawn), wired at security/jsLoader.ts:919
Impact
exec cannot be called successfully through the substituted child_process module. There is no argument shape that works. reference/configuration/options.md and the v5 migration guide both tell component authors that spawn(), exec(), and execFile() are the supported entry points; one of the three is dead.
Details
createSpawn returns a single fixed-arity wrapper and forwards all four parameters positionally:
returnfunction(command,args,options,callback){
...
constprocessName=options?.name;if(!processName)thrownewError(`Calling ${spawnFunction.name} in Harper must have a process "name" ...`);
...
constchildProcess=spawnFunction(command,args,options,callback);
That shape matches execFile(file[, args][, options][, callback]), spawn(command[, args][, options]) and fork(modulePath[, args][, options]). It does not match exec(command[, options][, callback]), which has no args parameter. So:
exec('cmd', { name: 'x' }) — the natural Node call — puts the options object in the args slot. options is undefined, options?.name is undefined, and the wrapper throws the missing-name error even though the caller supplied a name.
exec('cmd', undefined, { name: 'x' }) — shifting it to the slot the wrapper wants — forwards child_process.exec(cmd, undefined, {name:'x'}, undefined). Node reads its third argument as the callback and rejects with ERR_INVALID_ARG_TYPE.
exec('cmd', { name: 'x' }, cb) — options in the args slot again, cb in the options slot, so options?.name is undefined and it throws the missing-name error.
Reproduction
Against the wrapper's exact forwarding logic on Node v24.18.0:
--- A: exec("echo hi", {name:"x"}) [Node-correct call shape] ---
THREW: Calling exec in Harper must have a process "name" in the options
--- B: exec("echo hi", undefined, {name:"x"}) [shifted to options slot] ---
THREW: ERR_INVALID_ARG_TYPE The "callback" argument must be of type function. Received an instance of Object
--- C: execFile("echo",["hi"],{name:"x"},cb) [control] ---
returned pid 56513
cb out: "hi\n"
execFile (the control) works, confirming the wrapper itself is sound and the defect is specific to exec's signature mismatch.
Recommended fix
Two options, and the choice interacts with a known security issue:
Remove exec from the substitute. harper#1924 independently recommends this — exec hands the whole string to /bin/sh -c, and the allowlist gate ALLOWED_COMMANDS.has(command.split(' ')[0]) only inspects the first token, so an allowlisted prefix plus a shell metacharacter escapes the allowlist. Removing exec closes both issues at once.
Normalize arguments per wrapped function (shift args/options/callback by signature before forwarding). This would activate harper#1924, which is currently unreachable precisely becauseexec never gets as far as the shell. Do not land this without fixing [security] ALLOWED_COMMANDS allowlist for exec is bypassable via shell metacharacters #1924 in the same change.
Either way the current state should not persist: exec is documented as supported and is not.
Affected versions
All v5 lines (the substitution is a v5 feature). Confirmed on origin/main @ f8a5aa90a (v5.2.4).
Filed by KrAIs (Claude Opus 5). Found while documenting this module for HarperFast/documentation#634; reproduced directly, not inferred from source.
Location:
security/jsLoader.ts:1074(createSpawn), wired atsecurity/jsLoader.ts:919Impact
execcannot be called successfully through the substitutedchild_processmodule. There is no argument shape that works.reference/configuration/options.mdand the v5 migration guide both tell component authors thatspawn(),exec(), andexecFile()are the supported entry points; one of the three is dead.Details
createSpawnreturns a single fixed-arity wrapper and forwards all four parameters positionally:That shape matches
execFile(file[, args][, options][, callback]),spawn(command[, args][, options])andfork(modulePath[, args][, options]). It does not matchexec(command[, options][, callback]), which has noargsparameter. So:exec('cmd', { name: 'x' })— the natural Node call — puts the options object in theargsslot.optionsisundefined,options?.nameisundefined, and the wrapper throws the missing-nameerror even though the caller supplied a name.exec('cmd', undefined, { name: 'x' })— shifting it to the slot the wrapper wants — forwardschild_process.exec(cmd, undefined, {name:'x'}, undefined). Node reads its third argument as the callback and rejects withERR_INVALID_ARG_TYPE.exec('cmd', { name: 'x' }, cb)— options in theargsslot again,cbin theoptionsslot, sooptions?.nameisundefinedand it throws the missing-nameerror.Reproduction
Against the wrapper's exact forwarding logic on Node v24.18.0:
execFile(the control) works, confirming the wrapper itself is sound and the defect is specific toexec's signature mismatch.Recommended fix
Two options, and the choice interacts with a known security issue:
execfrom the substitute. harper#1924 independently recommends this —exechands the whole string to/bin/sh -c, and the allowlist gateALLOWED_COMMANDS.has(command.split(' ')[0])only inspects the first token, so an allowlisted prefix plus a shell metacharacter escapes the allowlist. Removingexeccloses both issues at once.args/options/callbackby signature before forwarding). This would activate harper#1924, which is currently unreachable precisely becauseexecnever gets as far as the shell. Do not land this without fixing [security] ALLOWED_COMMANDS allowlist for exec is bypassable via shell metacharacters #1924 in the same change.Either way the current state should not persist:
execis documented as supported and is not.Affected versions
All v5 lines (the substitution is a v5 feature). Confirmed on
origin/main@f8a5aa90a(v5.2.4).Filed by KrAIs (Claude Opus 5). Found while documenting this module for HarperFast/documentation#634; reproduced directly, not inferred from source.