惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Apple Machine Learning Research
Apple Machine Learning Research
J
Java Code Geeks
博客园 - 聂微东
Microsoft Azure Blog
Microsoft Azure Blog
量子位
T
Tailwind CSS Blog
Vercel News
Vercel News
I
InfoQ
Stack Overflow Blog
Stack Overflow Blog
U
Unit 42
Engineering at Meta
Engineering at Meta
L
LangChain Blog
大猫的无限游戏
大猫的无限游戏
D
Docker
博客园_首页
P
Proofpoint News Feed
月光博客
月光博客
T
The Blog of Author Tim Ferriss
MyScale Blog
MyScale Blog
酷 壳 – CoolShell
酷 壳 – CoolShell
Martin Fowler
Martin Fowler
腾讯CDC
N
Netflix TechBlog - Medium
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More

Recent Commits to openclaw:main

test: merge chat side-result checks · openclaw/openclaw@ddd2c2a test: merge cron history checks · openclaw/openclaw@f7eb746 test: merge responsive navigation shell checks · openclaw/openclaw@c2e4b47 docs(changelog): add codex oauth fixes · openclaw/openclaw@628e6cd test: merge navigation routing cases · openclaw/openclaw@5d8cecb Tests: mock channel registry bundled fallback · openclaw/openclaw@2b08233 Secrets: avoid broad web search discovery for single plugin config · openclaw/openclaw@a464f59 test: merge config view browser checks · openclaw/openclaw@20cf511 fix(status): align oauth health with runtime · openclaw/openclaw@eed7116 feat: add macOS screen snapshots for monitor preview (#67954) thanks … · openclaw/openclaw@f377db1 fix: report shared auth scopes in hello-ok (#67810) thanks @BunsDev · openclaw/openclaw@0b6c39b Auto-reply: avoid eager bundled route fallback · openclaw/openclaw@3ea1bf4 Tests: narrow session binding contract setup · openclaw/openclaw@54e4e16 fix(macOS): enable undo/redo in webchat composer text input (#34962) · openclaw/openclaw@00951dc Tests: speed up channel setup promotion · openclaw/openclaw@82b529a Docs: refresh agent instructions · openclaw/openclaw@5775fe2 fix(auth): serialize OAuth refresh across agents to fix #26322 (#67876) · openclaw/openclaw@8e79080 test: allow ollama public surface boundary test · openclaw/openclaw@7d4f1a6 Docs: add test performance guardrails · openclaw/openclaw@89706d3 Tests: restore context-engine usage proof · openclaw/openclaw@e4c4f95 Tests: slim context engine runtime coverage · openclaw/openclaw@74c198f ci: retry failed custom checkouts · openclaw/openclaw@0ee5baf test: trim duplicate provider auth onboarding cases · openclaw/openclaw@1ffc02e matrix: fix sessions_spawn --thread subagent session spawning (#67643) · openclaw/openclaw@1ce2596 test: reduce auth choice fixture churn · openclaw/openclaw@857b9cd test: mock health status config boundaries · openclaw/openclaw@9d5ab4a test: mock onboard config io boundary · openclaw/openclaw@299694d test: mock legacy state plugin boundaries · openclaw/openclaw@2713089 test: mock channel install boundaries · openclaw/openclaw@b945248 test: mock doctor preview channel boundaries · openclaw/openclaw@b1a3ad4
test: speed up security audit tests · openclaw/openclaw@1...
steipete · 2026-04-26 · via Recent Commits to openclaw:main

@@ -39,48 +39,6 @@ type ExecDockerRawFn = (

3939

) => Promise<import("../agents/sandbox/docker.js").ExecDockerRawResult>;

40404141

type CodeSafetySummaryCache = Map<string, Promise<unknown>>;

42-

type WorkspaceSkillScanLimits = {

43-

maxFiles?: number;

44-

maxDirVisits?: number;

45-

};

46-

const MAX_WORKSPACE_SKILL_SCAN_FILES_PER_WORKSPACE = 2_000;

47-

const MAX_WORKSPACE_SKILL_ESCAPE_DETAIL_ROWS = 12;

48-49-

/**

50-

* Resolves the realpath of `p` with a 2 s timeout.

51-

*

52-

* Returns the realpath string on success, or `null` if realpath fails or the

53-

* timeout fires first. Note: fs.realpath cannot be cancelled once submitted to

54-

* libuv — the underlying OS call continues running in the background after the

55-

* timeout resolves. Callers make sequential (not concurrent) calls so at most

56-

* one libuv thread is occupied at a time; the OS will eventually time out the

57-

* stuck NFS/SMB call independently.

58-

*

59-

* Timer cleanup: when realpath resolves before the deadline the timer is

60-

* cleared immediately so it does not linger across the rest of the audit run.

61-

* The timer is also unref'd so it cannot prevent process exit even if it fires

62-

* late (e.g. the process finishes while a hang is still in-flight).

63-

*/

64-

function realpathWithTimeout(p: string, timeoutMs = 2000): Promise<string | null> {

65-

let timerHandle: ReturnType<typeof setTimeout> | undefined;

66-67-

const realpathPromise = fs

68-

.realpath(p)

69-

.catch(() => null)

70-

.then((result) => {

71-

clearTimeout(timerHandle);

72-

return result;

73-

});

74-75-

const timeoutPromise = new Promise<null>((resolve) => {

76-

timerHandle = setTimeout(() => resolve(null), timeoutMs);

77-

// Prevent the timer from keeping the process alive while waiting on a

78-

// potentially hanging NFS/SMB path during a large audit run.

79-

timerHandle.unref?.();

80-

});

81-82-

return Promise.race([realpathPromise, timeoutPromise]);

83-

}

8442

let skillsModulePromise: Promise<typeof import("../agents/skills.js")> | undefined;

8543

let configModulePromise: Promise<typeof import("../config/config.js")> | undefined;

8644

let agentScopeModulePromise: Promise<typeof import("../agents/agent-scope.js")> | undefined;

@@ -302,66 +260,6 @@ async function getCodeSafetySummary(params: {

302260

});

303261

}

