T09 · Insecure Skill Coding Practices
- Location
index.js:4- Finding
Arbitrary Local File Read Through Path Traversal
- Content
View full analysis
Vulnerability Details
File Location:
index.js, lines 4-11
Vulnerability Type: Path traversal leading to unauthorized local file disclosure
Risk Level: HighVulnerable Code
js const filename = params.filename || "example.txt"; const path = `C:\\Users\\user\\.openclaw\\workspace\\${filename}`; if (!fs.existsSync(path)) { return `File ${filename} does not exist in workspace.`; } const content = fs.readFileSync(path, "utf-8"); const summary = content.slice(0, 500) + (content.length > 500 ? "..." : "");Technical Analysis
The attacker-controlled
params.filenamevalue is directly concatenated with the intended workspace directory. The implementation does not reject absolute paths, normalize the resulting path, resolve canonical paths, or verify that the resolved target remains inside the workspace.On Windows, a filename containing parent-directory components such as
..\can cause the resulting path to resolve outsideC:\Users\user\.openclaw\workspace. Thefs.existsSync()check only determines whether the constructed path exists; it does not provide any security boundary. If the path exists,fs.readFileSync()reads it with the privileges of the running process.This behavior contradicts the statement in
SKILL.mdthat file paths are validated against directory traversal.Attack Path
- An attacker causes the skill to run with a crafted
filenameparameter, such as..\..\..\sensitive.txt. - The skill appends that value to the configured workspace path.
- Windows resolves the embedded parent-directory components, moving the effective target outside the workspace.
fs.existsSync()confirms that the external target exists.fs.readFileSync()reads the target as UTF-8.- The skill returns the first 500 characters of the file, disclosing its contents to the requester.
Exploitation requires knowledge or discovery of a readable target path. The target mu ...[truncated 682 chars]
- An attacker causes the skill to run with a crafted
- Remediation
View remediation
Remediation Suggestions
- Define and canonicalize the permitted workspace root using Node.js
path.resolve(). - Require
params.filenameto be a non-empty string and reject null bytes and absolute paths. - Resolve the requested path relative to the workspace root.
- Use
path.relative()to verify that the resolved target remains within the workspace. Reject paths whose relative form is.., begins with..plus a path separator, or is absolute. - Where symlinks or junctions may exist, compare
fs.realpath()results for the workspace and target to prevent traversal through filesystem links. - Prefer the platform's declared
workspace.readcapability instead of direct unrestricted filesystem access. - Avoid exposing attacker-controlled filenames in error messages unless safely encoded for the output context.
- Add tests for Windows and POSIX traversal forms, absolute paths, mixed separators, drive-qualified paths, UNC paths, symlinks, and junctions.
Example containment approach:
js import fs from "fs"; import path from "path"; const workspaceRoot = path.resolve("C:\\Users\\user\\.openclaw\\workspace"); export async function run(params) { if (typeof params?.filename !== "string" || !params.filename) { throw new Error("A valid filename is required."); } if (path.isAbsolute(params.filename) || params.filename.includes("\0")) { throw new Error("Invalid filename."); } const requestedPath = path.resolve(workspaceRoot, params.filename); const relativePath = path.relative(workspaceRoot, requestedPath); if ( relativePath === ".." || relativePath.startsWith(`..${path.sep}`) || path.isAbsolute(relativePath) ) { throw new Error("Requested file is outside the workspace."); } const realRoot = fs.realpathSync(workspaceRoot); const realTarget = fs.realpathSync(requestedPath); const realRelative = path.relative(realRoot, realTarget); if ( ...[truncated 336 chars]- Define and canonicalize the permitted workspace root using Node.js
