const timeoutPromise = new Promise<null>((resolve) => {
timerHandle = setTimeout(() => resolve(null), timeoutMs); // Prevent the timer from keeping the process alive while waiting on a // potentially hanging NFS/SMB path during a large audit run.
timerHandle.unref?.();
});
return Promise.race([realpathPromise, timeoutPromise]);
}
let skillsModulePromise: Promise<typeofimport("../agents/skills.js")> | undefined;
let configModulePromise: Promise<typeofimport("../config/config.js")> | undefined;
function loadSkillsModule() {
skillsModulePromise ??= import("../agents/skills.js"); return skillsModulePromise;
}
function loadConfigModule() {
configModulePromise ??= import("../config/config.js"); return configModulePromise;
}
while (queue.length > 0 && skillFiles.length < maxFiles && totalDirVisits++ < maxTotalDirVisits) { const dir = queue.shift()!; // Use the module-level realpathWithTimeout so a hanging network FS doesn't // block the BFS indefinitely (same 2 s guard as the outer escape-detection loop). const dirRealPath = (await realpathWithTimeout(dir)) ?? path.resolve(dir); if (visitedDirs.has(dirRealPath)) { continue;
}
visitedDirs.add(dirRealPath);
const entries = await fs.readdir(dir, { withFileTypes: true }).catch(() => []); for (const entry of entries) { if (entry.name.startsWith(".") || entry.name === "node_modules") { continue;
} const fullPath = path.join(dir, entry.name); if (entry.isDirectory()) {
queue.push(fullPath); continue;
} if (entry.isSymbolicLink()) { const stat = await fs.stat(fullPath).catch(() => null); if (!stat) { continue;
} if (stat.isDirectory()) {
queue.push(fullPath); continue;
} if (stat.isFile() && entry.name === "SKILL.md") {
skillFiles.push(fullPath);
} continue;
} if (entry.isFile() && entry.name === "SKILL.md") {
skillFiles.push(fullPath);
}
}
}
if (truncated) { // The BFS visit cap was hit before the full skills/ tree was scanned. // Escaped SKILL.md symlinks in the unvisited portion will not be detected. // Surface this as a warning so the user knows coverage was incomplete.
findings.push({
checkId: "skills.workspace.scan_truncated",
severity: "warn",
title: "Workspace skill scan reached the directory visit limit",
detail:
`The skills/ directory scan in ${workspacePath} stopped early after reaching the ` +
`BFS visit cap. Skill files in the unscanned portion of the tree were not checked ` + "for symlink escapes.",
remediation: "Flatten or simplify the skills/ directory hierarchy to stay within the scan budget, " + "or move deeply-nested skill collections to a managed skill location.",
});
}
for (const skillFilePath of skillFilePaths) { const canonicalSkillPath = path.resolve(skillFilePath); if (seenSkillPaths.has(canonicalSkillPath)) { continue;
}
seenSkillPaths.add(canonicalSkillPath);
const skillRealPath = await realpathWithTimeout(canonicalSkillPath); if (!skillRealPath) { // realpath timed out or failed — cannot verify the symlink target. // Treat as a potential escape rather than silently bypassing the check. // An attacker on a slow/network FS could otherwise hang realpath to // prevent escape detection.
escapedSkillFiles.push({
workspaceDir: workspacePath,
skillFilePath: canonicalSkillPath,
skillRealPath: "(realpath timed out \u2014 symlink target unverifiable)",
}); continue;
} if (isPathInside(workspaceRealPath, skillRealPath)) { continue;
}
escapedSkillFiles.push({
workspaceDir: workspacePath,
skillFilePath: canonicalSkillPath,
skillRealPath,
});
}
}
if (escapedSkillFiles.length === 0) { return findings;
}
for (const pluginName of pluginDirs) { const pluginPath = path.join(extensionsDir, pluginName);
let extensionEntries: string[] = []; try {
extensionEntries = await readPluginManifestExtensions(pluginPath);
} catch (manifestErr) { // Malformed package.json — surface a warning so the user investigates. // A plugin could deliberately corrupt its manifest to hide declared // extension entrypoints from the deep code scanner.
findings.push({
checkId: "plugins.code_safety.manifest_parse_error",
severity: "warn",
title: `Plugin "${pluginName}" has a malformed package.json`,
detail:
`Could not parse plugin manifest: ${String(manifestErr)}.\n` + "The extension entrypoint list is unavailable. Deep scan will cover the plugin directory but may miss entries declared via `openclaw.extensions`.",
remediation: "Inspect the plugin package.json for syntax errors. If the plugin is untrusted, remove it from your OpenClaw extensions state directory.",
}); // Continue — getCodeSafetySummary below still scans the plugin directory
} const forcedScanEntries: string[] = []; const escapedEntries: string[] = [];
for (const entry of extensionEntries) { const resolvedEntry = path.resolve(pluginPath, entry); if (!isPathInside(pluginPath, resolvedEntry)) {
escapedEntries.push(entry); continue;
} if (extensionUsesSkippedScannerPath(entry)) {
findings.push({
checkId: "plugins.code_safety.entry_path",
severity: "warn",
title: `Plugin "${pluginName}" entry path is hidden or node_modules`,
detail: `Extension entry "${entry}" points to a hidden or node_modules path. Deep code scan will cover this entry explicitly, but review this path choice carefully.`,
remediation: "Prefer extension entrypoints under normal source paths like dist/ or src/.",
});
}
forcedScanEntries.push(resolvedEntry);
}
if (escapedEntries.length > 0) {
findings.push({
checkId: "plugins.code_safety.entry_escape",
severity: "critical",
title: `Plugin "${pluginName}" has extension entry path traversal`,
detail: `Found extension entries that escape the plugin directory:\n${escapedEntries.map((entry) => ` - ${entry}`).join("\n")}`,
remediation: "Update the plugin manifest so all openclaw.extensions entries stay inside the plugin directory.",
});
}
for (const workspaceDir of workspaceDirs) { const entries = loadWorkspaceSkillEntries(workspaceDir, { config: params.cfg }); for (const entry of entries) { if (resolveSkillSource(entry.skill) === "openclaw-bundled") { continue;
}
const skillDir = path.resolve(entry.skill.baseDir); if (isPathInside(pluginExtensionsDir, skillDir)) { // Plugin code is already covered by plugins.code_safety checks. continue;
} if (scannedSkillDirs.has(skillDir)) { continue;
}
scannedSkillDirs.add(skillDir);
const skillName = entry.skill.name; const summary = await getCodeSafetySummary({
dirPath: skillDir,
summaryCache: params.summaryCache,
}).catch((err) => {
findings.push({
checkId: "skills.code_safety.scan_failed",
severity: "warn",
title: `Skill "${skillName}" code scan failed`,
detail: `Static code scan could not complete for ${skillDir}: ${String(err)}`,
remediation: "Check file permissions and skill layout, then rerun `openclaw security audit --deep`.",
}); returnnull;
}); if (!summary) { continue;
}
if (summary.critical > 0) { const criticalFindings = summary.findings.filter(
(finding) => finding.severity === "critical",
); const details = formatCodeSafetyDetails(criticalFindings, skillDir);
findings.push({
checkId: "skills.code_safety",
severity: "critical",
title: `Skill "${skillName}" contains dangerous code patterns`,
detail: `Found ${summary.critical} critical issue(s) in ${summary.scannedFiles} scanned file(s) under ${skillDir}:\n${details}`,
remediation: `Review the skill source code before use. If untrusted, remove "${skillDir}".`,
});
} elseif (summary.warn > 0) { const warnFindings = summary.findings.filter((finding) => finding.severity === "warn"); const details = formatCodeSafetyDetails(warnFindings, skillDir);
findings.push({
checkId: "skills.code_safety",
severity: "warn",
title: `Skill "${skillName}" contains suspicious code patterns`,
detail: `Found ${summary.warn} warning(s) in ${summary.scannedFiles} scanned file(s) under ${skillDir}:\n${details}`,
remediation: "Review flagged lines to ensure the behavior is intentional and safe.",
});
}
}
}
return findings;
}
Messung V0.5 in Prozent
¤ Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.0.17Bemerkung:
(vorverarbeitet am 2026-06-10)
¤
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.