304262305-

async function listWorkspaceSkillMarkdownFiles(

306-

workspaceDir: string,

307-

limits: WorkspaceSkillScanLimits = {},

308-

): Promise<{ skillFilePaths: string[]; truncated: boolean }> {

309-

const skillsRoot = path.join(workspaceDir, "skills");

310-

const rootStat = await safeStat(skillsRoot);

311-

if (!rootStat.ok || !rootStat.isDir) {

312-

return { skillFilePaths: [], truncated: false };

313-

}

314-315-

const maxFiles = limits.maxFiles ?? MAX_WORKSPACE_SKILL_SCAN_FILES_PER_WORKSPACE;

316-

const maxTotalDirVisits = limits.maxDirVisits ?? maxFiles * 20;

317-

const skillFiles: string[] = [];

318-

const queue: string[] = [skillsRoot];

319-

const visitedDirs = new Set<string>();

320-

let totalDirVisits = 0;

321-322-

while (queue.length > 0 && skillFiles.length < maxFiles && totalDirVisits++ < maxTotalDirVisits) {

323-

const dir = queue.shift()!;

324-

// Use the module-level realpathWithTimeout so a hanging network FS doesn't

325-

// block the BFS indefinitely (same 2 s guard as the outer escape-detection loop).

326-

const dirRealPath = (await realpathWithTimeout(dir)) ?? path.resolve(dir);

327-

if (visitedDirs.has(dirRealPath)) {

328-

continue;

329-

}

330-

visitedDirs.add(dirRealPath);

331-332-

const entries = await fs.readdir(dir, { withFileTypes: true }).catch(() => []);

333-

for (const entry of entries) {

334-

if (entry.name.startsWith(".") || entry.name === "node_modules") {

335-

continue;

336-

}

337-

const fullPath = path.join(dir, entry.name);

338-

if (entry.isDirectory()) {

339-

queue.push(fullPath);

340-

continue;

341-

}

342-

if (entry.isSymbolicLink()) {

343-

const stat = await fs.stat(fullPath).catch(() => null);

344-

if (!stat) {

345-

continue;

346-

}

347-

if (stat.isDirectory()) {

348-

queue.push(fullPath);

349-

continue;

350-

}

351-

if (stat.isFile() && entry.name === "SKILL.md") {

352-

skillFiles.push(fullPath);

353-

}

354-

continue;

355-

}

356-

if (entry.isFile() && entry.name === "SKILL.md") {

357-

skillFiles.push(fullPath);

358-

}

359-

}

360-

}

361-362-

return { skillFilePaths: skillFiles, truncated: queue.length > 0 };

363-

}

364-365263

// --------------------------------------------------------------------------

366264

// Exported collectors

367265

// --------------------------------------------------------------------------

