mirror of
https://github.com/ruvnet/ruflo.git
synced 2026-09-14 14:01:28 +08:00
0e1d26c11e
* 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>
182 lines
5.1 KiB
TypeScript
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');
|
|
});
|
|
});
|
|
});
|