Skip to content

Fix sandbox path validation rejecting Docker bind mount paths#16509

Closed
Clawborn wants to merge 3 commits intoopenclaw:mainfrom
Clawborn:fix/sandbox-bound-folder-paths
Closed

Fix sandbox path validation rejecting Docker bind mount paths#16509
Clawborn wants to merge 3 commits intoopenclaw:mainfrom
Clawborn:fix/sandbox-bound-folder-paths

Conversation

@Clawborn
Copy link
Copy Markdown
Contributor

@Clawborn Clawborn commented Feb 14, 2026

Summary

Fixes #16379

Sandboxed agents with Docker bind mounts (docker.binds) could not use read, write, or edit tools on files within bound directories. The path validation in resolveSandboxPath only checked whether a file path was within the workspace root, rejecting all bind mount host paths with Path escapes sandbox root.

Root Cause

resolveSandboxPath() validates that resolved file paths are relative to params.root (the workspace directory). Docker bind mount host paths (e.g. /root/.openclaw/workspace-two) are outside the workspace root, so they always fail validation — even though the container has legitimate access via the bind mount.

Changes

  • sandbox-paths.ts: Add optional allowedPaths parameter to resolveSandboxPath and assertSandboxPath. When a path escapes the root, check if it falls within any allowed path before throwing.
  • sandbox/fs-bridge.ts: Parse bind mount host paths from docker.binds config and pass them to resolveSandboxPath.
  • pi-tools.read.ts: Thread allowedPaths through wrapSandboxPathGuard and SandboxToolParams.
  • pi-tools.ts: Extract bind mount host paths from sandbox context and pass to sandboxed tool constructors.
  • sandbox-paths.test.ts: Unit tests covering allowed paths, exact match, rejection of paths outside both root and allowed paths, and path traversal attacks.

Testing

  • 6 new unit tests for resolveSandboxPath with allowedPaths — all passing
  • Path traversal through allowed paths is correctly rejected
  • Backward compatible: existing behavior unchanged when allowedPaths is not provided

Greptile Summary

This PR adds allowedPaths support to the sandbox path validation so that Docker bind mount host paths are not rejected as sandbox escapes. The approach is sound: resolveSandboxPath now checks if an out-of-root path falls within any allowed path (derived from docker.binds), with proper traversal attack protection.

However, two issues need to be addressed before this is safe to merge:

  • Symlink check targets wrong directory: assertSandboxPath always passes params.root (workspace root) as the base for assertNoSymlink, even when the path was matched via an allowed path. This means symlink protection doesn't actually cover bind-mounted directories — symlinks within those directories won't be detected.
  • Container path mapping is incorrect for bind mounts: resolveSandboxFsPath computes containerPath by joining containerWorkdir with the relative path, but for bind mounts the container-side mount point differs from the workspace workdir. All Docker exec operations (read, write, stat, etc.) will target the wrong file inside the container.
  • The bind mount parsing and new tests are well-structured, with good coverage of edge cases including path traversal and Windows drive letters.

Confidence Score: 2/5

  • This PR has two logic bugs — a security gap in symlink checking and incorrect container path mapping — that should be fixed before merging.
  • The path validation logic itself is correct and properly rejects traversal attacks. However, the symlink check uses the wrong base directory for allowed paths (security regression), and the container path computation for bind-mounted files will cause Docker operations to target wrong files. Both issues affect the core functionality this PR is trying to enable.
  • Pay close attention to src/agents/sandbox-paths.ts (symlink check base directory) and src/agents/sandbox/fs-bridge.ts (container path mapping for bind mounts).

Last reviewed commit: f35b909

resolveSandboxPath only checked if file paths were within the workspace
root directory. For sandboxed agents with Docker bind mounts configured
via docker.binds, legitimate paths to bound directories were rejected
with 'Path escapes sandbox root' because the host paths of bind mounts
fell outside the workspace root.

Add allowedPaths parameter to resolveSandboxPath and assertSandboxPath
that accepts additional valid path prefixes. Parse Docker bind mount
host paths from the sandbox config and pass them through the tool
creation chain (pi-tools -> sandbox path guard -> path validation).

Fixes openclaw#16379
Copy link
Copy Markdown
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

5 files reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

Comment on lines +229 to +231
const parts = bind.split(":");
// Format: hostPath:containerPath[:options]
return parts.length >= 2 ? parts[0] : "";
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

parsing Windows bind mount host paths with drive letters (e.g., C:\path:/container:ro) would incorrectly split on the drive colon, extracting C instead of C:\path as the host path

on Windows, Docker bind mounts with drive letters like C:\Users\me:/data:ro would split into ["C", "\Users\me", "/data", "ro"], causing parts[0] to return only C instead of the full host path C:\Users\me

Suggested change
const parts = bind.split(":");
// Format: hostPath:containerPath[:options]
return parts.length >= 2 ? parts[0] : "";
const parts = bind.split(":");
// Format: hostPath:containerPath[:options]
// Handle Windows paths with drive letters (e.g., C:\path)
if (parts.length >= 3 && parts[0].match(/^[A-Za-z]$/)) {
return parts[0] + ":" + parts[1];
}
return parts.length >= 2 ? parts[0] : "";
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/agents/sandbox/fs-bridge.ts
Line: 229:231