@@ -552,110 +450,6 @@ export async function collectSandboxBrowserHashLabelFindings(params?: {

552450

return findings;

553451

}

554452555-

export async function collectWorkspaceSkillSymlinkEscapeFindings(params: {

556-

cfg: OpenClawConfig;

557-

skillScanLimits?: WorkspaceSkillScanLimits;

558-

}): Promise<SecurityAuditFinding[]> {

559-

const findings: SecurityAuditFinding[] = [];

560-

const { listAgentWorkspaceDirs } = await loadAgentWorkspaceDirsModule();

561-

const workspaceDirs = listAgentWorkspaceDirs(params.cfg);

562-

if (workspaceDirs.length === 0) {

563-

return findings;

564-

}

565-566-

const escapedSkillFiles: Array<{

567-

workspaceDir: string;

568-

skillFilePath: string;

569-

skillRealPath: string;

570-

}> = [];

571-

const seenSkillPaths = new Set<string>();

572-573-

for (const workspaceDir of workspaceDirs) {

574-

const workspacePath = path.resolve(workspaceDir);

575-

const workspaceRealPath = (await realpathWithTimeout(workspacePath)) ?? workspacePath;

576-

const { skillFilePaths, truncated } = await listWorkspaceSkillMarkdownFiles(

577-

workspacePath,

578-

params.skillScanLimits,

579-

);

580-581-

if (truncated) {

582-

// The BFS visit cap was hit before the full skills/ tree was scanned.

583-

// Escaped SKILL.md symlinks in the unvisited portion will not be detected.

584-

// Surface this as a warning so the user knows coverage was incomplete.

585-

findings.push({

586-

checkId: "skills.workspace.scan_truncated",

587-

severity: "warn",

588-

title: "Workspace skill scan reached the directory visit limit",

589-

detail:

590-

`The skills/ directory scan in ${workspacePath} stopped early after reaching the ` +

591-

`BFS visit cap. Skill files in the unscanned portion of the tree were not checked ` +

592-

"for symlink escapes.",

593-

remediation:

594-

"Flatten or simplify the skills/ directory hierarchy to stay within the scan budget, " +

595-

"or move deeply-nested skill collections to a managed skill location.",

596-

});

597-

}

598-599-

for (const skillFilePath of skillFilePaths) {

600-

const canonicalSkillPath = path.resolve(skillFilePath);

601-

if (seenSkillPaths.has(canonicalSkillPath)) {

602-

continue;

603-

}

604-

seenSkillPaths.add(canonicalSkillPath);

605-606-

const skillRealPath = await realpathWithTimeout(canonicalSkillPath);

607-

if (!skillRealPath) {

608-

// realpath timed out or failed — cannot verify the symlink target.

609-

// Treat as a potential escape rather than silently bypassing the check.

610-

// An attacker on a slow/network FS could otherwise hang realpath to

611-

// prevent escape detection.

612-

escapedSkillFiles.push({

613-

workspaceDir: workspacePath,

614-

skillFilePath: canonicalSkillPath,

615-

skillRealPath: "(realpath timed out \u2014 symlink target unverifiable)",

616-

});

617-

continue;

618-

}

619-

if (isPathInside(workspaceRealPath, skillRealPath)) {

620-

continue;

621-

}

622-

escapedSkillFiles.push({

623-

workspaceDir: workspacePath,

624-

skillFilePath: canonicalSkillPath,

625-

skillRealPath,

626-

});

627-

}

628-

}

629-630-

if (escapedSkillFiles.length === 0) {

631-

return findings;

632-

}

633-634-

findings.push({

635-

checkId: "skills.workspace.symlink_escape",

636-

severity: "warn",

637-

title: "Workspace skill files resolve outside the workspace root",

638-

detail:

639-

"Detected workspace `skills/**/SKILL.md` paths whose realpath escapes their workspace root:\n" +

640-

escapedSkillFiles

641-

.slice(0, MAX_WORKSPACE_SKILL_ESCAPE_DETAIL_ROWS)

642-

.map(

643-

(entry) =>

644-

`- workspace=${entry.workspaceDir}\n` +

645-

` skill=${entry.skillFilePath}\n` +

646-

` realpath=${entry.skillRealPath}`,

647-

)

648-

.join("\n") +

649-

(escapedSkillFiles.length > MAX_WORKSPACE_SKILL_ESCAPE_DETAIL_ROWS

650-

? `\n- +${escapedSkillFiles.length - MAX_WORKSPACE_SKILL_ESCAPE_DETAIL_ROWS} more`

651-

: ""),

652-

remediation:

653-

"Keep workspace skills inside the workspace root (replace symlinked escapes with real in-workspace files), or move trusted shared skills to managed/bundled skill locations.",

654-

});

655-656-

return findings;

657-

}

658-659453

export async function collectIncludeFilePermFindings(params: {

660454

configSnapshot: ConfigFileSnapshot;

661455

env?: NodeJS.ProcessEnv;