From 3c80fa2bf92f4c5888a74d1e38bd13811c26a5eb Mon Sep 17 00:00:00 2001 From: anupamme Date: Sun, 2 Aug 2026 00:15:47 +0000 Subject: [PATCH 1/3] fix: javascript.lang.security.detect-child-process.detect-child-process security vulnerability Automated security fix generated by OrbisAI Security --- scripts/postinstall.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/scripts/postinstall.js b/scripts/postinstall.js index 6b46dfda..34cf00b3 100644 --- a/scripts/postinstall.js +++ b/scripts/postinstall.js @@ -1,5 +1,5 @@ #!/usr/bin/env node -const { execSync } = require('child_process'); +const { execSync, execFileSync } = require('child_process'); const path = require('path'); const fs = require('fs'); @@ -25,7 +25,7 @@ if (process.platform !== 'win32') { const nodeModules = path.join(__dirname, '..', 'node_modules'); findFiles(nodeModules, '.node').forEach(file => { try { - execSync(`codesign --sign - --force "${file}"`, { stdio: 'ignore' }); + execFileSync('codesign', ['--sign', '-', '--force', file], { stdio: 'ignore' }); } catch {} }); } catch {} @@ -48,10 +48,11 @@ function findFiles(dir, suffix) { try { const entries = fs.readdirSync(dir, { withFileTypes: true }); for (const entry of entries) { - const full = path.join(dir, entry.name); + const safeName = entry.name.replace(/[\\/]/g, ''); + const full = dir + path.sep + safeName; if (entry.isDirectory()) { results.push(...findFiles(full, suffix)); - } else if (entry.name.endsWith(suffix)) { + } else if (safeName.endsWith(suffix)) { results.push(full); } } From 669abd365c5c09ce3fe98be27f6edc191f3b539d Mon Sep 17 00:00:00 2001 From: Anupam Mediratta Date: Sat, 5 Sep 2026 14:30:15 +0530 Subject: [PATCH 2/3] fix: restore filename traversal, keep only the execFileSync fix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Drop the safeName sanitization added in the previous commit — backslash is a valid filename character on Unix, so stripping it corrupted paths (e.g. native\addon.node became nativeaddon.node) and skipped the contents of any directory whose name contained a backslash. execFileSync already passes the filename as a single argv element with no shell involved, so no sanitization is needed. Also guard the top-level script body with require.main so tests can require findFiles/codesignFile without running the real rebuild/codesign flow, and add regression coverage for filenames with spaces, quotes, and backslashes. Co-Authored-By: Claude Sonnet 5 --- scripts/postinstall.js | 39 +++++++++++++++----------- test/postinstall.test.js | 60 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 83 insertions(+), 16 deletions(-) create mode 100644 test/postinstall.test.js diff --git a/scripts/postinstall.js b/scripts/postinstall.js index 34cf00b3..32c1db0f 100644 --- a/scripts/postinstall.js +++ b/scripts/postinstall.js @@ -3,29 +3,31 @@ const { execSync, execFileSync } = require('child_process'); const path = require('path'); const fs = require('fs'); -// Install native dependencies for Electron -try { - execSync('npx electron-builder install-app-deps', { stdio: 'inherit' }); -} catch (err) { - console.error('electron-builder install-app-deps failed:', err.message); - // Fallback: rebuild only better-sqlite3 for Electron (node-pty uses prebuilds) - console.log('Attempting fallback: rebuilding better-sqlite3 for Electron...'); +if (require.main === module) { + // Install native dependencies for Electron try { - execSync('npx @electron/rebuild -f -m . -o better-sqlite3', { stdio: 'inherit' }); - console.log('Fallback rebuild succeeded.'); - } catch (err2) { - console.error('Fallback rebuild also failed:', err2.message); + execSync('npx electron-builder install-app-deps', { stdio: 'inherit' }); + } catch (err) { + console.error('electron-builder install-app-deps failed:', err.message); + // Fallback: rebuild only better-sqlite3 for Electron (node-pty uses prebuilds) + console.log('Attempting fallback: rebuilding better-sqlite3 for Electron...'); + try { + execSync('npx @electron/rebuild -f -m . -o better-sqlite3', { stdio: 'inherit' }); + console.log('Fallback rebuild succeeded.'); + } catch (err2) { + console.error('Fallback rebuild also failed:', err2.message); + } } } // macOS/Linux: ad-hoc codesign native modules & fix node-pty permissions -if (process.platform !== 'win32') { +if (require.main === module && process.platform !== 'win32') { // Ad-hoc codesign all .node files so macOS doesn't block them try { const nodeModules = path.join(__dirname, '..', 'node_modules'); findFiles(nodeModules, '.node').forEach(file => { try { - execFileSync('codesign', ['--sign', '-', '--force', file], { stdio: 'ignore' }); + codesignFile(file); } catch {} }); } catch {} @@ -48,14 +50,19 @@ function findFiles(dir, suffix) { try { const entries = fs.readdirSync(dir, { withFileTypes: true }); for (const entry of entries) { - const safeName = entry.name.replace(/[\\/]/g, ''); - const full = dir + path.sep + safeName; + const full = path.join(dir, entry.name); if (entry.isDirectory()) { results.push(...findFiles(full, suffix)); - } else if (safeName.endsWith(suffix)) { + } else if (entry.name.endsWith(suffix)) { results.push(full); } } } catch {} return results; } + +function codesignFile(file, sign = execFileSync) { + sign('codesign', ['--sign', '-', '--force', file], { stdio: 'ignore' }); +} + +module.exports = { findFiles, codesignFile }; diff --git a/test/postinstall.test.js b/test/postinstall.test.js new file mode 100644 index 00000000..c5e3270d --- /dev/null +++ b/test/postinstall.test.js @@ -0,0 +1,60 @@ +const test = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const os = require('node:os'); +const path = require('node:path'); + +const { findFiles, codesignFile } = require('../scripts/postinstall'); + +const SPECIAL_NAMES = ['some file.node', 'some"file.node', 'native\\addon.node']; + +function withTempDir(fn) { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'postinstall-test-')); + try { + return fn(dir); + } finally { + fs.rmSync(dir, { recursive: true, force: true }); + } +} + +for (const name of SPECIAL_NAMES) { + test(`findFiles returns the exact, unmangled path for filename: ${name}`, () => { + withTempDir(dir => { + const expected = path.join(dir, name); + fs.writeFileSync(expected, ''); + const results = findFiles(dir, '.node'); + assert.deepEqual(results, [expected]); + // The regressed behavior stripped \\ and / from the name, breaking the path. + assert.ok(fs.existsSync(results[0])); + }); + }); +} + +test('findFiles descends into directories whose name contains a backslash', () => { + withTempDir(dir => { + const subdir = path.join(dir, 'sub\\dir'); + fs.mkdirSync(subdir); + const expected = path.join(subdir, 'addon.node'); + fs.writeFileSync(expected, ''); + const results = findFiles(dir, '.node'); + assert.deepEqual(results, [expected]); + }); +}); + +for (const name of SPECIAL_NAMES) { + test(`codesignFile passes filename unchanged to the signer for: ${name}`, () => { + withTempDir(dir => { + const file = path.join(dir, name); + fs.writeFileSync(file, ''); + const calls = []; + const stubSign = (...args) => calls.push(args); + codesignFile(file, stubSign); + assert.equal(calls.length, 1); + assert.deepEqual(calls[0], [ + 'codesign', + ['--sign', '-', '--force', file], + { stdio: 'ignore' }, + ]); + }); + }); +} From 3ba1600ef567164445664fd57ed0b1bcccbf3b34 Mon Sep 17 00:00:00 2001 From: Ali Basiri Date: Sat, 5 Sep 2026 15:18:58 -0700 Subject: [PATCH 3/3] test: make postinstall regression coverage portable to Windows --- .github/workflows/build.yml | 3 +++ test/postinstall.test.js | 33 ++++++++++++++++++--------------- 2 files changed, 21 insertions(+), 15 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 80804be6..ff145fda 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -48,6 +48,9 @@ jobs: node-version: 20 cache: npm + - name: Test postinstall helpers + run: node --test test/postinstall.test.js + - uses: actions/setup-python@v5 with: python-version: '3.12' diff --git a/test/postinstall.test.js b/test/postinstall.test.js index c5e3270d..bf39bc9c 100644 --- a/test/postinstall.test.js +++ b/test/postinstall.test.js @@ -18,7 +18,10 @@ function withTempDir(fn) { } for (const name of SPECIAL_NAMES) { - test(`findFiles returns the exact, unmangled path for filename: ${name}`, () => { + test(`findFiles returns the exact, unmangled path for filename: ${name}`, { + // Quotes are invalid and backslashes are path separators on Windows. + skip: process.platform === 'win32' && /["\\]/.test(name), + }, () => { withTempDir(dir => { const expected = path.join(dir, name); fs.writeFileSync(expected, ''); @@ -30,7 +33,9 @@ for (const name of SPECIAL_NAMES) { }); } -test('findFiles descends into directories whose name contains a backslash', () => { +test('findFiles descends into directories whose name contains a backslash', { + skip: process.platform === 'win32', +}, () => { withTempDir(dir => { const subdir = path.join(dir, 'sub\\dir'); fs.mkdirSync(subdir); @@ -43,18 +48,16 @@ test('findFiles descends into directories whose name contains a backslash', () = for (const name of SPECIAL_NAMES) { test(`codesignFile passes filename unchanged to the signer for: ${name}`, () => { - withTempDir(dir => { - const file = path.join(dir, name); - fs.writeFileSync(file, ''); - const calls = []; - const stubSign = (...args) => calls.push(args); - codesignFile(file, stubSign); - assert.equal(calls.length, 1); - assert.deepEqual(calls[0], [ - 'codesign', - ['--sign', '-', '--force', file], - { stdio: 'ignore' }, - ]); - }); + // A stubbed signer only needs a path string, not a real filesystem entry. + const file = path.join(os.tmpdir(), name); + const calls = []; + const stubSign = (...args) => calls.push(args); + codesignFile(file, stubSign); + assert.equal(calls.length, 1); + assert.deepEqual(calls[0], [ + 'codesign', + ['--sign', '-', '--force', file], + { stdio: 'ignore' }, + ]); }); }