claude-flow 3.6.24 → 3.6.25
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -2
- package/package.json +1 -1
- package/v3/@claude-flow/cli/README.md +8 -2
- package/v3/@claude-flow/cli/bin/cli.js +21 -0
- package/v3/@claude-flow/cli/bin/mcp-server.js +16 -0
- package/v3/@claude-flow/cli/dist/src/commands/appliance.js +8 -10
- package/v3/@claude-flow/cli/dist/src/commands/guidance.js +1 -5
- package/v3/@claude-flow/cli/dist/src/commands/performance.js +3 -3
- package/v3/@claude-flow/cli/dist/src/commands/process.js +6 -7
- package/v3/@claude-flow/cli/dist/src/commands/verify.js +24 -3
- package/v3/@claude-flow/cli/dist/src/encryption/vault.d.ts +94 -0
- package/v3/@claude-flow/cli/dist/src/encryption/vault.js +172 -0
- package/v3/@claude-flow/cli/dist/src/fs-secure.d.ts +67 -0
- package/v3/@claude-flow/cli/dist/src/fs-secure.js +74 -0
- package/v3/@claude-flow/cli/dist/src/mcp-tools/github-tools.js +122 -31
- package/v3/@claude-flow/cli/dist/src/mcp-tools/hooks-tools.js +2 -2
- package/v3/@claude-flow/cli/dist/src/mcp-tools/memory-tools.js +7 -12
- package/v3/@claude-flow/cli/dist/src/mcp-tools/session-tools.js +24 -12
- package/v3/@claude-flow/cli/dist/src/mcp-tools/terminal-tools.js +22 -7
- package/v3/@claude-flow/cli/dist/src/mcp-tools/validate-input.d.ts +12 -0
- package/v3/@claude-flow/cli/dist/src/mcp-tools/validate-input.js +56 -0
- package/v3/@claude-flow/cli/dist/src/memory/memory-initializer.js +17 -16
- package/v3/@claude-flow/cli/dist/src/transfer/ipfs/upload.js +2 -0
- package/v3/@claude-flow/cli/dist/src/update/executor.d.ts +1 -0
- package/v3/@claude-flow/cli/dist/src/update/executor.js +43 -7
- package/v3/@claude-flow/cli/package.json +1 -1
|
@@ -104,6 +104,62 @@ export function validateText(value, label, maxLen = 10_000) {
|
|
|
104
104
|
const sanitized = value.replace(/\0/g, '');
|
|
105
105
|
return { valid: true, sanitized };
|
|
106
106
|
}
|
|
107
|
+
/**
|
|
108
|
+
* Names that let an attacker pivot a child process before any user code runs:
|
|
109
|
+
* shared-library injection on Linux/macOS, Node hooks, and command resolution.
|
|
110
|
+
*
|
|
111
|
+
* audit_1776853149979: terminal_create previously merged caller-supplied env
|
|
112
|
+
* straight into execSync's environment for every subsequent command in the
|
|
113
|
+
* session. Setting LD_PRELOAD or NODE_OPTIONS via that path is functionally
|
|
114
|
+
* equivalent to remote code execution, so the env input needs an allowlist
|
|
115
|
+
* shape and a denylist on these specific names.
|
|
116
|
+
*/
|
|
117
|
+
const DENYLISTED_ENV_NAMES = new Set([
|
|
118
|
+
'LD_PRELOAD',
|
|
119
|
+
'LD_LIBRARY_PATH',
|
|
120
|
+
'LD_AUDIT',
|
|
121
|
+
'DYLD_INSERT_LIBRARIES',
|
|
122
|
+
'DYLD_LIBRARY_PATH',
|
|
123
|
+
'DYLD_FALLBACK_LIBRARY_PATH',
|
|
124
|
+
'DYLD_FORCE_FLAT_NAMESPACE',
|
|
125
|
+
'NODE_OPTIONS',
|
|
126
|
+
'NODE_PATH',
|
|
127
|
+
]);
|
|
128
|
+
const ENV_NAME_RE = /^[A-Za-z_][A-Za-z0-9_]{0,127}$/;
|
|
129
|
+
/**
|
|
130
|
+
* Validate a Record<string,string> of environment variables: enforce POSIX
|
|
131
|
+
* names, reject hijack-prone names (LD_PRELOAD, NODE_OPTIONS, …), forbid null
|
|
132
|
+
* bytes in values, and cap value length so a malicious caller can't bloat the
|
|
133
|
+
* stored session past reasonable bounds.
|
|
134
|
+
*/
|
|
135
|
+
export function validateEnv(value, label = 'env') {
|
|
136
|
+
if (value === undefined || value === null) {
|
|
137
|
+
return { valid: true, sanitized: {} };
|
|
138
|
+
}
|
|
139
|
+
if (typeof value !== 'object' || Array.isArray(value)) {
|
|
140
|
+
return { valid: false, sanitized: {}, error: `${label} must be an object of string→string` };
|
|
141
|
+
}
|
|
142
|
+
const out = {};
|
|
143
|
+
for (const [name, rawVal] of Object.entries(value)) {
|
|
144
|
+
if (!ENV_NAME_RE.test(name)) {
|
|
145
|
+
return { valid: false, sanitized: {}, error: `${label} key "${name}" is not a valid POSIX env name` };
|
|
146
|
+
}
|
|
147
|
+
if (DENYLISTED_ENV_NAMES.has(name)) {
|
|
148
|
+
return { valid: false, sanitized: {}, error: `${label} key "${name}" is denylisted (loader/runtime hijack)` };
|
|
149
|
+
}
|
|
150
|
+
if (typeof rawVal !== 'string') {
|
|
151
|
+
return { valid: false, sanitized: {}, error: `${label}["${name}"] must be a string` };
|
|
152
|
+
}
|
|
153
|
+
if (rawVal.length > 32_768) {
|
|
154
|
+
return { valid: false, sanitized: {}, error: `${label}["${name}"] exceeds 32768 characters` };
|
|
155
|
+
}
|
|
156
|
+
if (rawVal.includes('\0')) {
|
|
157
|
+
return { valid: false, sanitized: {}, error: `${label}["${name}"] contains a null byte` };
|
|
158
|
+
}
|
|
159
|
+
out[name] = rawVal;
|
|
160
|
+
}
|
|
161
|
+
return { valid: true, sanitized: out };
|
|
162
|
+
}
|
|
107
163
|
/**
|
|
108
164
|
* Assert validation or throw with a structured error.
|
|
109
165
|
*/
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import * as fs from 'fs';
|
|
12
12
|
import * as path from 'path';
|
|
13
|
+
import { readFileMaybeEncrypted, writeFileRestricted } from '../fs-secure.js';
|
|
13
14
|
// ADR-053: Lazy import of AgentDB v3 bridge
|
|
14
15
|
let _bridge;
|
|
15
16
|
async function getBridge() {
|
|
@@ -403,7 +404,7 @@ export async function getHNSWIndex(options) {
|
|
|
403
404
|
try {
|
|
404
405
|
const initSqlJs = (await import('sql.js')).default;
|
|
405
406
|
const SQL = await initSqlJs();
|
|
406
|
-
const fileBuffer =
|
|
407
|
+
const fileBuffer = readFileMaybeEncrypted(dbPath, null);
|
|
407
408
|
const sqlDb = new SQL.Database(fileBuffer);
|
|
408
409
|
// Load all entries with embeddings
|
|
409
410
|
const result = sqlDb.exec(`
|
|
@@ -828,7 +829,7 @@ export async function ensureSchemaColumns(dbPath) {
|
|
|
828
829
|
}
|
|
829
830
|
const initSqlJs = (await import('sql.js')).default;
|
|
830
831
|
const SQL = await initSqlJs();
|
|
831
|
-
const fileBuffer =
|
|
832
|
+
const fileBuffer = readFileMaybeEncrypted(dbPath, null);
|
|
832
833
|
const db = new SQL.Database(fileBuffer);
|
|
833
834
|
// Get current columns in memory_entries
|
|
834
835
|
const tableInfo = db.exec("PRAGMA table_info(memory_entries)");
|
|
@@ -865,7 +866,7 @@ export async function ensureSchemaColumns(dbPath) {
|
|
|
865
866
|
if (modified) {
|
|
866
867
|
// Save updated database
|
|
867
868
|
const data = db.export();
|
|
868
|
-
|
|
869
|
+
writeFileRestricted(dbPath, Buffer.from(data), { encrypt: true });
|
|
869
870
|
}
|
|
870
871
|
db.close();
|
|
871
872
|
return { success: true, columnsAdded };
|
|
@@ -1035,7 +1036,7 @@ export async function initializeMemoryDatabase(options) {
|
|
|
1035
1036
|
// Save to file
|
|
1036
1037
|
const data = db.export();
|
|
1037
1038
|
const buffer = Buffer.from(data);
|
|
1038
|
-
|
|
1039
|
+
writeFileRestricted(dbPath, buffer, { encrypt: true });
|
|
1039
1040
|
// Close database
|
|
1040
1041
|
db.close();
|
|
1041
1042
|
// Also create schema file for reference
|
|
@@ -1101,7 +1102,7 @@ export async function initializeMemoryDatabase(options) {
|
|
|
1101
1102
|
sqliteHeader[25] = 0x40;
|
|
1102
1103
|
sqliteHeader[26] = 0x20; // min embedded payload
|
|
1103
1104
|
sqliteHeader[27] = 0x20; // leaf payload
|
|
1104
|
-
|
|
1105
|
+
writeFileRestricted(dbPath, sqliteHeader, { encrypt: true });
|
|
1105
1106
|
// ADR-053: Activate ControllerRegistry even on fallback path
|
|
1106
1107
|
const controllerResult = await activateControllerRegistry(dbPath, verbose);
|
|
1107
1108
|
return {
|
|
@@ -1546,7 +1547,7 @@ export async function verifyMemoryInit(dbPath, options) {
|
|
|
1546
1547
|
const SQL = await initSqlJs();
|
|
1547
1548
|
const fs = await import('fs');
|
|
1548
1549
|
// Load database
|
|
1549
|
-
const fileBuffer =
|
|
1550
|
+
const fileBuffer = readFileMaybeEncrypted(dbPath, null);
|
|
1550
1551
|
const db = new SQL.Database(fileBuffer);
|
|
1551
1552
|
// Test 1: Schema verification
|
|
1552
1553
|
const schemaStart = Date.now();
|
|
@@ -1679,7 +1680,7 @@ export async function verifyMemoryInit(dbPath, options) {
|
|
|
1679
1680
|
db.run(`DELETE FROM memory_entries WHERE id = ?`, [testId]);
|
|
1680
1681
|
// Save changes
|
|
1681
1682
|
const data = db.export();
|
|
1682
|
-
|
|
1683
|
+
writeFileRestricted(dbPath, Buffer.from(data), { encrypt: true });
|
|
1683
1684
|
db.close();
|
|
1684
1685
|
const passed = tests.filter(t => t.passed).length;
|
|
1685
1686
|
const failed = tests.filter(t => !t.passed).length;
|
|
@@ -1740,7 +1741,7 @@ export async function storeEntry(options) {
|
|
|
1740
1741
|
await ensureSchemaColumns(dbPath);
|
|
1741
1742
|
const initSqlJs = (await import('sql.js')).default;
|
|
1742
1743
|
const SQL = await initSqlJs();
|
|
1743
|
-
const fileBuffer =
|
|
1744
|
+
const fileBuffer = readFileMaybeEncrypted(dbPath, null);
|
|
1744
1745
|
const db = new SQL.Database(fileBuffer);
|
|
1745
1746
|
const id = `entry_${Date.now()}_${Math.random().toString(36).substring(7)}`;
|
|
1746
1747
|
const now = Date.now();
|
|
@@ -1782,7 +1783,7 @@ export async function storeEntry(options) {
|
|
|
1782
1783
|
]);
|
|
1783
1784
|
// Save
|
|
1784
1785
|
const data = db.export();
|
|
1785
|
-
|
|
1786
|
+
writeFileRestricted(dbPath, Buffer.from(data), { encrypt: true });
|
|
1786
1787
|
db.close();
|
|
1787
1788
|
// Add to HNSW index for faster future searches
|
|
1788
1789
|
if (embeddingJson) {
|
|
@@ -1843,7 +1844,7 @@ export async function searchEntries(options) {
|
|
|
1843
1844
|
// Rerank candidates with exact cosine similarity from SQLite
|
|
1844
1845
|
const initSqlJs = (await import('sql.js')).default;
|
|
1845
1846
|
const SQL = await initSqlJs();
|
|
1846
|
-
const fileBuffer =
|
|
1847
|
+
const fileBuffer = readFileMaybeEncrypted(dbPath, null);
|
|
1847
1848
|
const db = new SQL.Database(fileBuffer);
|
|
1848
1849
|
const reranked = [];
|
|
1849
1850
|
for (const candidate of rabitqCandidates) {
|
|
@@ -1893,7 +1894,7 @@ export async function searchEntries(options) {
|
|
|
1893
1894
|
// Fall back to brute-force SQLite search
|
|
1894
1895
|
const initSqlJs = (await import('sql.js')).default;
|
|
1895
1896
|
const SQL = await initSqlJs();
|
|
1896
|
-
const fileBuffer =
|
|
1897
|
+
const fileBuffer = readFileMaybeEncrypted(dbPath, null);
|
|
1897
1898
|
const db = new SQL.Database(fileBuffer);
|
|
1898
1899
|
// Get entries with embeddings
|
|
1899
1900
|
const searchStmt = db.prepare(effectiveNamespace !== 'all'
|
|
@@ -2004,7 +2005,7 @@ export async function listEntries(options) {
|
|
|
2004
2005
|
await ensureSchemaColumns(dbPath);
|
|
2005
2006
|
const initSqlJs = (await import('sql.js')).default;
|
|
2006
2007
|
const SQL = await initSqlJs();
|
|
2007
|
-
const fileBuffer =
|
|
2008
|
+
const fileBuffer = readFileMaybeEncrypted(dbPath, null);
|
|
2008
2009
|
const db = new SQL.Database(fileBuffer);
|
|
2009
2010
|
// Get total count
|
|
2010
2011
|
const countStmt = namespace
|
|
@@ -2089,7 +2090,7 @@ export async function getEntry(options) {
|
|
|
2089
2090
|
await ensureSchemaColumns(dbPath);
|
|
2090
2091
|
const initSqlJs = (await import('sql.js')).default;
|
|
2091
2092
|
const SQL = await initSqlJs();
|
|
2092
|
-
const fileBuffer =
|
|
2093
|
+
const fileBuffer = readFileMaybeEncrypted(dbPath, null);
|
|
2093
2094
|
const db = new SQL.Database(fileBuffer);
|
|
2094
2095
|
// Find entry by key
|
|
2095
2096
|
const getStmt = db.prepare(`
|
|
@@ -2120,7 +2121,7 @@ export async function getEntry(options) {
|
|
|
2120
2121
|
`, [String(id)]);
|
|
2121
2122
|
// Save updated database
|
|
2122
2123
|
const data = db.export();
|
|
2123
|
-
|
|
2124
|
+
writeFileRestricted(dbPath, Buffer.from(data), { encrypt: true });
|
|
2124
2125
|
db.close();
|
|
2125
2126
|
let tags = [];
|
|
2126
2127
|
if (tagsJson) {
|
|
@@ -2200,7 +2201,7 @@ export async function deleteEntry(options) {
|
|
|
2200
2201
|
await ensureSchemaColumns(dbPath);
|
|
2201
2202
|
const initSqlJs = (await import('sql.js')).default;
|
|
2202
2203
|
const SQL = await initSqlJs();
|
|
2203
|
-
const fileBuffer =
|
|
2204
|
+
const fileBuffer = readFileMaybeEncrypted(dbPath, null);
|
|
2204
2205
|
const db = new SQL.Database(fileBuffer);
|
|
2205
2206
|
// Check if entry exists first
|
|
2206
2207
|
const checkStmt = db.prepare(`
|
|
@@ -2249,7 +2250,7 @@ export async function deleteEntry(options) {
|
|
|
2249
2250
|
const remainingEntries = countResult[0]?.values?.[0]?.[0] || 0;
|
|
2250
2251
|
// Save updated database
|
|
2251
2252
|
const data = db.export();
|
|
2252
|
-
|
|
2253
|
+
writeFileRestricted(dbPath, Buffer.from(data), { encrypt: true });
|
|
2253
2254
|
db.close();
|
|
2254
2255
|
// Clean up in-memory HNSW index so ghost vectors don't appear in searches.
|
|
2255
2256
|
// Remove the entry from the HNSW entries map and invalidate the index.
|
|
@@ -279,6 +279,8 @@ export async function checkContent(cid, gateway = 'https://w3s.link') {
|
|
|
279
279
|
try {
|
|
280
280
|
const response = await fetch(`${gateway}/ipfs/${cid}`, {
|
|
281
281
|
method: 'HEAD',
|
|
282
|
+
// audit_1776853149979: HEAD probe should never hang; 10s upper bound.
|
|
283
|
+
signal: AbortSignal.timeout(10000),
|
|
282
284
|
});
|
|
283
285
|
if (response.ok) {
|
|
284
286
|
const size = parseInt(response.headers.get('content-length') || '0', 10);
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { UpdateCheckResult } from './checker.js';
|
|
6
6
|
import { ValidationResult } from './validator.js';
|
|
7
|
+
export declare function isSafePackageSpec(pkg: string, version: string): boolean;
|
|
7
8
|
export interface UpdateHistoryEntry {
|
|
8
9
|
timestamp: string;
|
|
9
10
|
package: string;
|
|
@@ -2,11 +2,27 @@
|
|
|
2
2
|
* Update executor - performs actual package updates
|
|
3
3
|
* Includes rollback capability
|
|
4
4
|
*/
|
|
5
|
-
import {
|
|
5
|
+
import { execFileSync } from 'child_process';
|
|
6
6
|
import * as fs from 'fs';
|
|
7
7
|
import * as path from 'path';
|
|
8
8
|
import * as os from 'os';
|
|
9
9
|
import { validateUpdate } from './validator.js';
|
|
10
|
+
/**
|
|
11
|
+
* audit_1776853149979: package name and version come from npm-view output and
|
|
12
|
+
* the update-history.json file (writable by anyone with FS access). Both
|
|
13
|
+
* previously interpolated straight into a shell string for `npm install`.
|
|
14
|
+
* These regexes pre-flight values so a hostile package name can't slip
|
|
15
|
+
* shell metacharacters through, even though execFileSync below already
|
|
16
|
+
* eliminates the shell.
|
|
17
|
+
*/
|
|
18
|
+
// First char of the unscoped name forbids `-` to defang CLI-flag confusion
|
|
19
|
+
// when the spec is passed to npm (npm install -evil@1.0.0 looks flag-shaped).
|
|
20
|
+
const SAFE_PKG_RE = /^(@[a-zA-Z0-9_\-]+\/)?[a-zA-Z0-9_][a-zA-Z0-9_\-.]{0,213}$/;
|
|
21
|
+
// semver / dist-tag / range chars only — no shell metas.
|
|
22
|
+
const SAFE_VERSION_RE = /^[a-zA-Z0-9._\-+~^*xX]{1,64}$/;
|
|
23
|
+
export function isSafePackageSpec(pkg, version) {
|
|
24
|
+
return SAFE_PKG_RE.test(pkg) && SAFE_VERSION_RE.test(version);
|
|
25
|
+
}
|
|
10
26
|
const HISTORY_FILE = path.join(os.homedir(), '.claude-flow', 'update-history.json');
|
|
11
27
|
const MAX_HISTORY_ENTRIES = 100;
|
|
12
28
|
function ensureDir() {
|
|
@@ -58,13 +74,25 @@ export async function executeUpdate(update, installedPackages, dryRun = false) {
|
|
|
58
74
|
validation,
|
|
59
75
|
};
|
|
60
76
|
}
|
|
77
|
+
// audit_1776853149979: validate package + version regex before any exec.
|
|
78
|
+
if (!isSafePackageSpec(update.package, update.latestVersion)) {
|
|
79
|
+
return {
|
|
80
|
+
success: false,
|
|
81
|
+
package: update.package,
|
|
82
|
+
version: update.latestVersion,
|
|
83
|
+
error: `Refusing to install: package or version contains disallowed characters (pkg="${update.package}", version="${update.latestVersion}")`,
|
|
84
|
+
validation,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
61
87
|
try {
|
|
62
|
-
//
|
|
63
|
-
|
|
64
|
-
|
|
88
|
+
// audit_1776853149979: switched to execFileSync('npm', argv) — no shell,
|
|
89
|
+
// so even if validation regressed, metas in update.package would stay
|
|
90
|
+
// literal in the argv slot.
|
|
91
|
+
execFileSync('npm', ['install', `${update.package}@${update.latestVersion}`, '--save-exact'], {
|
|
65
92
|
encoding: 'utf-8',
|
|
66
93
|
stdio: 'pipe',
|
|
67
94
|
timeout: 60000, // 1 minute timeout
|
|
95
|
+
shell: false,
|
|
68
96
|
});
|
|
69
97
|
// Record successful update
|
|
70
98
|
recordUpdate({
|
|
@@ -139,13 +167,21 @@ export async function rollbackUpdate(packageName) {
|
|
|
139
167
|
: 'No rollback available',
|
|
140
168
|
};
|
|
141
169
|
}
|
|
170
|
+
// audit_1776853149979: history entries can be tampered with by anyone who
|
|
171
|
+
// can write update-history.json — gate before exec.
|
|
172
|
+
if (!isSafePackageSpec(lastUpdate.package, lastUpdate.fromVersion)) {
|
|
173
|
+
return {
|
|
174
|
+
success: false,
|
|
175
|
+
message: `Refusing to rollback: package or version contains disallowed characters (pkg="${lastUpdate.package}", version="${lastUpdate.fromVersion}")`,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
142
178
|
try {
|
|
143
|
-
//
|
|
144
|
-
|
|
145
|
-
execSync(installCmd, {
|
|
179
|
+
// execFileSync, no shell.
|
|
180
|
+
execFileSync('npm', ['install', `${lastUpdate.package}@${lastUpdate.fromVersion}`, '--save-exact'], {
|
|
146
181
|
encoding: 'utf-8',
|
|
147
182
|
stdio: 'pipe',
|
|
148
183
|
timeout: 60000,
|
|
184
|
+
shell: false,
|
|
149
185
|
});
|
|
150
186
|
// Record the rollback
|
|
151
187
|
recordUpdate({
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@claude-flow/cli",
|
|
3
|
-
"version": "3.6.
|
|
3
|
+
"version": "3.6.25",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Ruflo CLI - Enterprise AI agent orchestration with 60+ specialized agents, swarm coordination, MCP server, self-learning hooks, and vector memory for Claude Code",
|
|
6
6
|
"main": "dist/src/index.js",
|