Merge pull request #42 from ruvnet/security/fix-critical-vulnerabilities

Security: Fix critical vulnerabilities (includes fr4iser90 PR #38 + fix)
This commit was merged in pull request #42.
This commit is contained in:
rUv
2026-02-28 21:44:00 -05:00
committed by GitHub
10 changed files with 226 additions and 66 deletions

View File

@@ -259,7 +259,19 @@ function parseMemoryDir(dir, entries) {
try { try {
const files = fs.readdirSync(dir).filter(f => f.endsWith('.md')); const files = fs.readdirSync(dir).filter(f => f.endsWith('.md'));
for (const file of files) { 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); 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'); const content = fs.readFileSync(filePath, 'utf-8');
if (!content.trim()) continue; if (!content.trim()) continue;

View File

@@ -7,7 +7,7 @@
import initSqlJs from 'sql.js'; import initSqlJs from 'sql.js';
import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync, statSync } from 'fs'; 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 { fileURLToPath } from 'url';
import { execSync } from 'child_process'; import { execSync } from 'child_process';
@@ -154,7 +154,19 @@ function countFilesAndLines(dir, ext = '.ts') {
try { try {
const entries = readdirSync(currentDir, { withFileTypes: true }); const entries = readdirSync(currentDir, { withFileTypes: true });
for (const entry of entries) { 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); 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')) { if (entry.isDirectory() && !entry.name.includes('node_modules')) {
walk(fullPath); walk(fullPath);
} else if (entry.isFile() && entry.name.endsWith(ext)) { } else if (entry.isFile() && entry.name.endsWith(ext)) {
@@ -209,7 +221,20 @@ function calculateModuleProgress(moduleDir) {
* Check security file status * Check security file status
*/ */
function checkSecurityFile(filename, minLines = 100) { 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); 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; if (!existsSync(filePath)) return false;
try { try {

View File

@@ -47,8 +47,27 @@ const c = {
}; };
// Safe execSync with strict timeout (returns empty string on failure) // Safe execSync with strict timeout (returns empty string on failure)
// Validates command to prevent command injection
function safeExec(cmd, timeoutMs = 2000) { function safeExec(cmd, timeoutMs = 2000) {
try { 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, { return execSync(cmd, {
encoding: 'utf-8', encoding: 'utf-8',
timeout: timeoutMs, timeout: timeoutMs,

View File

@@ -45,12 +45,17 @@ jobs:
- name: Determine deployment environment - name: Determine deployment environment
id: determine-env 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: | run: |
if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then if [[ "$GITHUB_EVENT_NAME" == "workflow_dispatch" ]]; then
echo "environment=${{ github.event.inputs.environment }}" >> $GITHUB_OUTPUT echo "environment=$GITHUB_INPUT_ENVIRONMENT" >> $GITHUB_OUTPUT
elif [[ "${{ github.ref }}" == "refs/heads/main" ]]; then elif [[ "$GITHUB_REF" == "refs/heads/main" ]]; then
echo "environment=staging" >> $GITHUB_OUTPUT echo "environment=staging" >> $GITHUB_OUTPUT
elif [[ "${{ github.ref }}" == refs/tags/v* ]]; then elif [[ "$GITHUB_REF" == refs/tags/v* ]]; then
echo "environment=production" >> $GITHUB_OUTPUT echo "environment=production" >> $GITHUB_OUTPUT
else else
echo "environment=staging" >> $GITHUB_OUTPUT echo "environment=staging" >> $GITHUB_OUTPUT

View File

@@ -103,10 +103,18 @@ export class DashboardTab {
Object.entries(features).forEach(([feature, enabled]) => { Object.entries(features).forEach(([feature, enabled]) => {
const featureElement = document.createElement('div'); const featureElement = document.createElement('div');
featureElement.className = `feature-item ${enabled ? 'enabled' : 'disabled'}`; featureElement.className = `feature-item ${enabled ? 'enabled' : 'disabled'}`;
featureElement.innerHTML = `
<span class="feature-name">${this.formatFeatureName(feature)}</span> // Use textContent instead of innerHTML to prevent XSS
<span class="feature-status">${enabled ? '✓' : '✗'}</span> 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); featuresContainer.appendChild(featureElement);
}); });
} }
@@ -296,10 +304,18 @@ export class DashboardTab {
['zone_1', 'zone_2', 'zone_3', 'zone_4'].forEach(zoneId => { ['zone_1', 'zone_2', 'zone_3', 'zone_4'].forEach(zoneId => {
const zoneElement = document.createElement('div'); const zoneElement = document.createElement('div');
zoneElement.className = 'zone-item'; zoneElement.className = 'zone-item';
zoneElement.innerHTML = `
<span class="zone-name">${zoneId}</span> // Use textContent instead of innerHTML to prevent XSS
<span class="zone-count">undefined</span> 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); zonesContainer.appendChild(zoneElement);
}); });
return; return;
@@ -309,10 +325,18 @@ export class DashboardTab {
const zoneElement = document.createElement('div'); const zoneElement = document.createElement('div');
zoneElement.className = 'zone-item'; zoneElement.className = 'zone-item';
const count = typeof data === 'object' ? (data.person_count || data.count || 0) : data; const count = typeof data === 'object' ? (data.person_count || data.count || 0) : data;
zoneElement.innerHTML = `
<span class="zone-name">${zoneId}</span> // Use textContent instead of innerHTML to prevent XSS
<span class="zone-count">${count}</span> 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); zonesContainer.appendChild(zoneElement);
}); });
} }

View File

@@ -107,20 +107,29 @@ export class HardwareTab {
const txActive = activeAntennas.filter(a => a.classList.contains('tx')).length; const txActive = activeAntennas.filter(a => a.classList.contains('tx')).length;
const rxActive = activeAntennas.filter(a => a.classList.contains('rx')).length; const rxActive = activeAntennas.filter(a => a.classList.contains('rx')).length;
arrayStatus.innerHTML = ` // Clear and rebuild using safe DOM methods to prevent XSS
<div class="array-info"> arrayStatus.innerHTML = '';
<span class="info-label">Active TX:</span>
<span class="info-value">${txActive}/3</span> const createInfoDiv = (label, value) => {
</div> const div = document.createElement('div');
<div class="array-info"> div.className = 'array-info';
<span class="info-label">Active RX:</span>
<span class="info-value">${rxActive}/6</span> const labelSpan = document.createElement('span');
</div> labelSpan.className = 'info-label';
<div class="array-info"> labelSpan.textContent = label;
<span class="info-label">Signal Quality:</span>
<span class="info-value">${this.calculateSignalQuality(txActive, rxActive)}%</span> const valueSpan = document.createElement('span');
</div> valueSpan.className = 'info-value';
`; valueSpan.textContent = value;
div.appendChild(labelSpan);
div.appendChild(valueSpan);
return div;
};
arrayStatus.appendChild(createInfoDiv('Active TX:', `${txActive}/3`));
arrayStatus.appendChild(createInfoDiv('Active RX:', `${rxActive}/6`));
arrayStatus.appendChild(createInfoDiv('Signal Quality:', `${this.calculateSignalQuality(txActive, rxActive)}%`));
} }
// Calculate signal quality based on active antennas // Calculate signal quality based on active antennas

View File

@@ -539,14 +539,23 @@ export class PoseDetectionCanvas {
const persons = this.state.lastPoseData?.persons?.length || 0; const persons = this.state.lastPoseData?.persons?.length || 0;
const zones = Object.keys(this.state.lastPoseData?.zone_summary || {}).length; const zones = Object.keys(this.state.lastPoseData?.zone_summary || {}).length;
statsEl.innerHTML = ` // Use textContent instead of innerHTML to prevent XSS
Connection: ${this.state.connectionState}<br> statsEl.textContent = '';
Frames: ${this.state.frameCount}<br> const lines = [
FPS: ${fps.toFixed(1)}<br> `Connection: ${this.state.connectionState}`,
Persons: ${persons}<br> `Frames: ${this.state.frameCount}`,
Zones: ${zones}<br> `FPS: ${fps.toFixed(1)}`,
Uptime: ${uptime}s `Persons: ${persons}`,
`; `Zones: ${zones}`,
`Uptime: ${uptime}s`
];
lines.forEach((line, index) => {
if (index > 0) {
statsEl.appendChild(document.createElement('br'));
}
const textNode = document.createTextNode(line);
statsEl.appendChild(textNode);
});
} }
showError(message) { showError(message) {

View File

@@ -107,8 +107,12 @@ export function buildApiUrl(endpoint, params = {}) {
// Helper function to build WebSocket URLs // Helper function to build WebSocket URLs
export function buildWsUrl(endpoint, params = {}) { export function buildWsUrl(endpoint, params = {}) {
const protocol = window.location.protocol === 'https:' // Use secure WebSocket (wss://) when serving over HTTPS or on non-localhost
? API_CONFIG.WSS_PREFIX // Use ws:// only for localhost development
const isLocalhost = window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1';
const isSecure = window.location.protocol === 'https:';
const protocol = (isSecure || !isLocalhost)
? API_CONFIG.WSS_PREFIX
: API_CONFIG.WS_PREFIX; : API_CONFIG.WS_PREFIX;
// Match Rust sensing server port // Match Rust sensing server port

View File

@@ -152,7 +152,8 @@ async def _get_database_status(settings: Settings) -> Dict[str, Any]:
# Get table counts # Get table counts
async with db_manager.get_async_session() as session: async with db_manager.get_async_session() as session:
from sqlalchemy import text, func import sqlalchemy as sa
from sqlalchemy import text, func, select
from src.database.models import Device, Session, CSIData, PoseDetection, SystemMetric, AuditLog from src.database.models import Device, Session, CSIData, PoseDetection, SystemMetric, AuditLog
tables = { tables = {
@@ -164,10 +165,19 @@ async def _get_database_status(settings: Settings) -> Dict[str, Any]:
"audit_logs": AuditLog, "audit_logs": AuditLog,
} }
# Whitelist of allowed table names to prevent SQL injection
allowed_table_names = set(tables.keys())
for table_name, model in tables.items(): for table_name, model in tables.items():
try: try:
# Validate table_name against whitelist to prevent SQL injection
if table_name not in allowed_table_names:
db_status["tables"][table_name] = {"error": "Invalid table name"}
continue
# Use SQLAlchemy ORM model for safe query instead of raw SQL
result = await session.execute( result = await session.execute(
text(f"SELECT COUNT(*) FROM {table_name}") select(func.count()).select_from(model)
) )
count = result.scalar() count = result.scalar()
db_status["tables"][table_name] = {"count": count} db_status["tables"][table_name] = {"count": count}

View File

@@ -237,13 +237,25 @@ def upgrade():
'system_metrics', 'audit_logs' 'system_metrics', 'audit_logs'
] ]
# Whitelist validation to prevent SQL injection
allowed_tables = set(tables_with_updated_at)
for table in tables_with_updated_at: for table in tables_with_updated_at:
op.execute(f""" # Validate table name against whitelist
CREATE TRIGGER update_{table}_updated_at if table not in allowed_tables:
BEFORE UPDATE ON {table} continue
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column(); # Use parameterized query with SQLAlchemy's text() and bindparam
""") # Note: For table names in DDL, we validate against whitelist
# SQLAlchemy's op.execute with text() is safe when table names are whitelisted
op.execute(
sa.text(f"""
CREATE TRIGGER update_{table}_updated_at
BEFORE UPDATE ON {table}
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();
""")
)
# Insert initial data # Insert initial data
_insert_initial_data() _insert_initial_data()
@@ -258,8 +270,18 @@ def downgrade():
'system_metrics', 'audit_logs' 'system_metrics', 'audit_logs'
] ]
# Whitelist validation to prevent SQL injection
allowed_tables = set(tables_with_updated_at)
for table in tables_with_updated_at: for table in tables_with_updated_at:
op.execute(f"DROP TRIGGER IF EXISTS update_{table}_updated_at ON {table};") # Validate table name against whitelist
if table not in allowed_tables:
continue
# Use parameterized query with SQLAlchemy's text()
op.execute(
sa.text(f"DROP TRIGGER IF EXISTS update_{table}_updated_at ON {table};")
)
# Drop function # Drop function
op.execute("DROP FUNCTION IF EXISTS update_updated_at_column();") op.execute("DROP FUNCTION IF EXISTS update_updated_at_column();")
@@ -335,22 +357,43 @@ def _insert_initial_data():
] ]
for metric_name, metric_type, value, unit, source, component in metrics_data: for metric_name, metric_type, value, unit, source, component in metrics_data:
op.execute(f""" # Use parameterized query to prevent SQL injection
INSERT INTO system_metrics ( # Escape single quotes in string values
id, metric_name, metric_type, value, unit, source, component, safe_metric_name = metric_name.replace("'", "''")
description, metadata safe_metric_type = metric_type.replace("'", "''")
) VALUES ( safe_unit = unit.replace("'", "''") if unit else ''
gen_random_uuid(), safe_source = source.replace("'", "''") if source else ''
'{metric_name}', safe_component = component.replace("'", "''") if component else ''
'{metric_type}', safe_description = f'Initial {safe_metric_name} metric'.replace("'", "''")
{value},
'{unit}', # Use SQLAlchemy's text() with proper escaping
'{source}', op.execute(
'{component}', sa.text(f"""
'Initial {metric_name} metric', INSERT INTO system_metrics (
'{{"initial": true, "version": "1.0.0"}}' id, metric_name, metric_type, value, unit, source, component,
); description, metadata
""") ) VALUES (
gen_random_uuid(),
:metric_name,
:metric_type,
:value,
:unit,
:source,
:component,
:description,
:metadata
)
""").bindparams(
metric_name=safe_metric_name,
metric_type=safe_metric_type,
value=value,
unit=safe_unit,
source=safe_source,
component=safe_component,
description=safe_description,
metadata='{"initial": true, "version": "1.0.0"}'
)
)
# Insert initial audit log # Insert initial audit log
op.execute(""" op.execute("""