mirror of
https://github.com/anthropic-experimental/sandbox-runtime.git
synced 2026-05-07 06:01:25 +08:00
Sandbox hardening: TMPDIR write scope and seccomp arg comparison (#182)
* Harden macOS sandbox to only use the configured TMPDIR for writes * Use 32-bit masked comparison for socket() domain argument in seccomp filter
This commit is contained in:
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@anthropic-ai/sandbox-runtime",
|
||||
"version": "0.0.42",
|
||||
"version": "0.0.43",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@anthropic-ai/sandbox-runtime",
|
||||
"version": "0.0.42",
|
||||
"version": "0.0.43",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@pondwader/socks5-server": "^1.0.10",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@anthropic-ai/sandbox-runtime",
|
||||
"version": "0.0.42",
|
||||
"version": "0.0.43",
|
||||
"description": "Anthropic Sandbox Runtime (ASRT) - A general-purpose tool for wrapping security boundaries around arbitrary processes",
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
|
||||
@@ -99,6 +99,11 @@ build_platform() {
|
||||
echo 'Binary size:'
|
||||
ls -lh /output/seccomp-unix-block
|
||||
|
||||
echo ''
|
||||
echo 'Generating BPF filter...'
|
||||
/output/seccomp-unix-block /output/unix-block.bpf
|
||||
ls -lh /output/unix-block.bpf
|
||||
|
||||
echo ''
|
||||
echo 'Building apply-seccomp (no libseccomp dependency)...'
|
||||
gcc -o /output/apply-seccomp /src/apply-seccomp.c \
|
||||
@@ -124,23 +129,7 @@ build_platform() {
|
||||
return 1
|
||||
}
|
||||
|
||||
# Verify binary exists
|
||||
if [ ! -f "$output_dir/seccomp-unix-block" ]; then
|
||||
echo "✗ Error: Binary not found in $output_dir"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Generate BPF filter using the seccomp-unix-block binary
|
||||
echo "Generating BPF filter..."
|
||||
local bpf_file="$output_dir/unix-block.bpf"
|
||||
|
||||
# Run the generator to create the BPF file
|
||||
if ! "$output_dir/seccomp-unix-block" "$bpf_file" 2>&1; then
|
||||
echo "✗ Error: Failed to generate BPF filter"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Verify BPF file was created
|
||||
# Verify BPF file was created inside the container
|
||||
if [ ! -f "$bpf_file" ]; then
|
||||
echo "✗ Error: BPF file not created"
|
||||
return 1
|
||||
@@ -149,7 +138,6 @@ build_platform() {
|
||||
echo "✓ BPF filter generated: $(ls -lh "$bpf_file" | awk '{print $5}')"
|
||||
|
||||
# Remove the generator binary (we only need the BPF file)
|
||||
echo "Removing generator binary to save space..."
|
||||
rm -f "$output_dir/seccomp-unix-block"
|
||||
|
||||
# Verify final state
|
||||
|
||||
@@ -265,11 +265,8 @@ function generateReadRules(
|
||||
// directory (e.g. /Users, /Users/chris) even if only a subdirectory like
|
||||
// ~/.local is in allowWithinDeny. This only allows metadata reads on
|
||||
// directories — not listing contents (readdir) or reading files.
|
||||
if ((config.denyOnly).length > 0) {
|
||||
rules.push(
|
||||
`(allow file-read-metadata`,
|
||||
` (vnode-type DIRECTORY))`,
|
||||
)
|
||||
if (config.denyOnly.length > 0) {
|
||||
rules.push(`(allow file-read-metadata`, ` (vnode-type DIRECTORY))`)
|
||||
}
|
||||
|
||||
// Block file movement to prevent bypass via mv/rename
|
||||
@@ -292,17 +289,6 @@ function generateWriteRules(
|
||||
|
||||
const rules: string[] = []
|
||||
|
||||
// Automatically allow TMPDIR parent on macOS when write restrictions are enabled
|
||||
const tmpdirParents = getTmpdirParentIfMacOSPattern()
|
||||
for (const tmpdirParent of tmpdirParents) {
|
||||
const normalizedPath = normalizePathForSandbox(tmpdirParent)
|
||||
rules.push(
|
||||
`(allow file-write*`,
|
||||
` (subpath ${escapePath(normalizedPath)})`,
|
||||
` (with message "${logTag}"))`,
|
||||
)
|
||||
}
|
||||
|
||||
// Generate allow rules
|
||||
for (const pathPattern of config.allowOnly || []) {
|
||||
const normalizedPath = normalizePathForSandbox(pathPattern)
|
||||
@@ -651,31 +637,6 @@ function escapePath(pathStr: string): string {
|
||||
return JSON.stringify(pathStr)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get TMPDIR parent directory if it matches macOS pattern /var/folders/XX/YYY/T/
|
||||
* Returns both /var/ and /private/var/ versions since /var is a symlink
|
||||
*/
|
||||
function getTmpdirParentIfMacOSPattern(): string[] {
|
||||
const tmpdir = process.env.TMPDIR
|
||||
if (!tmpdir) return []
|
||||
|
||||
const match = tmpdir.match(
|
||||
/^\/(private\/)?var\/folders\/[^/]{2}\/[^/]+\/T\/?$/,
|
||||
)
|
||||
if (!match) return []
|
||||
|
||||
const parent = tmpdir.replace(/\/T\/?$/, '')
|
||||
|
||||
// Return both /var/ and /private/var/ versions since /var is a symlink
|
||||
if (parent.startsWith('/private/var/')) {
|
||||
return [parent, parent.replace('/private', '')]
|
||||
} else if (parent.startsWith('/var/')) {
|
||||
return [parent, '/private' + parent]
|
||||
}
|
||||
|
||||
return [parent]
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrap command with macOS sandbox
|
||||
*/
|
||||
|
||||
@@ -46,8 +46,8 @@ function cleanupTmpClaude(): void {
|
||||
|
||||
describe('macOS Seatbelt Symlink Boundary Validation', () => {
|
||||
// Use unique test directories per run
|
||||
// IMPORTANT: Use /private/tmp (not os.tmpdir() which is /var/folders/...)
|
||||
// because /var/folders is automatically allowed by the sandbox via TMPDIR parent rule
|
||||
// Use /private/tmp (not os.tmpdir()) so test paths are outside any
|
||||
// default-allowed write location
|
||||
const TEST_ID = Date.now()
|
||||
const TEST_BASE_DIR = `/private/tmp/symlink-boundary-test-${TEST_ID}`
|
||||
const WORKSPACE_DIR = join(TEST_BASE_DIR, 'workspace')
|
||||
|
||||
6
vendor/seccomp-src/seccomp-unix-block.c
vendored
6
vendor/seccomp-src/seccomp-unix-block.c
vendored
@@ -65,8 +65,12 @@ int main(int argc, char *argv[]) {
|
||||
/* Add rule to block socket(AF_UNIX, ...) */
|
||||
/* socket() syscall signature: int socket(int domain, int type, int protocol) */
|
||||
/* arg0 = domain (AF_UNIX = 1) */
|
||||
/* Use SCMP_CMP_MASKED_EQ with a 32-bit mask: the domain argument is a 32-bit
|
||||
* int, so the kernel ignores the upper 32 bits of the register. A plain
|
||||
* SCMP_CMP_EQ would compare all 64 bits and miss calls where the upper bits
|
||||
* are set. */
|
||||
rc = seccomp_rule_add(ctx, SCMP_ACT_ERRNO(EPERM), SCMP_SYS(socket), 1,
|
||||
SCMP_A0(SCMP_CMP_EQ, AF_UNIX));
|
||||
SCMP_A0(SCMP_CMP_MASKED_EQ, 0xffffffff, AF_UNIX));
|
||||
if (rc < 0) {
|
||||
fprintf(stderr, "Error: Failed to add seccomp rule: %s\n", strerror(-rc));
|
||||
seccomp_release(ctx);
|
||||
|
||||
BIN
vendor/seccomp/arm64/unix-block.bpf
vendored
BIN
vendor/seccomp/arm64/unix-block.bpf
vendored
Binary file not shown.
BIN
vendor/seccomp/x64/unix-block.bpf
vendored
BIN
vendor/seccomp/x64/unix-block.bpf
vendored
Binary file not shown.
Reference in New Issue
Block a user