diff --git a/scripts/commands/pushAll.js b/scripts/commands/pushAll.js index 0329a65..7a72e4f 100644 --- a/scripts/commands/pushAll.js +++ b/scripts/commands/pushAll.js @@ -1,7 +1,8 @@ -import { execSync } from 'child_process'; +// Note: Using spawn instead of execSync for security (prevents command injection) /** * Push to all remotes + * Security: Uses spawn with argument arrays to prevent command injection */ export async function pushAll(args, server, json) { // Check for help flag @@ -47,9 +48,20 @@ Notes: const dryRun = args.includes('--dry-run') || args.includes('-n'); // Get all remotes + // Security: Use spawn with argument arrays to prevent command injection let remotes = []; try { - const remoteOutput = execSync('git remote', { encoding: 'utf-8' }).trim(); + const { spawn } = await import('child_process'); + const remoteOutput = await new Promise((resolve, reject) => { + const proc = spawn('git', ['remote'], { encoding: 'utf-8' }); + let output = ''; + proc.stdout.on('data', (chunk) => { output += chunk.toString(); }); + proc.on('close', (code) => { + if (code === 0) resolve(output.trim()); + else reject(new Error(`git remote exited with code ${code}`)); + }); + proc.on('error', reject); + }); remotes = remoteOutput.split('\n').filter(r => r.trim()); } catch (err) { console.error('Error: Not in a git repository or unable to read remotes'); @@ -85,11 +97,20 @@ Notes: console.log(`\nPushing to ${remote}...`); } + // Security: Use spawn with argument arrays to prevent command injection + const { spawn } = await import('child_process'); const command = ['push', remote, ...pushArgs]; - execSync(`git ${command.join(' ')}`, { - stdio: json ? 'pipe' : 'inherit', - encoding: 'utf-8' + await new Promise((resolve, reject) => { + const proc = spawn('git', command, { + stdio: json ? 'pipe' : 'inherit', + encoding: 'utf-8' + }); + proc.on('close', (code) => { + if (code === 0) resolve(); + else reject(new Error(`git push exited with code ${code}`)); + }); + proc.on('error', reject); }); results.push({ remote, status: 'success' }); diff --git a/scripts/git-commit-msg-hook.js b/scripts/git-commit-msg-hook.js index d94d27f..830ac33 100755 --- a/scripts/git-commit-msg-hook.js +++ b/scripts/git-commit-msg-hook.js @@ -33,8 +33,8 @@ import { finalizeEvent, getPublicKey, nip19 } from 'nostr-tools'; import { publishToRelays } from './relay/publisher.js'; import { enhanceRelayList } from './relay/relay-fetcher.js'; -import { readFileSync, writeFileSync, existsSync } from 'fs'; -import { execSync } from 'child_process'; +import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs'; +import { spawnSync } from 'child_process'; import { join, dirname, resolve } from 'path'; // Commit signature event kind (1640) @@ -76,10 +76,18 @@ function decodeNostrKey(key) { /** * Get git config value + * Security: Validates key to prevent command injection */ function getGitConfig(key) { try { - return execSync(`git config --get ${key}`, { encoding: 'utf-8' }).trim() || null; + // Validate key to prevent injection (git config keys are alphanumeric with dots and hyphens) + if (!key || typeof key !== 'string' || !/^[a-zA-Z0-9.-]+$/.test(key)) { + return null; + } + // Security: Use spawnSync with argument array instead of string concatenation + const result = spawnSync('git', ['config', '--get', key], { encoding: 'utf-8' }); + if (result.status !== 0) return null; + return result.stdout.trim() || null; } catch { return null; } @@ -94,8 +102,10 @@ function getGitConfig(key) { function isGitRepublicRepo() { try { // Get all remotes - const remotes = execSync('git remote -v', { encoding: 'utf-8' }); - const remoteLines = remotes.split('\n').filter(line => line.trim()); + // Security: Use spawnSync with argument arrays + const result = spawnSync('git', ['remote', '-v'], { encoding: 'utf-8' }); + if (result.status !== 0) return false; + const remoteLines = result.stdout.split('\n').filter(line => line.trim()); // Check if any remote URL matches GitRepublic patterns // GitRepublic URLs use specific path patterns to distinguish from GRASP: @@ -303,7 +313,8 @@ async function signCommitMessage(commitMessageFile) { // Store in nostr/ folder in repository root const nostrDir = join(repoRoot, 'nostr'); if (!existsSync(nostrDir)) { - execSync(`mkdir -p "${nostrDir}"`, { stdio: 'ignore' }); + // Security: Use fs.mkdirSync instead of execSync for path safety + mkdirSync(nostrDir, { recursive: true }); } // Append to commit-signatures.jsonl (JSON Lines format) diff --git a/scripts/git-wrapper.js b/scripts/git-wrapper.js index bf20f9e..3ebe335 100755 --- a/scripts/git-wrapper.js +++ b/scripts/git-wrapper.js @@ -21,7 +21,7 @@ * gitrep publish */ -import { spawn, execSync } from 'child_process'; +import { spawn, spawnSync } from 'child_process'; import { createHash } from 'crypto'; import { finalizeEvent } from 'nostr-tools'; import { decode } from 'nostr-tools/nip19'; @@ -51,10 +51,17 @@ const API_COMMANDS = [ ]; // Get git remote URL +// Security: Validates remote name to prevent command injection function getRemoteUrl(remote = 'origin') { try { - const url = execSync(`git config --get remote.${remote}.url`, { encoding: 'utf-8' }).trim(); - return url; + // Validate remote name to prevent injection + if (!remote || typeof remote !== 'string' || !/^[a-zA-Z0-9_-]+$/.test(remote)) { + return null; + } + // Security: Use spawnSync with argument array instead of string concatenation + const result = spawnSync('git', ['config', '--get', `remote.${remote}.url`], { encoding: 'utf-8' }); + if (result.status !== 0) return null; + return result.stdout.trim(); } catch { return null; } diff --git a/scripts/setup.js b/scripts/setup.js index 19ba70e..eae12e7 100755 --- a/scripts/setup.js +++ b/scripts/setup.js @@ -16,8 +16,8 @@ import { fileURLToPath } from 'url'; import { dirname, join, resolve } from 'path'; -import { existsSync } from 'fs'; -import { execSync } from 'child_process'; +import { existsSync, mkdirSync, unlinkSync, symlinkSync } from 'fs'; +import { spawnSync } from 'child_process'; // Get the directory where this script is located const __filename = fileURLToPath(import.meta.url); @@ -108,19 +108,25 @@ function setupCredentialHelper() { try { let configCommand; + // Security: Use spawnSync with argument arrays to prevent command injection if (domain) { // Configure for specific domain + // Validate domain to prevent injection const protocol = domain.startsWith('https://') ? 'https' : domain.startsWith('http://') ? 'http' : 'https'; const host = domain.replace(/^https?:\/\//, '').split('/')[0]; - configCommand = `git config --global credential.${protocol}://${host}.helper '!node ${credentialScript}'`; + // Validate host format (basic check) + if (!/^[a-zA-Z0-9.-]+$/.test(host)) { + throw new Error('Invalid domain format'); + } + const configKey = `credential.${protocol}://${host}.helper`; + const configValue = `!node ${credentialScript}`; + spawnSync('git', ['config', '--global', configKey, configValue], { stdio: 'inherit' }); console.log(` Configuring for domain: ${host}`); } else { // Configure globally for all domains - configCommand = `git config --global credential.helper '!node ${credentialScript}'`; + spawnSync('git', ['config', '--global', 'credential.helper', `!node ${credentialScript}`], { stdio: 'inherit' }); console.log(' Configuring globally for all domains'); } - - execSync(configCommand, { stdio: 'inherit' }); console.log('✅ Credential helper configured successfully!\n'); } catch (error) { console.error('❌ Failed to configure credential helper:', error.message); @@ -138,21 +144,24 @@ function setupCommitHook() { const hooksDir = resolve(process.env.HOME, '.git-hooks'); // Create hooks directory if it doesn't exist + // Security: Use fs.mkdirSync instead of execSync if (!existsSync(hooksDir)) { - execSync(`mkdir -p "${hooksDir}"`, { stdio: 'inherit' }); + mkdirSync(hooksDir, { recursive: true }); } // Create symlink const hookPath = join(hooksDir, 'commit-msg'); if (existsSync(hookPath)) { console.log(' Removing existing hook...'); - execSync(`rm "${hookPath}"`, { stdio: 'inherit' }); + unlinkSync(hookPath); } - execSync(`ln -s "${commitHookScript}" "${hookPath}"`, { stdio: 'inherit' }); + // Security: Use fs.symlinkSync instead of execSync + symlinkSync(commitHookScript, hookPath); // Configure git to use global hooks - execSync('git config --global core.hooksPath ~/.git-hooks', { stdio: 'inherit' }); + // Note: Using ~/.git-hooks is safe as it's a literal string, not user input + spawnSync('git', ['config', '--global', 'core.hooksPath', '~/.git-hooks'], { stdio: 'inherit' }); console.log('✅ Commit hook installed globally for all repositories!\n'); } else { @@ -166,18 +175,20 @@ function setupCommitHook() { const hookPath = join(gitDir, 'hooks', 'commit-msg'); // Create hooks directory if it doesn't exist + // Security: Use fs.mkdirSync instead of execSync const hooksDir = join(gitDir, 'hooks'); if (!existsSync(hooksDir)) { - execSync(`mkdir -p "${hooksDir}"`, { stdio: 'inherit' }); + mkdirSync(hooksDir, { recursive: true }); } // Create symlink + // Security: Use fs operations instead of execSync if (existsSync(hookPath)) { console.log(' Removing existing hook...'); - execSync(`rm "${hookPath}"`, { stdio: 'inherit' }); + unlinkSync(hookPath); } - execSync(`ln -s "${commitHookScript}" "${hookPath}"`, { stdio: 'inherit' }); + symlinkSync(commitHookScript, hookPath); console.log('✅ Commit hook installed for current repository!\n'); } @@ -235,3 +246,4 @@ if (!secretKey) { } console.log('2. Test credential helper: gitrep clone gitrepublic-web'); console.log('3. Test commit signing: gitrep commit -m "Test commit"'); + diff --git a/scripts/uninstall.js b/scripts/uninstall.js index 831b18c..0a933aa 100755 --- a/scripts/uninstall.js +++ b/scripts/uninstall.js @@ -5,7 +5,7 @@ * Removes all GitRepublic CLI configuration from your system */ -import { execSync } from 'child_process'; +import { spawnSync } from 'child_process'; import { existsSync, unlinkSync, rmdirSync, readFileSync, writeFileSync } from 'fs'; import { join } from 'path'; import { homedir } from 'os'; @@ -96,23 +96,28 @@ function main() { // Remove credential helper configurations console.log('Removing git credential helper configurations...'); try { - const credentialConfigs = execSync('git config --global --get-regexp credential.*helper', { encoding: 'utf-8' }) - .split('\n') - .filter(line => line.trim() && line.includes('gitrepublic') || line.includes('git-credential-nostr')); - - for (const config of credentialConfigs) { - if (config.trim()) { - const key = config.split(' ')[0]; - if (key) { - console.log(` - ${key}`); - if (!dryRun) { - try { - execSync(`git config --global --unset "${key}"`, { stdio: 'ignore' }); - } catch { - // Ignore if already removed + // Security: Use spawnSync with argument arrays + const result = spawnSync('git', ['config', '--global', '--get-regexp', 'credential.*helper'], { encoding: 'utf-8' }); + if (result.status === 0) { + const credentialConfigs = result.stdout + .split('\n') + .filter(line => line.trim() && (line.includes('gitrepublic') || line.includes('git-credential-nostr'))); + + for (const config of credentialConfigs) { + if (config.trim()) { + const key = config.split(' ')[0]; + if (key) { + console.log(` - ${key}`); + if (!dryRun) { + try { + // Security: Use spawnSync with argument arrays + spawnSync('git', ['config', '--global', '--unset', key], { stdio: 'ignore' }); + } catch { + // Ignore if already removed + } } + removed++; } - removed++; } } } @@ -123,7 +128,9 @@ function main() { // Remove commit hook (global) console.log('\nRemoving global commit hook...'); try { - const hooksPath = execSync('git config --global --get core.hooksPath', { encoding: 'utf-8' }).trim(); + // Security: Use spawnSync with argument arrays + const result = spawnSync('git', ['config', '--global', '--get', 'core.hooksPath'], { encoding: 'utf-8' }); + const hooksPath = result.status === 0 ? result.stdout.trim() : null; if (hooksPath) { const hookFile = join(hooksPath, 'commit-msg'); if (existsSync(hookFile)) { @@ -147,7 +154,8 @@ function main() { // Remove core.hooksPath config try { - execSync('git config --global --unset core.hooksPath', { stdio: 'ignore' }); + // Security: Use spawnSync with argument arrays + spawnSync('git', ['config', '--global', '--unset', 'core.hooksPath'], { stdio: 'ignore' }); if (!dryRun) { console.log(' - Removed core.hooksPath configuration'); } diff --git a/scripts/utils/event-storage.js b/scripts/utils/event-storage.js index a57ef28..eb2f879 100644 --- a/scripts/utils/event-storage.js +++ b/scripts/utils/event-storage.js @@ -1,9 +1,9 @@ -import { writeFileSync, existsSync } from 'fs'; -import { execSync } from 'child_process'; +import { writeFileSync, existsSync, mkdirSync } from 'fs'; import { join, dirname } from 'path'; /** * Store event in appropriate JSONL file based on event kind + * Security: Uses fs.mkdirSync instead of execSync to prevent command injection */ export function storeEventInJsonl(event) { try { @@ -28,9 +28,10 @@ export function storeEventInJsonl(event) { } // Create nostr/ directory if it doesn't exist + // Security: Use fs.mkdirSync instead of execSync for path safety const nostrDir = join(repoRoot, 'nostr'); if (!existsSync(nostrDir)) { - execSync(`mkdir -p "${nostrDir}"`, { stdio: 'ignore' }); + mkdirSync(nostrDir, { recursive: true }); } // Determine JSONL file name based on event kind