Files
Dragan Spiridonov 3030924f22 fix(security): compare candidate and prefix in the same canonical form (#3011)
* fix(security): compare candidate and prefix in the same canonical form (#3010)

`validate()` resolved the candidate with `fs.realpath` but the allowed
prefixes were only `path.resolve()`d, so the two sides were compared in
different forms and nothing under a prefix reached through a symlink could
ever match. On macOS that is every temp directory — `os.tmpdir()` is
`/var/folders/...` and `/var` is a symlink to `/private/var` — so
`ruflo proxy install` rejected the binary it had just signature-verified.
Linux is unaffected because `/tmp` is real, which is why CI stayed green.

Keep both forms of every prefix and compare against the one matching how
the candidate was resolved. Canonicalizing the prefixes outright would have
broken `validateSync()` / `isWithinAllowed()`, which are documented as
lexical-only, in the mirror direction.

When the candidate's leaf does not exist, canonicalize its longest existing
ancestor instead of falling back to the lexical path — otherwise
`allowNonExistent` (the default) would regress under a symlinked prefix.
That also closes a hole this exposed: `<allowed>/escape/not-written-yet`,
where `escape` symlinks outside the prefix, was previously accepted because
`realpath` threw ENOENT on the missing leaf.

Escapes stay blocked, and the regression tests construct the symlink
explicitly rather than relying on the macOS quirk, so they hold on every
platform. 3 of the 6 fail without this change.

Co-Authored-By: RuFlo <ruv@ruv.net>

* fix(security): keep root prefixes usable and leave strict results untouched

Three defects found by an adversarial review of the previous commit.

`allowNonExistent: false` was still canonicalizing the candidate on ENOENT.
The call already fails, so the only effect was rewriting the `resolvedPath`
and `matchedPrefix` reported back — a silent shape change for any caller
reading those off a rejected result. Canonicalize only when non-existent
paths are actually allowed.

A root prefix (`/`, `C:\`, a UNC share root) already ends in a separator, so
anchoring the boundary as `prefix + path.sep` searched for `//` and rejected
every descendant. Pre-existing at all three comparison sites — this change
only widened how a prefix can end up being a root, since one can now
canonicalize to it. Extracted `isWithinPrefix`, which keeps the boundary
anchored so `/srv/app-secrets` is still outside `/srv/app`.

The guarded symlink tests returned early instead of skipping, so they
reported as passed on a host that cannot create links, and the flag was
never reset between cases. They now skip properly and reset per case.

Also documents that prefixes are canonicalized once, at construction: the
allowlist keeps denoting what it named when configured, rather than handing
whoever can rewrite a link the ability to redirect it later.

Co-Authored-By: RuFlo <ruv@ruv.net>

---------

Co-authored-by: RuFlo <ruv@ruv.net>
2026-08-13 17:02:17 -04:00
..
2026-01-04 17:48:23 +00:00

@claude-flow/security

npm version npm downloads License: MIT TypeScript Security Audit

Comprehensive security module for Claude Flow V3 - CVE fixes, input validation, path security, and secure credential management.

Features

  • CVE Remediation - Fixes for CVE-2 (Weak Password Hashing), CVE-3 (Hardcoded Credentials), HIGH-1 (Command Injection), HIGH-2 (Path Traversal)
  • Password Hashing - Secure bcrypt-based password hashing with configurable rounds (12+ recommended)
  • Credential Generation - Cryptographically secure credential and API key generation
  • Safe Command Execution - Allowlist-based command execution preventing injection attacks
  • Path Validation - Protection against path traversal and symlink attacks
  • Input Validation - Zod-based schema validation for all input types
  • Token Generation - Secure token creation with HMAC signing

Installation

npm install @claude-flow/security

Standalone use (without the Ruflo CLI)

This is a plain library — every primitive (PasswordHasher, PathValidator, SafeExecutor, InputValidator, CredentialGenerator) is independently importable. No CLI, no MCP server, no daemon. The primitives close CVE-2 (weak hashing), CVE-3 (default credentials), HIGH-1 (command injection), and HIGH-2 (path traversal) — drop them into any Node app that needs to validate at a system boundary.

Recipe — Validate input, hash a password, generate an API key

// recipe.mjs
import {
  InputValidator,
  EmailSchema,
  PasswordHasher,
  createCredentialGenerator,
} from '@claude-flow/security';

// 1. Validate user input at the boundary (Zod schemas, no surprises)
const email = InputValidator.validate(EmailSchema, 'user@example.com');
//             ↳ throws if invalid; returns the typed value otherwise

// 2. Hash a password with bcrypt at the recommended cost (CVE-2)
//    Default policy: ≥8 chars, ≥1 upper, ≥1 lower, ≥1 digit.
//    Tune with new PasswordHasher({ rounds, requireUppercase, ... }).
const hasher = new PasswordHasher({ rounds: 12 });
const hash   = await hasher.hash('CorrectHorseBatteryStaple9');
const ok     = await hasher.verify('CorrectHorseBatteryStaple9', hash); // true

// 3. Generate a high-entropy API key (CVE-3) — uses crypto.randomBytes
//    under the hood and refuses to emit keys below 32 bytes of entropy.
const creds = createCredentialGenerator();
const apiKey = creds.generateApiKey('ck_live_');
console.log(apiKey.keyId, apiKey.key.slice(0, 16) + '…');
// → 7e2b1a9c-…  ck_live_xX9aQ…

Recipe — Refuse command injection + path traversal

import {
  createDevelopmentExecutor,
  createProjectPathValidator,
} from '@claude-flow/security';

// SafeExecutor — only the allow-listed commands ever run (HIGH-1)
const exec = createDevelopmentExecutor({ projectRoot: process.cwd() });
const { stdout } = await exec.execute('git', ['status', '--porcelain']);

// PathValidator — refuses traversal and symlinks out of the project (HIGH-2)
const paths = createProjectPathValidator(process.cwd());
const result = await paths.validate('../../etc/passwd');
if (!result.isValid) console.log('blocked:', result.errors.join('; '));
// → blocked: Path traversal pattern detected

Quick Start

import { createSecurityModule } from '@claude-flow/security';

// Create a complete security module
const security = createSecurityModule({
  projectRoot: '/workspaces/project',
  hmacSecret: process.env.HMAC_SECRET!,
  bcryptRounds: 12,
  allowedCommands: ['git', 'npm', 'node']
});

// Hash a password
const hash = await security.passwordHasher.hash('userPassword123');

// Validate a path
const pathResult = await security.pathValidator.validate('/workspaces/project/src/file.ts');

// Execute command safely
const output = await security.safeExecutor.execute('git', ['status']);

// Generate secure credentials
const creds = await security.credentialGenerator.generate();

API Reference

Password Hashing (CVE-2 Fix)

import { PasswordHasher, createPasswordHasher } from '@claude-flow/security';

const hasher = createPasswordHasher({ rounds: 12 });

// Hash password
const hash = await hasher.hash('password');

// Verify password
const isValid = await hasher.verify('password', hash);

// Check if hash needs rehashing
const needsRehash = hasher.needsRehash(hash);

Credential Generation (CVE-3 Fix)

import { CredentialGenerator, generateCredentials } from '@claude-flow/security';

const generator = new CredentialGenerator();

// Generate API key
const apiKey = await generator.generateApiKey({
  prefix: 'cf',
  length: 32
});

// Generate complete credentials
const creds = generateCredentials({
  includeApiKey: true,
  includeSecret: true
});

Safe Command Execution (HIGH-1 Fix)

import { SafeExecutor, createDevelopmentExecutor } from '@claude-flow/security';

const executor = createDevelopmentExecutor();

// Execute allowed command
const result = await executor.execute('git', ['status']);

// With timeout
const result2 = await executor.execute('npm', ['install'], {
  timeout: 60000,
  cwd: '/workspaces/project'
});

Path Validation (HIGH-2 Fix)

import { PathValidator, createProjectPathValidator } from '@claude-flow/security';

const validator = createProjectPathValidator('/workspaces/project');

// Validate path
const result = await validator.validate('../../../etc/passwd');
// { valid: false, reason: 'Path traversal detected' }

// Safe path
const result2 = await validator.validate('/workspaces/project/src/index.ts');
// { valid: true, normalized: '/workspaces/project/src/index.ts' }

Input Validation

import {
  InputValidator,
  SafeStringSchema,
  EmailSchema,
  PasswordSchema,
  SpawnAgentSchema
} from '@claude-flow/security';

// Validate email
const email = EmailSchema.parse('user@example.com');

// Validate password
const password = PasswordSchema.parse('SecurePass123!');

// Validate agent spawn request
const agentRequest = SpawnAgentSchema.parse({
  type: 'coder',
  name: 'code-agent-1'
});

// Sanitize HTML
import { sanitizeHtml } from '@claude-flow/security';
const safe = sanitizeHtml('<script>alert("xss")</script>Hello');
// 'Hello'

Token Generation

import { TokenGenerator, quickGenerate } from '@claude-flow/security';

const generator = new TokenGenerator({
  hmacSecret: process.env.HMAC_SECRET!
});

// Generate signed token
const token = await generator.generate({
  type: 'session',
  expiresIn: 3600
});

// Verify token
const verified = await generator.verify(token);

// Quick generation
const sessionToken = quickGenerate.sessionToken();
const verificationCode = quickGenerate.verificationCode();

Security Constants

import {
  MIN_BCRYPT_ROUNDS,      // 12
  MAX_BCRYPT_ROUNDS,      // 14
  MIN_PASSWORD_LENGTH,    // 8
  MAX_PASSWORD_LENGTH,    // 72 (bcrypt limit)
  DEFAULT_TOKEN_EXPIRATION,   // 3600 (1 hour)
  DEFAULT_SESSION_EXPIRATION  // 86400 (24 hours)
} from '@claude-flow/security';

Security Audit

import { auditSecurityConfig } from '@claude-flow/security';

const warnings = auditSecurityConfig({
  bcryptRounds: 10,
  hmacSecret: 'short'
});

// ['bcryptRounds (10) below recommended minimum (12)',
//  'hmacSecret should be at least 32 characters']

Validation Schemas

Schema Description
SafeStringSchema Basic safe string with length limits
IdentifierSchema Alphanumeric identifiers
FilenameSchema Safe filenames
EmailSchema Email addresses
PasswordSchema Secure passwords
UUIDSchema UUID v4 format
HttpsUrlSchema HTTPS URLs only
SemverSchema Semantic versions
PortSchema Valid port numbers
IPv4Schema IPv4 addresses
SpawnAgentSchema Agent spawn requests
TaskInputSchema Task definitions
SecurityConfigSchema Security configuration

Dependencies

  • bcrypt - Password hashing
  • zod - Schema validation

License

MIT