diff --git a/.claude/helpers/intelligence.cjs b/.claude/helpers/intelligence.cjs index e4cc631..a182ace 100644 --- a/.claude/helpers/intelligence.cjs +++ b/.claude/helpers/intelligence.cjs @@ -259,7 +259,19 @@ function parseMemoryDir(dir, entries) { try { const files = fs.readdirSync(dir).filter(f => f.endsWith('.md')); for (const file of files) { + // Validate file name to prevent path traversal + if (file.includes('..') || file.includes('/') || file.includes('\\')) { + continue; + } + const filePath = path.join(dir, file); + // Additional validation: ensure resolved path is within the base directory + const resolvedPath = path.resolve(filePath); + const resolvedDir = path.resolve(dir); + if (!resolvedPath.startsWith(resolvedDir)) { + continue; // Path traversal attempt detected + } + const content = fs.readFileSync(filePath, 'utf-8'); if (!content.trim()) continue; diff --git a/.claude/helpers/metrics-db.mjs b/.claude/helpers/metrics-db.mjs index 510ada9..cbe6344 100755 --- a/.claude/helpers/metrics-db.mjs +++ b/.claude/helpers/metrics-db.mjs @@ -7,7 +7,7 @@ import initSqlJs from 'sql.js'; import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync, statSync } from 'fs'; -import { dirname, join, basename } from 'path'; +import { dirname, join, basename, resolve } from 'path'; import { fileURLToPath } from 'url'; import { execSync } from 'child_process'; @@ -154,7 +154,19 @@ function countFilesAndLines(dir, ext = '.ts') { try { const entries = readdirSync(currentDir, { withFileTypes: true }); for (const entry of entries) { + // Validate entry name to prevent path traversal + if (entry.name.includes('..') || entry.name.includes('/') || entry.name.includes('\\')) { + continue; + } + const fullPath = join(currentDir, entry.name); + // Additional validation: ensure resolved path is within the base directory + const resolvedPath = resolve(fullPath); + const resolvedCurrentDir = resolve(currentDir); + if (!resolvedPath.startsWith(resolvedCurrentDir)) { + continue; // Path traversal attempt detected + } + if (entry.isDirectory() && !entry.name.includes('node_modules')) { walk(fullPath); } else if (entry.isFile() && entry.name.endsWith(ext)) { @@ -209,7 +221,20 @@ function calculateModuleProgress(moduleDir) { * Check security file status */ function checkSecurityFile(filename, minLines = 100) { + // Validate filename to prevent path traversal + if (filename.includes('..') || filename.includes('/') || filename.includes('\\')) { + return false; + } + const filePath = join(V3_DIR, '@claude-flow/security/src', filename); + + // Additional validation: ensure resolved path is within the expected directory + const resolvedPath = resolve(filePath); + const expectedDir = resolve(join(V3_DIR, '@claude-flow/security/src')); + if (!resolvedPath.startsWith(expectedDir)) { + return false; // Path traversal attempt detected + } + if (!existsSync(filePath)) return false; try { diff --git a/.claude/helpers/statusline.cjs b/.claude/helpers/statusline.cjs index 602907f..0f4b1e5 100644 --- a/.claude/helpers/statusline.cjs +++ b/.claude/helpers/statusline.cjs @@ -47,8 +47,27 @@ const c = { }; // Safe execSync with strict timeout (returns empty string on failure) +// Validates command to prevent command injection function safeExec(cmd, timeoutMs = 2000) { try { + // Validate command to prevent command injection + // Only allow commands that match safe patterns (no shell metacharacters) + if (typeof cmd !== 'string') { + return ''; + } + + // Check for dangerous shell metacharacters that could allow injection + const dangerousChars = /[;&|`$(){}[\]<>'"\\]/; + if (dangerousChars.test(cmd)) { + // If dangerous chars found, only allow if it's a known safe pattern + // Allow 'sh -c' with single-quoted script (already escaped) + const safeShPattern = /^sh\s+-c\s+'[^']*'$/; + if (!safeShPattern.test(cmd)) { + console.warn('safeExec: Command contains potentially dangerous characters'); + return ''; + } + } + return execSync(cmd, { encoding: 'utf-8', timeout: timeoutMs, diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 86af02f..93990f5 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -45,12 +45,17 @@ jobs: - name: Determine deployment environment id: determine-env + env: + # Use environment variable to prevent shell injection + GITHUB_EVENT_NAME: ${{ github.event_name }} + GITHUB_REF: ${{ github.ref }} + GITHUB_INPUT_ENVIRONMENT: ${{ github.event.inputs.environment }} run: | - if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then - echo "environment=${{ github.event.inputs.environment }}" >> $GITHUB_OUTPUT - elif [[ "${{ github.ref }}" == "refs/heads/main" ]]; then + if [[ "$GITHUB_EVENT_NAME" == "workflow_dispatch" ]]; then + echo "environment=$GITHUB_INPUT_ENVIRONMENT" >> $GITHUB_OUTPUT + elif [[ "$GITHUB_REF" == "refs/heads/main" ]]; then echo "environment=staging" >> $GITHUB_OUTPUT - elif [[ "${{ github.ref }}" == refs/tags/v* ]]; then + elif [[ "$GITHUB_REF" == refs/tags/v* ]]; then echo "environment=production" >> $GITHUB_OUTPUT else echo "environment=staging" >> $GITHUB_OUTPUT diff --git a/ui/components/DashboardTab.js b/ui/components/DashboardTab.js index 951f029..e456eda 100644 --- a/ui/components/DashboardTab.js +++ b/ui/components/DashboardTab.js @@ -103,10 +103,18 @@ export class DashboardTab { Object.entries(features).forEach(([feature, enabled]) => { const featureElement = document.createElement('div'); featureElement.className = `feature-item ${enabled ? 'enabled' : 'disabled'}`; - featureElement.innerHTML = ` - ${this.formatFeatureName(feature)} - ${enabled ? '✓' : '✗'} - `; + + // Use textContent instead of innerHTML to prevent XSS + const featureNameSpan = document.createElement('span'); + featureNameSpan.className = 'feature-name'; + featureNameSpan.textContent = this.formatFeatureName(feature); + + const featureStatusSpan = document.createElement('span'); + featureStatusSpan.className = 'feature-status'; + featureStatusSpan.textContent = enabled ? '✓' : '✗'; + + featureElement.appendChild(featureNameSpan); + featureElement.appendChild(featureStatusSpan); featuresContainer.appendChild(featureElement); }); } @@ -296,10 +304,18 @@ export class DashboardTab { ['zone_1', 'zone_2', 'zone_3', 'zone_4'].forEach(zoneId => { const zoneElement = document.createElement('div'); zoneElement.className = 'zone-item'; - zoneElement.innerHTML = ` - ${zoneId} - undefined - `; + + // Use textContent instead of innerHTML to prevent XSS + const zoneNameSpan = document.createElement('span'); + zoneNameSpan.className = 'zone-name'; + zoneNameSpan.textContent = zoneId; + + const zoneCountSpan = document.createElement('span'); + zoneCountSpan.className = 'zone-count'; + zoneCountSpan.textContent = 'undefined'; + + zoneElement.appendChild(zoneNameSpan); + zoneElement.appendChild(zoneCountSpan); zonesContainer.appendChild(zoneElement); }); return; @@ -309,10 +325,18 @@ export class DashboardTab { const zoneElement = document.createElement('div'); zoneElement.className = 'zone-item'; const count = typeof data === 'object' ? (data.person_count || data.count || 0) : data; - zoneElement.innerHTML = ` - ${zoneId} - ${count} - `; + + // Use textContent instead of innerHTML to prevent XSS + const zoneNameSpan = document.createElement('span'); + zoneNameSpan.className = 'zone-name'; + zoneNameSpan.textContent = zoneId; + + const zoneCountSpan = document.createElement('span'); + zoneCountSpan.className = 'zone-count'; + zoneCountSpan.textContent = String(count); + + zoneElement.appendChild(zoneNameSpan); + zoneElement.appendChild(zoneCountSpan); zonesContainer.appendChild(zoneElement); }); } diff --git a/ui/components/HardwareTab.js b/ui/components/HardwareTab.js index baef164..7f36113 100644 --- a/ui/components/HardwareTab.js +++ b/ui/components/HardwareTab.js @@ -107,20 +107,29 @@ export class HardwareTab { const txActive = activeAntennas.filter(a => a.classList.contains('tx')).length; const rxActive = activeAntennas.filter(a => a.classList.contains('rx')).length; - arrayStatus.innerHTML = ` -