Merge pull request #153 from patrick-premont/patrick/fix-allowwrite-glob-stripping

fix: strip glob suffixes from allowWrite/denyWrite on Linux
This commit is contained in:
David Dworken
2026-02-25 17:08:25 -08:00
committed by GitHub
3 changed files with 90 additions and 4 deletions

View File

@@ -519,12 +519,30 @@ async function wrapWithSandbox(
// Get configs - use custom if provided, otherwise fall back to main config
// If neither exists, defaults to empty arrays (most restrictive)
// Always include default system write paths (like /dev/null, /tmp/claude)
const userAllowWrite =
customConfig?.filesystem?.allowWrite ?? config?.filesystem.allowWrite ?? []
//
// Strip trailing /** and filter remaining globs on Linux (bwrap needs
// real paths, not globs; macOS subpath matching is also recursive so
// stripping is harmless there).
const stripWriteGlobs = (paths: string[]): string[] =>
paths
.map(p => removeTrailingGlobSuffix(p))
.filter(p => {
if (getPlatform() === 'linux' && containsGlobChars(p)) {
logForDebugging(
`[Sandbox] Skipping glob write pattern on Linux: ${p}`,
)
return false
}
return true
})
const userAllowWrite = stripWriteGlobs(
customConfig?.filesystem?.allowWrite ?? config?.filesystem.allowWrite ?? [],
)
const writeConfig = {
allowOnly: [...getDefaultWritePaths(), ...userAllowWrite],
denyWithinAllow:
denyWithinAllow: stripWriteGlobs(
customConfig?.filesystem?.denyWrite ?? config?.filesystem.denyWrite ?? [],
),
}
const rawDenyRead =
customConfig?.filesystem?.denyRead ?? config?.filesystem.denyRead ?? []

View File

@@ -69,7 +69,8 @@ export function containsGlobChars(pathPattern: string): boolean {
* Used to normalize path patterns since /** just means "directory and everything under it"
*/
export function removeTrailingGlobSuffix(pathPattern: string): string {
return pathPattern.replace(/\/\*\*$/, '')
const stripped = pathPattern.replace(/\/\*\*$/, '')
return stripped || '/'
}
/**

View File

@@ -4,6 +4,9 @@ import type { SandboxRuntimeConfig } from '../../src/sandbox/sandbox-config.js'
import { getPlatform } from '../../src/utils/platform.js'
import { wrapCommandWithSandboxLinux } from '../../src/sandbox/linux-sandbox-utils.js'
import { wrapCommandWithSandboxMacOS } from '../../src/sandbox/macos-sandbox-utils.js'
import { mkdirSync, rmSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
/**
* Create a test configuration with network access
@@ -651,3 +654,67 @@ describe('empty allowedDomains network blocking (CVE fix)', () => {
})
})
})
describe('allowWrite glob suffix handling', () => {
const command = 'echo hello'
it('allowWrite with /** suffix includes path in sandbox command', async () => {
if (skipIfUnsupportedPlatform()) {
return
}
const testDir = join(tmpdir(), `srt-test-glob-allow-${Date.now()}`)
mkdirSync(testDir, { recursive: true })
try {
await SandboxManager.reset()
await SandboxManager.initialize({
network: { allowedDomains: [], deniedDomains: [] },
filesystem: {
denyRead: [],
allowWrite: [`${testDir}/**`],
denyWrite: [],
},
})
const result = await SandboxManager.wrapWithSandbox(command)
expect(result).not.toBe(command)
expect(result).toContain(testDir)
} finally {
await SandboxManager.reset()
rmSync(testDir, { recursive: true, force: true })
}
})
it('denyWrite with /** suffix within allowed parent includes both paths', async () => {
if (skipIfUnsupportedPlatform()) {
return
}
const parentDir = join(tmpdir(), `srt-test-glob-deny-${Date.now()}`)
const childDir = join(parentDir, 'denied')
mkdirSync(childDir, { recursive: true })
try {
await SandboxManager.reset()
await SandboxManager.initialize({
network: { allowedDomains: [], deniedDomains: [] },
filesystem: {
denyRead: [],
allowWrite: [parentDir],
denyWrite: [`${childDir}/**`],
},
})
const result = await SandboxManager.wrapWithSandbox(command)
expect(result).not.toBe(command)
expect(result).toContain(parentDir)
expect(result).toContain(childDir)
} finally {
await SandboxManager.reset()
rmSync(parentDir, { recursive: true, force: true })
}
})
})