From b40af6c29437bc2a824f390420c7d0cb78006d2a Mon Sep 17 00:00:00 2001 From: Jwalin Shah Date: Thu, 23 Apr 2026 22:16:44 -0700 Subject: [PATCH] fix(security): command injection in npm postinstall + weak RNG fallback (#837) - Replace `execSync` with `execFileSync` in npm install script to prevent command injection. - Pass PowerShell paths via environment variables to avoid shell metacharacter interpolation. - Add `-NoProfile` and `-NonInteractive` flags to PowerShell extraction for cleaner installs. - Upgrade `makeAccountId` to prioritize `crypto.getRandomValues` over `Math.random` for suffixes. - Update internal thread title logic to import `collapse_whitespace` from the correct location. Co-authored-by: Steven Enamakel --- app/src/pages/Accounts.tsx | 6 ++++++ packages/npm/install.js | 19 +++++++++++++------ src/openhuman/threads/ops.rs | 6 +++--- 3 files changed, 22 insertions(+), 9 deletions(-) diff --git a/app/src/pages/Accounts.tsx b/app/src/pages/Accounts.tsx index 192821421..cdc5cde7e 100644 --- a/app/src/pages/Accounts.tsx +++ b/app/src/pages/Accounts.tsx @@ -18,6 +18,12 @@ import { AgentChatPanel } from './Conversations'; function makeAccountId(): string { const c = globalThis.crypto; if (c && typeof c.randomUUID === 'function') return c.randomUUID(); + if (c && typeof c.getRandomValues === 'function') { + const bytes = new Uint8Array(4); + c.getRandomValues(bytes); + const suffix = Array.from(bytes, b => b.toString(16).padStart(2, '0')).join(''); + return `acct-${Date.now().toString(36)}-${suffix}`; + } return `acct-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`; } diff --git a/packages/npm/install.js b/packages/npm/install.js index c62318566..3f4ddfdf1 100644 --- a/packages/npm/install.js +++ b/packages/npm/install.js @@ -10,7 +10,7 @@ const https = require('https'); const fs = require('fs'); const path = require('path'); const crypto = require('crypto'); -const { execSync } = require('child_process'); +const { execFileSync } = require('child_process'); const REPO = 'tinyhumansai/openhuman'; const pkg = require('./package.json'); @@ -125,17 +125,24 @@ async function main() { } console.log('[openhuman] Checksum verified.'); - // Extract + // Extract — use execFileSync (no shell interpolation) so paths with spaces + // or shell metacharacters in `tmpTarball` / `binDir` can't be injected. if (isWin) { // PowerShell is available on Windows runners - execSync( - `powershell -Command "Expand-Archive -Path '${tmpTarball}' -DestinationPath '${binDir}' -Force"`, - { stdio: 'inherit' } + execFileSync( + 'powershell', + [ + '-NoProfile', + '-NonInteractive', + '-Command', + `Expand-Archive -Path $env:TC_SRC -DestinationPath $env:TC_DEST -Force`, + ], + { stdio: 'inherit', env: { ...process.env, TC_SRC: tmpTarball, TC_DEST: binDir } } ); const extracted = path.join(binDir, 'openhuman-core.exe'); if (fs.existsSync(extracted)) fs.renameSync(extracted, binDest); } else { - execSync(`tar -xzf "${tmpTarball}" -C "${binDir}"`, { stdio: 'inherit' }); + execFileSync('tar', ['-xzf', tmpTarball, '-C', binDir], { stdio: 'inherit' }); const extracted = path.join(binDir, 'openhuman-core'); if (fs.existsSync(extracted)) { fs.renameSync(extracted, binDest); diff --git a/src/openhuman/threads/ops.rs b/src/openhuman/threads/ops.rs index dd9e06cf6..ed762d648 100644 --- a/src/openhuman/threads/ops.rs +++ b/src/openhuman/threads/ops.rs @@ -16,9 +16,9 @@ use crate::openhuman::memory::{ }; use crate::openhuman::providers::{self, ProviderRuntimeOptions}; use crate::openhuman::threads::title::{ - build_title_prompt, is_auto_generated_thread_title, sanitize_generated_title, - title_log_fingerprint, THREAD_TITLE_LOG_PREFIX, THREAD_TITLE_MODEL_HINT, - THREAD_TITLE_SYSTEM_PROMPT, + build_title_prompt, collapse_whitespace, is_auto_generated_thread_title, + sanitize_generated_title, title_log_fingerprint, THREAD_TITLE_LOG_PREFIX, + THREAD_TITLE_MODEL_HINT, THREAD_TITLE_SYSTEM_PROMPT, }; use crate::rpc::RpcOutcome; use serde::Serialize;