Comment:
parsing Windows bind mount host paths with drive letters (e.g., `C:\path:/container:ro`) would incorrectly split on the drive colon, extracting `C` instead of `C:\path` as the host path

on Windows, Docker bind mounts with drive letters like `C:\Users\me:/data:ro` would split into `["C", "\Users\me", "/data", "ro"]`, causing `parts[0]` to return only `C` instead of the full host path `C:\Users\me`

```suggestion
      const parts = bind.split(":");
      // Format: hostPath:containerPath[:options]
      // Handle Windows paths with drive letters (e.g., C:\path)
      if (parts.length >= 3 && parts[0].match(/^[A-Za-z]$/)) {
        return parts[0] + ":" + parts[1];
      }
      return parts.length >= 2 ? parts[0] : "";
```

How can I resolve this? If you propose a fix, please make it concise.

@openclaw-barnacle openclaw-barnacle bot added docker Docker and sandbox tooling agents Agent runtime and tooling size: S labels Feb 14, 2026
@steipete steipete closed this Feb 16, 2026
@steipete steipete reopened this Feb 17, 2026
Copy link
Copy Markdown
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

6 files reviewed, 2 comments

Edit Code Review Agent Settings | Greptile

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Feb 17, 2026

Additional Comments (2)

src/agents/sandbox-paths.ts
Symlink check uses wrong base directory for allowed paths

When resolveSandboxPath matches an allowed path, it returns relative as the path relative to that allowed directory. But assertSandboxPath always passes params.root (the workspace root) as the base for assertNoSymlink:

await assertNoSymlink(resolved.relative, path.resolve(params.root));

For example, if the file is /data/workspace-two/MEMORY.md resolved via allowed path /data/workspace-two, assertNoSymlink("MEMORY.md", "/data/workspace-one") will check /data/workspace-one/MEMORY.md for symlinks — not the actual file being accessed. This means symlink-based sandbox escapes within bind-mounted directories would go undetected.

To fix this, you need to also return the matched allowed-path root from resolveSandboxPath and use it as the base for symlink checking:

const base = resolved.matchedAllowedPath ?? path.resolve(params.root);
await assertNoSymlink(resolved.relative, base);
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/agents/sandbox-paths.ts
Line: 69:78

Comment:
**Symlink check uses wrong base directory for allowed paths**

When `resolveSandboxPath` matches an allowed path, it returns `relative` as the path relative to that *allowed* directory. But `assertSandboxPath` always passes `params.root` (the workspace root) as the base for `assertNoSymlink`:

```typescript
await assertNoSymlink(resolved.relative, path.resolve(params.root));
```

For example, if the file is `/data/workspace-two/MEMORY.md` resolved via allowed path `/data/workspace-two`, `assertNoSymlink("MEMORY.md", "/data/workspace-one")` will check `/data/workspace-one/MEMORY.md` for symlinks — not the actual file being accessed. This means symlink-based sandbox escapes within bind-mounted directories would go undetected.

To fix this, you need to also return the matched allowed-path root from `resolveSandboxPath` and use it as the base for symlink checking:

```
const base = resolved.matchedAllowedPath ?? path.resolve(params.root);
await assertNoSymlink(resolved.relative, base);
```

How can I resolve this? If you propose a fix, please make it concise.

src/agents/sandbox/fs-bridge.ts
Container path is wrong for bind-mounted files

When a file resolves via an allowed (bind mount) path, relative is relative to the host-side bind mount path. The containerPath is then computed as containerWorkdir + relative, but for bind mounts the container-side mount point is different from containerWorkdir.

For example, with bind "/host/data:/container/data:ro" and file /host/data/file.txt:

  • relative = "file.txt"
  • containerPath = "/workspace/file.txt" (wrong — should be "/container/data/file.txt")

All Docker exec operations (cat, stat, mkdir, etc.) will target the wrong path inside the container. You need to map the host bind path to its corresponding container mount point and use that instead of containerWorkdir when the path was matched via an allowed path.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/agents/sandbox/fs-bridge.ts
Line: 270:275

Comment:
**Container path is wrong for bind-mounted files**

When a file resolves via an allowed (bind mount) path, `relative` is relative to the *host-side* bind mount path. The `containerPath` is then computed as `containerWorkdir + relative`, but for bind mounts the container-side mount point is different from `containerWorkdir`.

For example, with bind `"/host/data:/container/data:ro"` and file `/host/data/file.txt`:
- `relative` = `"file.txt"` 
- `containerPath` = `"/workspace/file.txt"` (wrong — should be `"/container/data/file.txt"`)

All Docker exec operations (`cat`, `stat`, `mkdir`, etc.) will target the wrong path inside the container. You need to map the host bind path to its corresponding container mount point and use that instead of `containerWorkdir` when the path was matched via an allowed path.

How can I resolve this? If you propose a fix, please make it concise.

@openclaw-barnacle
Copy link
Copy Markdown

This pull request has been automatically marked as stale due to inactivity.
Please add updates or it will be closed.

@openclaw-barnacle openclaw-barnacle bot added stale Marked as stale due to inactivity and removed stale Marked as stale due to inactivity labels Feb 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agents Agent runtime and tooling docker Docker and sandbox tooling size: S

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Sandbox tool permissions do not check bound folders

3 participants