Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit fd8dec5

Browse files
authored
Merge pull request ruvnet#42 from ruvnet/security/fix-critical-vulnerabilities
Security: Fix critical vulnerabilities (includes fr4iser90 PR ruvnet#38 + fix)
2 parents dd419da + e320bc9 commit fd8dec5

10 files changed

Lines changed: 226 additions & 66 deletions

File tree

.claude/helpers/intelligence.cjs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,7 +259,19 @@ function parseMemoryDir(dir, entries) {
259259
try {
260260
const files = fs.readdirSync(dir).filter(f => f.endsWith('.md'));
261261
for (const file of files) {
262+
// Validate file name to prevent path traversal
263+
if (file.includes('..') || file.includes('/') || file.includes('\\')) {
264+
continue;
265+
}
266+
262267
const filePath = path.join(dir, file);
268+
// Additional validation: ensure resolved path is within the base directory
269+
const resolvedPath = path.resolve(filePath);
270+
const resolvedDir = path.resolve(dir);
271+
if (!resolvedPath.startsWith(resolvedDir)) {
272+
continue; // Path traversal attempt detected
273+
}
274+
263275
const content = fs.readFileSync(filePath, 'utf-8');
264276
if (!content.trim()) continue;
265277

.claude/helpers/metrics-db.mjs

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
import initSqlJs from 'sql.js';
99
import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync, statSync } from 'fs';
10-
import { dirname, join, basename } from 'path';
10+
import { dirname, join, basename, resolve } from 'path';
1111
import { fileURLToPath } from 'url';
1212
import { execSync } from 'child_process';
1313

@@ -154,7 +154,19 @@ function countFilesAndLines(dir, ext = '.ts') {
154154
try {
155155
const entries = readdirSync(currentDir, { withFileTypes: true });
156156
for (const entry of entries) {
157+
// Validate entry name to prevent path traversal
158+
if (entry.name.includes('..') || entry.name.includes('/') || entry.name.includes('\\')) {
159+
continue;
160+
}
161+
157162
const fullPath = join(currentDir, entry.name);
163+
// Additional validation: ensure resolved path is within the base directory
164+
const resolvedPath = resolve(fullPath);
165+
const resolvedCurrentDir = resolve(currentDir);
166+
if (!resolvedPath.startsWith(resolvedCurrentDir)) {
167+
continue; // Path traversal attempt detected
168+
}
169+
158170
if (entry.isDirectory() && !entry.name.includes('node_modules')) {
159171
walk(fullPath);
160172
} else if (entry.isFile() && entry.name.endsWith(ext)) {
@@ -209,7 +221,20 @@ function calculateModuleProgress(moduleDir) {
209221
* Check security file status
210222
*/
211223
function checkSecurityFile(filename, minLines = 100) {
224+
// Validate filename to prevent path traversal
225+
if (filename.includes('..') || filename.includes('/') || filename.includes('\\')) {
226+
return false;
227+
}
228+
212229
const filePath = join(V3_DIR, '@claude-flow/security/src', filename);
230+
231+
// Additional validation: ensure resolved path is within the expected directory
232+
const resolvedPath = resolve(filePath);
233+
const expectedDir = resolve(join(V3_DIR, '@claude-flow/security/src'));
234+
if (!resolvedPath.startsWith(expectedDir)) {
235+
return false; // Path traversal attempt detected
236+
}
237+
213238
if (!existsSync(filePath)) return false;
214239

215240
try {

.claude/helpers/statusline.cjs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,27 @@ const c = {
4747
};
4848

4949
// Safe execSync with strict timeout (returns empty string on failure)
50+
// Validates command to prevent command injection
5051
function safeExec(cmd, timeoutMs = 2000) {
5152
try {
53+
// Validate command to prevent command injection
54+
// Only allow commands that match safe patterns (no shell metacharacters)
55+
if (typeof cmd !== 'string') {
56+
return '';
57+
}
58+
59+
// Check for dangerous shell metacharacters that could allow injection
60+
const dangerousChars = /[;&|`$(){}[\]<>'"\\]/;
61+
if (dangerousChars.test(cmd)) {
62+
// If dangerous chars found, only allow if it's a known safe pattern
63+
// Allow 'sh -c' with single-quoted script (already escaped)
64+
const safeShPattern = /^sh\s+-c\s+'[^']*'$/;
65+
if (!safeShPattern.test(cmd)) {
66+
console.warn('safeExec: Command contains potentially dangerous characters');
67+
return '';
68+
}
69+
}
70+
5271
return execSync(cmd, {
5372
encoding: 'utf-8',
5473
timeout: timeoutMs,

.github/workflows/cd.yml

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,12 +45,17 @@ jobs:
4545

4646
- name: Determine deployment environment
4747
id: determine-env
48+
env:
49+
# Use environment variable to prevent shell injection
50+
GITHUB_EVENT_NAME: ${{ github.event_name }}
51+
GITHUB_REF: ${{ github.ref }}
52+
GITHUB_INPUT_ENVIRONMENT: ${{ github.event.inputs.environment }}
4853
run: |
49-
if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
50-
echo "environment=${{ github.event.inputs.environment }}" >> $GITHUB_OUTPUT
51-
elif [[ "${{ github.ref }}" == "refs/heads/main" ]]; then
54+
if [[ "$GITHUB_EVENT_NAME" == "workflow_dispatch" ]]; then
55+
echo "environment=$GITHUB_INPUT_ENVIRONMENT" >> $GITHUB_OUTPUT
56+
elif [[ "$GITHUB_REF" == "refs/heads/main" ]]; then
5257
echo "environment=staging" >> $GITHUB_OUTPUT
53-
elif [[ "${{ github.ref }}" == refs/tags/v* ]]; then
58+
elif [[ "$GITHUB_REF" == refs/tags/v* ]]; then
5459
echo "environment=production" >> $GITHUB_OUTPUT
5560
else
5661
echo "environment=staging" >> $GITHUB_OUTPUT

ui/components/DashboardTab.js

Lines changed: 36 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -103,10 +103,18 @@ export class DashboardTab {
103103
Object.entries(features).forEach(([feature, enabled]) => {
104104
const featureElement = document.createElement('div');
105105
featureElement.className = `feature-item ${enabled ? 'enabled' : 'disabled'}`;
106-
featureElement.innerHTML = `
107-
<span class="feature-name">${this.formatFeatureName(feature)}</span>
108-
<span class="feature-status">${enabled ? '✓' : '✗'}</span>
109-
`;
106+
107+
// Use textContent instead of innerHTML to prevent XSS
108+
const featureNameSpan = document.createElement('span');
109+
featureNameSpan.className = 'feature-name';
110+
featureNameSpan.textContent = this.formatFeatureName(feature);
111+
112+
const featureStatusSpan = document.createElement('span');
113+
featureStatusSpan.className = 'feature-status';
114+
featureStatusSpan.textContent = enabled ? '✓' : '✗';
115+
116+
featureElement.appendChild(featureNameSpan);
117+
featureElement.appendChild(featureStatusSpan);
110118
featuresContainer.appendChild(featureElement);
111119
});
112120
}
@@ -296,10 +304,18 @@ export class DashboardTab {
296304
['zone_1', 'zone_2', 'zone_3', 'zone_4'].forEach(zoneId => {
297305
const zoneElement = document.createElement('div');
298306
zoneElement.className = 'zone-item';
299-
zoneElement.innerHTML = `
300-
<span class="zone-name">${zoneId}</span>
301-
<span class="zone-count">undefined</span>
302-
`;
307+
308+
// Use textContent instead of innerHTML to prevent XSS
309+
const zoneNameSpan = document.createElement('span');
310+
zoneNameSpan.className = 'zone-name';
311+
zoneNameSpan.textContent = zoneId;
312+
313+
const zoneCountSpan = document.createElement('span');
314+
zoneCountSpan.className = 'zone-count';
315+
zoneCountSpan.textContent = 'undefined';
316+
317+
zoneElement.appendChild(zoneNameSpan);
318+
zoneElement.appendChild(zoneCountSpan);
303319
zonesContainer.appendChild(zoneElement);
304320
});
305321
return;
@@ -309,10 +325,18 @@ export class DashboardTab {
309325
const zoneElement = document.createElement('div');
310326
zoneElement.className = 'zone-item';
311327
const count = typeof data === 'object' ? (data.person_count || data.count || 0) : data;
312-
zoneElement.innerHTML = `
313-
<span class="zone-name">${zoneId}</span>
314-
<span class="zone-count">${count}</span>
315-
`;
328+
329+
// Use textContent instead of innerHTML to prevent XSS
330+
const zoneNameSpan = document.createElement('span');
331+
zoneNameSpan.className = 'zone-name';
332+
zoneNameSpan.textContent = zoneId;
333+
334+
const zoneCountSpan = document.createElement('span');
335+
zoneCountSpan.className = 'zone-count';
336+
zoneCountSpan.textContent = String(count);
337+
338+
zoneElement.appendChild(zoneNameSpan);
339+
zoneElement.appendChild(zoneCountSpan);
316340
zonesContainer.appendChild(zoneElement);
317341
});
318342
}

ui/components/HardwareTab.js

Lines changed: 23 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -107,20 +107,29 @@ export class HardwareTab {
107107
const txActive = activeAntennas.filter(a => a.classList.contains('tx')).length;
108108
const rxActive = activeAntennas.filter(a => a.classList.contains('rx')).length;
109109

110-
arrayStatus.innerHTML = `
111-
<div class="array-info">
112-
<span class="info-label">Active TX:</span>
113-
<span class="info-value">${txActive}/3</span>
114-
</div>
115-
<div class="array-info">
116-
<span class="info-label">Active RX:</span>
117-
<span class="info-value">${rxActive}/6</span>
118-
</div>
119-
<div class="array-info">
120-
<span class="info-label">Signal Quality:</span>
121-
<span class="info-value">${this.calculateSignalQuality(txActive, rxActive)}%</span>
122-
</div>
123-
`;
110+
// Clear and rebuild using safe DOM methods to prevent XSS
111+
arrayStatus.innerHTML = '';
112+
113+
const createInfoDiv = (label, value) => {
114+
const div = document.createElement('div');
115+
div.className = 'array-info';
116+
117+
const labelSpan = document.createElement('span');
118+
labelSpan.className = 'info-label';
119+
labelSpan.textContent = label;
120+
121+
const valueSpan = document.createElement('span');
122+
valueSpan.className = 'info-value';
123+
valueSpan.textContent = value;
124+
125+
div.appendChild(labelSpan);
126+
div.appendChild(valueSpan);
127+
return div;
128+
};
129+
130+
arrayStatus.appendChild(createInfoDiv('Active TX:', `${txActive}/3`));
131+
arrayStatus.appendChild(createInfoDiv('Active RX:', `${rxActive}/6`));
132+
arrayStatus.appendChild(createInfoDiv('Signal Quality:', `${this.calculateSignalQuality(txActive, rxActive)}%`));
124133
}
125134

126135
// Calculate signal quality based on active antennas

ui/components/PoseDetectionCanvas.js

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -539,14 +539,23 @@ export class PoseDetectionCanvas {
539539
const persons = this.state.lastPoseData?.persons?.length || 0;
540540
const zones = Object.keys(this.state.lastPoseData?.zone_summary || {}).length;
541541

542-
statsEl.innerHTML = `
543-
Connection: ${this.state.connectionState}<br>
544-
Frames: ${this.state.frameCount}<br>
545-
FPS: ${fps.toFixed(1)}<br>
546-
Persons: ${persons}<br>
547-
Zones: ${zones}<br>
548-
Uptime: ${uptime}s
549-
`;
542+
// Use textContent instead of innerHTML to prevent XSS
543+
statsEl.textContent = '';
544+
const lines = [
545+
`Connection: ${this.state.connectionState}`,
546+
`Frames: ${this.state.frameCount}`,
547+
`FPS: ${fps.toFixed(1)}`,
548+
`Persons: ${persons}`,
549+
`Zones: ${zones}`,
550+
`Uptime: ${uptime}s`
551+
];
552+
lines.forEach((line, index) => {
553+
if (index > 0) {
554+
statsEl.appendChild(document.createElement('br'));
555+
}
556+
const textNode = document.createTextNode(line);
557+
statsEl.appendChild(textNode);
558+
});
550559
}
551560

552561
showError(message) {

ui/config/api.config.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,8 +107,12 @@ export function buildApiUrl(endpoint, params = {}) {
107107

108108
// Helper function to build WebSocket URLs
109109
export function buildWsUrl(endpoint, params = {}) {
110-
const protocol = window.location.protocol === 'https:'
111-
? API_CONFIG.WSS_PREFIX
110+
// Use secure WebSocket (wss://) when serving over HTTPS or on non-localhost
111+
// Use ws:// only for localhost development
112+
const isLocalhost = window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1';
113+
const isSecure = window.location.protocol === 'https:';
114+
const protocol = (isSecure || !isLocalhost)
115+
? API_CONFIG.WSS_PREFIX
112116
: API_CONFIG.WS_PREFIX;
113117

114118
// Match Rust sensing server port

v1/src/commands/status.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,8 @@ async def _get_database_status(settings: Settings) -> Dict[str, Any]:
152152

153153
# Get table counts
154154
async with db_manager.get_async_session() as session:
155-
from sqlalchemy import text, func
155+
import sqlalchemy as sa
156+
from sqlalchemy import text, func, select
156157
from src.database.models import Device, Session, CSIData, PoseDetection, SystemMetric, AuditLog
157158

158159
tables = {
@@ -164,10 +165,19 @@ async def _get_database_status(settings: Settings) -> Dict[str, Any]:
164165
"audit_logs": AuditLog,
165166
}
166167

168+
# Whitelist of allowed table names to prevent SQL injection
169+
allowed_table_names = set(tables.keys())
170+
167171
for table_name, model in tables.items():
168172
try:
173+
# Validate table_name against whitelist to prevent SQL injection
174+
if table_name not in allowed_table_names:
175+
db_status["tables"][table_name] = {"error": "Invalid table name"}
176+
continue
177+
178+
# Use SQLAlchemy ORM model for safe query instead of raw SQL
169179
result = await session.execute(
170-
text(f"SELECT COUNT(*) FROM {table_name}")
180+
select(func.count()).select_from(model)
171181
)
172182
count = result.scalar()
173183
db_status["tables"][table_name] = {"count": count}

0 commit comments

Comments
 (0)