T05 · Unauthorized Access and Privilege Escalation
Warning
- Location
- index.js:96
- Finding
- Workspace Boundary Bypass Through Symbolic-Link Search Directories<![CDATA[ ## Vulnerability Details **File Location**: `index.js:96-109` **Vulnerability Type**: Improper filesystem boundary validation involving symbolic links **Risk Level**: Medium ### Vulnerable Code ```js const allowedSubdirs = ['memory', 'skills']; // Do not include '.' to prevent scanning root if it has sensitive files for (const subdir of allowedSubdirs) { const searchPath = path.join(workspaceRoot, subdir); if (!fs.existsSync(searchPath)) continue; // Additional safety check: ensure searchPath is actually inside workspaceRoot if (!path.resolve(searchPath).startsWith(workspaceRoot)) continue; const files = fs.readdirSync(searchPath, { withFileTypes: true }); for (const file of files) { if (file.isFile() && file.name.toLowerCase().includes(sanitizedQuery.toLowerCase())) { ``` ### Technical Analysis The containment check uses `path.resolve(searchPath)`, which performs lexical path normalization but does not resolve symbolic links. Because `searchPath` is constructed from `workspaceRoot` and a fixed subdirectory name, its normalized string will appear to be inside the workspace even when `memory` or `skills` is a symbolic link pointing to an external directory. `fs.existsSync()` and `fs.readdirSync()` follow a symbolic link used as the directory being inspected. Consequently, a link such as `~/.openclaw/workspace/memory -> /sensitive/directory` passes the existing check, after which the external directory is enumerated. The code only tests immediate entries with `file.isFile()` and does not read their contents. The direct exposure is therefore limited to matching filenames and generated path metadata, rather than file contents. ### Attack Path 1. An attacker who can modify the workspace creates or replaces an allowed search directory with a symbolic link: ```bash ln -s /sensitive/directory ~/.openclaw/workspace/memory ``` 2. The attacker invokes the Skill with the `local` source and a likely filename fragment: ```bas ...[truncated 901 chars]
- Remediation
- <![CDATA[ ## Remediation Suggestions Resolve canonical filesystem paths before performing directory operations: 1. Resolve `workspaceRoot` with `fs.realpathSync()` after verifying that it exists. 2. Resolve each candidate directory with `fs.realpathSync()` so symbolic links are expanded. 3. Use `path.relative()` rather than a raw string-prefix comparison to verify containment. 4. Explicitly reject an allowed search root when `fs.lstatSync(searchPath).isSymbolicLink()` is true. 5. Open or enumerate the canonical path only after validation. 6. Handle race conditions where a path may be replaced between validation and use. Where practical, operate through trusted directory handles or ensure the workspace is not writable by untrusted users. Example hardening pattern: ```js const canonicalRoot = fs.realpathSync(workspaceRoot); const candidatePath = path.join(canonicalRoot, subdir); if (fs.lstatSync(candidatePath).isSymbolicLink()) { continue; } const canonicalSearchPath = fs.realpathSync(candidatePath); const relative = path.relative(canonicalRoot, canonicalSearchPath); if (relative === '' || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) { continue; } const files = fs.readdirSync(canonicalSearchPath, { withFileTypes: true }); ``` The check should be covered by tests in which both `memory` and `skills` are symbolic links to locations outside the workspace. ]]>
