From a2599ac4d3a80adbcda324193caeac74757d7fa9 Mon Sep 17 00:00:00 2001 From: JunghwanNA <70629228+shaun0927@users.noreply.github.com> Date: Fri, 17 Apr 2026 17:03:16 +0900 Subject: [PATCH] refactor: replace execSync shell-string curl with spawnSync argv-array (#410) Two places built curl command lines as shell strings and ran them through child_process.execSync, interpolating Bearer tokens into the command: src/gep/signals.js:260-274 curl with nodeSecret src/adapters/scripts/evolver-session-end.js:104-109 curl with apiKey Today the interpolated values are internally sourced, so exploit difficulty is high; the pattern is still an anti-pattern because any future contributor piping user-influenced data into nodeSecret or apiKey would turn a shell metacharacter into code execution. Both call sites move to child_process.spawnSync with an argv array and shell: false. Behaviour is preserved: - Still synchronous (addresses the comment in signals.js noting that the spin-wait loop cannot await async fetch). - Still uses curl, so no new dependency. - Same timeout, stdio, and windowsHide options. signals.js additionally reads spawnSync return status (res.status, res.error) to match the existing catch-all return [] behaviour; the prior code relied on execSync's throw-on-nonzero, so the explicit check is equivalent. evolver-session-end.js imports spawnSync alongside the existing execSync (the other two execSync call sites in this file use git shell pipelines with 2>/dev/null fallbacks, which are not credential call sites and stay as-is). Testing: node test/signals.test.js # fail 0 node -c src/adapters/scripts/evolver-session-end.js # syntax OK Closes #410 --- src/adapters/scripts/evolver-session-end.js | 18 ++++++++------ src/gep/signals.js | 27 ++++++++++++--------- 2 files changed, 27 insertions(+), 18 deletions(-) diff --git a/src/adapters/scripts/evolver-session-end.js b/src/adapters/scripts/evolver-session-end.js index 3e069d1f..4b3eaf03 100755 --- a/src/adapters/scripts/evolver-session-end.js +++ b/src/adapters/scripts/evolver-session-end.js @@ -6,7 +6,7 @@ const fs = require('fs'); const path = require('path'); -const { execSync } = require('child_process'); +const { execSync, spawnSync } = require('child_process'); function findEvolverRoot() { const candidates = [ @@ -101,12 +101,16 @@ function recordToHub(outcome) { summary: outcome.summary, sender_id: nodeId || undefined, }); - const curlCmd = `curl -s -m 8 -X POST` - + ` -H "Content-Type: application/json"` - + ` -H "Authorization: Bearer ${apiKey}"` - + ` -d '${payload.replace(/'/g, "'\\''")}'` - + ` "${hubUrl.replace(/\/+$/, '')}/a2a/evolution/record"`; - execSync(curlCmd, { timeout: 10000, stdio: ['pipe', 'pipe', 'pipe'] }); + // Argv-array form avoids shell interpretation of apiKey, payload, or the + // hub URL. Values cannot break out through quoting metacharacters. + const res = spawnSync('curl', [ + '-s', '-m', '8', '-X', 'POST', + '-H', 'Content-Type: application/json', + '-H', `Authorization: Bearer ${apiKey}`, + '-d', payload, + `${hubUrl.replace(/\/+$/, '')}/a2a/evolution/record`, + ], { timeout: 10000, stdio: ['pipe', 'pipe', 'pipe'], shell: false }); + if (res.status !== 0 || res.error) return false; return true; } catch { return false; diff --git a/src/gep/signals.js b/src/gep/signals.js index 30aa79ae..5e9fb587 100644 --- a/src/gep/signals.js +++ b/src/gep/signals.js @@ -254,24 +254,29 @@ function _extractLLM(corpus) { var url = hubUrl + '/a2a/signal/analyze'; - // Use execSync + curl for truly synchronous HTTP. Node's http.request() is - // async and its callbacks cannot fire inside a synchronous spin-wait loop - // because execSync blocks the event loop. - var curlCmd = 'curl -s -m 10 -X POST' - + ' -H "Content-Type: application/json"' - + ' -H "Authorization: Bearer ' + nodeSecret + '"' - + ' -d ' + JSON.stringify(postData).replace(/'/g, "'\\''") - + ' ' + JSON.stringify(url); - - var execSync = require('child_process').execSync; + // Use spawnSync + curl for truly synchronous HTTP. Node's http.request() + // is async and its callbacks cannot fire inside a synchronous spin-wait + // loop because a synchronous subprocess blocks the event loop. Argv-array + // form avoids shell interpretation, so values in nodeSecret, postData, or + // url cannot escape through shell metacharacters. + var spawnSync = require('child_process').spawnSync; var stdout = ''; try { - stdout = execSync(curlCmd, { + var res = spawnSync('curl', [ + '-s', '-m', '10', '-X', 'POST', + '-H', 'Content-Type: application/json', + '-H', 'Authorization: Bearer ' + nodeSecret, + '-d', postData, + url, + ], { timeout: 12000, windowsHide: true, stdio: ['pipe', 'pipe', 'pipe'], encoding: 'utf8', + shell: false, }); + if (res.status !== 0 || res.error) return []; + stdout = res.stdout || ''; } catch (_) { return []; }