Skip to content

exec is unusable through the substituted child_process module: no argument shape satisfies both the wrapper and Node's signature #2278

Description

@kriszyp

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:

return function (command, args, options, callback) {
    ...
    const processName = options?.name;
    if (!processName) throw new Error(`Calling ${spawnFunction.name} in Harper must have a process "name" ...`);
    ...
    const childProcess = 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:

  1. 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.
  2. Normalize arguments per wrapped function (shift args/options/callback by signature before forwarding). This would activate harper#1924, which is currently unreachable precisely because exec 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    Fields

    Priority

    P2

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions