fix(sandbox): handle existing directories in mkdirp boundary checks#31547
fix(sandbox): handle existing directories in mkdirp boundary checks#31547steipete merged 3 commits intoopenclaw:mainfrom
Conversation
Greptile SummaryFixed How it works:
Key changes:
The fix correctly addresses the issue where Confidence Score: 4/5
Last reviewed commit: 0e455d9 |
src/agents/sandbox/fs-bridge.ts
Outdated
| try { | ||
| const st = fs.statSync(target.hostPath); | ||
| if (!st.isDirectory()) { | ||
| throw new Error( | ||
| `Sandbox boundary checks failed; cannot ${options.action}: ${target.containerPath}`, | ||
| ); | ||
| } | ||
| } catch { | ||
| throw guarded.error instanceof Error | ||
| ? guarded.error | ||
| : new Error( | ||
| `Sandbox boundary checks failed; cannot ${options.action}: ${target.containerPath}`, | ||
| ); | ||
| } |
There was a problem hiding this comment.
confusing error handling - the thrown error on line 277 is immediately caught and replaced
The current implementation throws an error when !st.isDirectory() but that error is immediately caught by the catch block and replaced with guarded.error. Consider simplifying:
| try { | |
| const st = fs.statSync(target.hostPath); | |
| if (!st.isDirectory()) { | |
| throw new Error( | |
| `Sandbox boundary checks failed; cannot ${options.action}: ${target.containerPath}`, | |
| ); | |
| } | |
| } catch { | |
| throw guarded.error instanceof Error | |
| ? guarded.error | |
| : new Error( | |
| `Sandbox boundary checks failed; cannot ${options.action}: ${target.containerPath}`, | |
| ); | |
| } | |
| let isDirectory = false; | |
| try { | |
| const st = fs.statSync(target.hostPath); | |
| isDirectory = st.isDirectory(); | |
| } catch { | |
| // statSync failed, treat as not a directory | |
| } | |
| if (!isDirectory) { | |
| throw guarded.error instanceof Error | |
| ? guarded.error | |
| : new Error( | |
| `Sandbox boundary checks failed; cannot ${options.action}: ${target.containerPath}`, | |
| ); | |
| } |
This makes the intent clearer: check if the path is an existing directory, and if not, throw the original boundary check error.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/agents/sandbox/fs-bridge.ts
Line: 274-287
Comment:
confusing error handling - the thrown error on line 277 is immediately caught and replaced
The current implementation throws an error when `!st.isDirectory()` but that error is immediately caught by the catch block and replaced with `guarded.error`. Consider simplifying:
```suggestion
let isDirectory = false;
try {
const st = fs.statSync(target.hostPath);
isDirectory = st.isDirectory();
} catch {
// statSync failed, treat as not a directory
}
if (!isDirectory) {
throw guarded.error instanceof Error
? guarded.error
: new Error(
`Sandbox boundary checks failed; cannot ${options.action}: ${target.containerPath}`,
);
}
```
This makes the intent clearer: check if the path is an existing directory, and if not, throw the original boundary check error.
How can I resolve this? If you propose a fix, please make it concise.0e455d9 to
061121f
Compare
🔒 Aisle Security AnalysisWe found 1 potential security issue(s) in this PR:
1. 🔵 Sandbox boundary validation bypass via directory stat() fallback in assertPathSafety
Description
A new fallback treats certain Problem:
As a result, an attacker can potentially bypass boundary validation for directory operations (currently Vulnerable code (introduced in this diff): if (!guarded.ok) {
if (guarded.reason !== "path") {
const canFallbackToDirectoryStat =
options.allowedType === "directory" && this.pathIsExistingDirectory(target.hostPath);
if (!canFallbackToDirectoryStat) {
throw guarded.error instanceof Error
? guarded.error
: new Error(
`Sandbox boundary checks failed; cannot ${options.action}: ${target.containerPath}`,
);
}
}
}Security impact:
This broad fallback should be limited to the specific platform-IO case it was meant to address; otherwise it undermines boundary enforcement for directories. RecommendationLimit the fallback to the intended IO-only directory-open incompatibility case, and (optionally) to Suggested change: if (!guarded.ok) {
if (guarded.reason === "io" && options.action === "create directories" && options.allowedType === "directory") {
if (!this.pathIsExistingDirectory(target.hostPath)) {
throw guarded.error instanceof Error ? guarded.error : new Error(...);
}
} else if (guarded.reason !== "path") {
throw guarded.error instanceof Error ? guarded.error : new Error(...);
}
}This preserves support for platforms that cannot Analyzed PR: #31547 at commit Last updated on: 2026-03-02T17:10:30Z |
|
Landed via temp rebase onto main.
Thanks @stakeswky! |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 061121f2b1
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const canFallbackToDirectoryStat = | ||
| options.allowedType === "directory" && this.pathIsExistingDirectory(target.hostPath); |
There was a problem hiding this comment.
Restrict mkdirp fallback to I/O-only boundary failures
The new fallback now runs for every non-path failure, but openBoundaryFile also reports symlink/hardlink boundary violations as reason: "validation"; with options.allowedType === "directory", pathIsExistingDirectory uses statSync (follows symlinks) and can turn those validation failures into success. This means mkdirp can bypass host-side alias boundary rejection and proceed to Docker execution for paths that were explicitly rejected by boundary validation, which weakens the sandbox safety model for pre-existing directory aliases. The fallback should be limited to the platform I/O case (reason === "io") that this fix is targeting.
Useful? React with 👍 / 👎.
* main: (174 commits) refactor(gateway): unify control-ui and plugin webhook routing fix(exec): resolve PATH key case-insensitively for Windows pathPrepend (openclaw#25399) (openclaw#31879) fix(tsgo): unblock baseline type errors (openclaw#31873) fix(security): harden sms.send dangerous-node defaults fix(gateway): let POST requests pass through root-mounted Control UI to plugin handlers fix(browser): fail closed navigation guard with env proxy test(perf): reduce timer teardown overhead in cron issue regressions refactor: split browser context/actions and unify CDP timeout policy test(perf): cache redact hints and tune guardrail scan concurrency docs(changelog): credit sessions_spawn agentId validation fix (openclaw#31381) fix(agents): validate sessions_spawn agentId format (openclaw#31381) fix(agents): add strict format validation to sessions_spawn for agentId fix(logging): log timestamps use local time instead of UTC (openclaw#28434) test(perf): remove redundant module reset in system presence version tests test(perf): avoid module reload churn in config guard tests fix(gateway): fail closed plugin auth path canonicalization docs(changelog): credit sandbox mkdirp boundary fix (openclaw#31547) fix(sandbox): allow mkdirp boundary checks on existing directories (openclaw#31547) fix(sandbox): allow mkdirp boundary check on existing directories fix: preserve dns pinning for strict web SSRF fetches ...
Summary
mkdirpboundary validation to handle pre-existing directory targetsRoot cause
mkdirpusesopenBoundaryFilefor boundary checks.openBoundaryFileis optimized for open-verified paths and can return non-path validation failures for directory targets in some mounted workspace flows. The bridge treated these as hard failures, somkdir -pfailed even when the target directory already existed and was in-bounds.Fix
When
allowedType === "directory"and boundary open returns a non-path failure, fallback to a host-sidestatSynccheck for an existing directory. If it is a directory, continue; otherwise preserve the original failure behavior.Testing
pnpm vitest run src/agents/sandbox/fs-bridge.test.ts -t "allows mkdirp for existing in-boundary subdirectories|rejects mkdirp when target exists as a file|rejects pre-existing host symlink escapes before docker exec"Closes #31438