Files
ruvnet__ruflo/v3/@claude-flow/browser/tests/e2e/browser-e2e.test.ts
rUv 0e1d26c11e fix(security): patch CVEs, shell injection, SSRF in ruflo (#2114)
* fix(security): patch CVEs, shell injection, SSRF in ruflo

CWE-78 Shell Injection (9 execSync → execFileSync conversions):
- .claude/helpers/github-safe.js: replace execSync(`gh ${args.join(' ')}`)
  with execFileSync('gh', args) in no-body and non-body code paths; the
  body-with-file path already used a tempfile but still joined args into
  a shell string for the final gh invocation.
- v3/@claude-flow/cli/.claude/helpers/github-safe.js: same fix (init-
  template copy mirrors the dogfood helper).
- v3/@claude-flow/mcp/.claude/helpers/github-safe.js: older variant
  without body-cap; apply execFileSync throughout.
- scripts/audit-supply-chain.mjs: execSync(`npm view "${name}"`) →
  execFileSync('npm', ['view', name, ...]) so a malicious package name
  in the allowlist JSON cannot inject shell commands.
- scripts/track-clones.mjs: execSync(`gh api repos/${repo}/...`) →
  execFileSync('gh', ['api', `repos/${repo}/...`]).
- plugins/ruflo-core/scripts/witness/perf.mjs: six execSync calls in
  benchmark runners (git rev-parse, pnpm pack ×2, npm init, npm pkg
  set, npm install <tarball>, node verify.mjs) converted to
  execFileSync with split argv arrays.
- v3/@claude-flow/browser/tests/e2e/browser-e2e.test.ts: execSync with
  template-literal command string replaced by execFileSync with a
  split-on-whitespace argv array.

CWE-918 SSRF (2 callCloudFunction guards):
- ruflo/src/mcp-bridge/index.js: add assertSafeUrl() before every
  callCloudFunction call — rejects non-HTTPS schemes and private/
  loopback IP ranges (10.x, 172.16-31.x, 192.168.x, 127.x, ::1,
  fc/fd ULA, localhost, *.local).
- ruflo/src/ruvocal/mcp-bridge/index.js: identical guard applied to
  the ruvocal variant of callCloudFunction.

npm audit: 0 vulnerabilities (package.json overrides already cover all
known CVEs; confirmed clean on this audit run).

All 10 github-safe injection smoke tests pass. No new test file failures
introduced vs pre-patch baseline (verified via vitest diff).

Co-Authored-By: claude-flow <ruv@ruv.net>

* fix(supply-chain): use execFileSync instead of undefined execSync in runCveAudit

The import on line 35 only brought in `execFileSync` from `node:child_process`,
but `runCveAudit` called the never-imported `execSync`, causing a ReferenceError
on every audit run and reporting 2 spurious CVE findings (one per audited package).

Replace `execSync('npm audit ...')` with `execFileSync('npm', [...args])` to
match the already-correct pattern used in `runPublisherTrustAudit` (line 303).
This also provides the CWE-78 shell-injection mitigation already documented in
that function's comment.

Full audit now exits 0: 0 CVE, 0 lockfile, 0 allowlist, 0 typosquat findings.

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-05-23 04:52:41 -04:00

182 lines
5.1 KiB
TypeScript

/**
* @claude-flow/browser - E2E Browser Tests
*
* These tests run against a real browser using agent-browser.
* Run with: docker compose --profile e2e up browser-e2e
*/
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { execFileSync } from 'child_process';
const TEST_URL = process.env.TEST_URL || 'http://localhost:3000';
const SESSION = 'e2e-test';
/**
* Execute agent-browser command.
*
* The `command` argument is split on whitespace into discrete tokens so that
* execFileSync can receive an argv array. This prevents shell injection when
* test fixtures contain special characters (CWE-78).
*/
function browser(command: string): string {
try {
// Split command into tokens and pass as array — no shell expansion.
const argv = ['--session', SESSION, '--json', ...command.trim().split(/\s+/)];
const result = execFileSync('agent-browser', argv, {
encoding: 'utf-8',
timeout: 30000,
});
return result;
} catch (error) {
throw new Error(`Browser command failed: ${error instanceof Error ? error.message : error}`);
}
}
/**
* Parse JSON result from agent-browser
*/
function parseResult(output: string): { success: boolean; data?: unknown; error?: string } {
try {
return JSON.parse(output);
} catch {
return { success: true, data: output.trim() };
}
}
describe('Browser E2E Tests', () => {
// Skip if not in E2E environment
const runE2E = process.env.TEST_URL !== undefined;
beforeAll(async () => {
if (!runE2E) {
console.log('Skipping E2E tests - TEST_URL not set');
return;
}
// Open test page
const result = browser(`open ${TEST_URL}`);
const parsed = parseResult(result);
expect(parsed.success).toBe(true);
});
afterAll(async () => {
if (!runE2E) return;
// Close browser
try {
browser('close');
} catch {
// Ignore close errors
}
});
describe('Navigation', () => {
it.skipIf(!runE2E)('should navigate to test page', () => {
const result = browser('get url');
const parsed = parseResult(result);
expect(parsed.success).toBe(true);
expect(parsed.data).toContain(TEST_URL);
});
it.skipIf(!runE2E)('should get page title', () => {
const result = browser('get title');
const parsed = parseResult(result);
expect(parsed.success).toBe(true);
expect(parsed.data).toBe('Browser Test Page');
});
});
describe('Snapshot', () => {
it.skipIf(!runE2E)('should take accessibility snapshot', () => {
const result = browser('snapshot');
const parsed = parseResult(result);
expect(parsed.success).toBe(true);
expect(parsed.data).toBeDefined();
});
it.skipIf(!runE2E)('should get interactive elements only', () => {
const result = browser('snapshot -i');
const parsed = parseResult(result);
expect(parsed.success).toBe(true);
// Should have element refs like @e1, @e2
});
});
describe('Form Interaction', () => {
it.skipIf(!runE2E)('should fill email input', () => {
// First get snapshot to find refs
browser('snapshot -i');
// Fill using CSS selector
const result = browser('fill "#email" "test@example.com"');
const parsed = parseResult(result);
expect(parsed.success).toBe(true);
});
it.skipIf(!runE2E)('should fill password input', () => {
const result = browser('fill "#password" "secretpassword"');
const parsed = parseResult(result);
expect(parsed.success).toBe(true);
});
it.skipIf(!runE2E)('should check checkbox', () => {
const result = browser('check "#agree"');
const parsed = parseResult(result);
expect(parsed.success).toBe(true);
});
it.skipIf(!runE2E)('should submit form', () => {
const result = browser('click "#submit-btn"');
const parsed = parseResult(result);
expect(parsed.success).toBe(true);
});
it.skipIf(!runE2E)('should see result after form submit', () => {
// Wait for result to appear
browser('wait "#result"');
const result = browser('get text "#result-data"');
const parsed = parseResult(result);
expect(parsed.success).toBe(true);
expect(parsed.data).toContain('test@example.com');
});
});
describe('Screenshot', () => {
it.skipIf(!runE2E)('should take screenshot', () => {
const result = browser('screenshot');
const parsed = parseResult(result);
expect(parsed.success).toBe(true);
// Should return base64 image data
});
});
describe('JavaScript Execution', () => {
it.skipIf(!runE2E)('should execute JavaScript', () => {
const result = browser('eval "document.title"');
const parsed = parseResult(result);
expect(parsed.success).toBe(true);
expect(parsed.data).toBe('Browser Test Page');
});
it.skipIf(!runE2E)('should extract data with JavaScript', () => {
const result = browser('eval "document.getElementById(\'email\').value"');
const parsed = parseResult(result);
expect(parsed.success).toBe(true);
expect(parsed.data).toBe('test@example.com');
});
});
});