use std::fs; use std::path::Path; use std::process::{Command, Output}; use std::sync::Arc; use avril_runtime::caps::{Capabilities, Category, Initial, NetGrant, PathTarget}; fn policy(zero: bool, grants: &[(Category, bool, &str)], cwd: &Path) -> Arc { let mut initial = Initial::new(zero); for (category, deny, value) in grants { initial.grant(*category, *deny, value, cwd).unwrap(); } Capabilities::new(initial, cwd.to_owned()) } fn run(dir: &Path, args: &[&str]) -> Output { Command::new(env!("CARGO_BIN_EXE_avril")) .arg("run") .current_dir(dir) .args(args) .output() .unwrap() } #[test] fn network_grants_are_exact_normalized_and_port_scoped() { let dir = tempfile::tempdir().unwrap(); let caps = policy( true, &[ (Category::Net, false, "BÜCHER.example"), (Category::Net, false, "localhost:80"), (Category::Net, false, "[2001:0db8::1]:443"), (Category::Net, false, "[::2]"), (Category::Net, false, "0x7f.1"), (Category::Net, true, "bücher.example:443"), ], dir.path(), ); assert!(caps.check_net("xn--bcher-kva.example.", 8080).is_ok()); assert!(caps.check_net("bücher.example。", 8080).is_ok()); assert!(caps.check_net("bücher.example", 443).is_err()); assert!(caps.check_net("sub.xn--bcher-kva.example", 8080).is_err()); assert!(caps.check_net("localhost", 80).is_ok()); assert!(caps.check_net("localhost", 443).is_err()); assert!(caps.check_net("[2001:db8::1]", 443).is_ok()); assert!(caps.check_net("[2001:db8::1]", 80).is_err()); assert!(caps.check_net("2001:db8::1", 443).is_ok()); assert!(caps.check_net("2001:0db8::1", 443).is_ok()); assert!(caps.check_net("2001:db8::1", 80).is_err()); assert!(caps.check_net("2001:db8::2", 443).is_err()); assert!(caps.check_net("[2001:db8::1]:443", 443).is_err()); assert!(caps.check_net("::2", 80).is_ok()); assert!(caps.check_net("::2", 443).is_ok()); assert!(caps.check_net("127.0.0.1", 1234).is_ok()); for grant in [ "", "a,b", "https://a", "user@a", "a/path", "a?b", "a#b", "a:", "a:65536", "a:1x", "a:-1", "::1", "[::1]extra", "[::1]extra:80", "[::1]:", "[::1]:80]", "a b", "*.example.com", "a%2eb", ] { assert!(NetGrant::parse(grant).is_err(), "{grant}"); } } #[test] fn pledges_are_atomic_monotonic_and_inherited_as_snapshots() { let dir = tempfile::tempdir().unwrap(); let caps = policy(false, &[], dir.path()); assert!( caps.pledge(vec![ (Category::Net, Some(vec![]), vec![]), (Category::Read, None, vec!["\0".into()]) ]) .is_err() ); assert!(caps.check_net("a.example", 80).is_ok()); let child = Capabilities::inherit(caps.clone()); child .pledge(vec![( Category::Net, Some(vec!["a.example".into(), "b.example".into()]), vec![], )]) .unwrap(); assert!(child.check_net("a.example", 80).is_ok()); assert!(child.check_net("c.example", 80).is_err()); caps.pledge(vec![(Category::Net, None, vec!["a.example:80".into()])]) .unwrap(); assert!(caps.check_net("a.example", 80).is_err()); assert!(child.check_net("a.example", 80).is_ok()); assert!(child.check_net("a.example", 443).is_ok()); child .pledge(vec![( Category::Net, Some(vec!["b.example".into(), "c.example".into()]), vec![], )]) .unwrap(); assert!(child.check_net("a.example", 443).is_err()); assert!(child.check_net("b.example", 443).is_ok()); assert!(child.check_net("c.example", 443).is_err()); caps.pledge(vec![(Category::Net, Some(vec![]), vec![])]) .unwrap(); assert!(caps.check_net("b.example", 443).is_err()); assert!(child.check_net("b.example", 443).is_ok()); let later = Capabilities::inherit(caps.clone()); assert!(later.check_net("b.example", 443).is_err()); assert!( policy(true, &[], dir.path()) .check_net("b.example", 443) .is_err() ); } #[test] fn read_implies_module_loading_but_execution_denials_override_it() { let dir = tempfile::tempdir().unwrap(); fs::create_dir(dir.path().join("app")).unwrap(); fs::create_dir(dir.path().join("application")).unwrap(); for file in ["app/public.js", "app/private.js", "application/other.js"] { fs::write(dir.path().join(file), "").unwrap(); } let public = PathTarget::resolve(dir.path().join("app/public.js")).unwrap(); let private = PathTarget::resolve(dir.path().join("app/private.js")).unwrap(); let outside = PathTarget::resolve(dir.path().join("application/other.js")).unwrap(); let caps = policy( true, &[ (Category::Read, false, "app"), (Category::JsExec, true, "app/private.js"), ], dir.path(), ); assert!(caps.check_read(&private).is_ok()); assert!(caps.check_js_exec(&private).is_err()); assert!(caps.check_js_exec(&public).is_ok()); assert!(caps.check_js_exec(&outside).is_err()); caps.pledge(vec![( Category::JsExec, Some(vec!["application".into()]), vec![], )]) .unwrap(); assert!(caps.check_js_exec(&public).is_err()); let exec_only = policy(true, &[(Category::JsExec, false, "app")], dir.path()); assert!(exec_only.check_js_exec(&public).is_ok()); assert!(exec_only.check_read(&public).is_err()); let inherited_read = policy(true, &[(Category::Read, false, "app")], dir.path()); inherited_read .pledge(vec![(Category::Read, Some(vec![]), vec![])]) .unwrap(); assert!(inherited_read.check_js_exec(&public).is_err()); } #[test] fn zero_keeps_stdio_and_the_entry_but_not_its_directory() { let dir = tempfile::tempdir().unwrap(); fs::write( dir.path().join("main.js"), r#" console.log('stdio'); for (const path of ['./dep.js', '././dep.js']) { try { await import(path); throw new Error('Unexpected import'); } catch (e) { if (!e.message.includes('PermissionDenied: js-exec')) throw e; } } export const main = import.meta.main; import(import.meta.url).then(namespace => console.log(namespace.main)); "#, ) .unwrap(); fs::write( dir.path().join("dep.js"), "throw new Error('Executed denied module');", ) .unwrap(); let output = run(dir.path(), &["-0", "main.js"]); assert!( output.status.success(), "{}", String::from_utf8_lossy(&output.stderr) ); assert_eq!(output.stdout, b"stdio\ntrue\n"); } #[test] fn repeated_cli_grants_preserve_commas_and_apply_denials_last() { let dir = tempfile::tempdir().unwrap(); for folder in ["app, data", "other", "app, data/private"] { fs::create_dir_all(dir.path().join(folder)).unwrap(); } fs::write( dir.path().join("app, data/one.ts"), "export const one: number = 1;", ) .unwrap(); fs::write(dir.path().join("other/two.js"), "export const two = 2;").unwrap(); fs::write(dir.path().join("app, data/private/no.js"), "export {};").unwrap(); fs::write( dir.path().join("main.js"), r#" import {one} from './app, data/one.ts'; import {two} from './other/two.js'; try { await import('./app, data/private/no.js'); throw new Error('Unexpected import'); } catch (e) { if (!e.message.includes('PermissionDenied: js-exec')) throw e; } console.log(one + two); "#, ) .unwrap(); for args in [ vec![ "-0", "--allow-read=app, data", "--allow-js-exec=other", "--deny-js-exec=app, data/private", "main.js", ], vec![ "--deny-js-exec=app, data/private", "main.js", "--allow-js-exec=other", "--allow-read=app, data", "-0", ], ] { let output = run(dir.path(), &args); assert!( output.status.success(), "{}", String::from_utf8_lossy(&output.stderr) ); assert_eq!(output.stdout, b"3\n"); } let output = run(dir.path(), &["-0", "main.js"]); assert!(!output.status.success()); assert!(String::from_utf8_lossy(&output.stderr).contains("PermissionDenied: js-exec")); } #[test] fn runtime_pledges_recheck_cached_imports_and_cannot_restore_authority() { let dir = tempfile::tempdir().unwrap(); fs::write(dir.path().join("a.js"), "export const value = 42;").unwrap(); fs::write(dir.path().join("b.js"), "export const value = 7;").unwrap(); fs::write( dir.path().join("main.js"), r#" import { pledge } from 'avril:std'; const a = await import('./a.js'); pledge({jsExec:{deny:['./a.js']}}); for (const path of ['./a.js', new URL('./a.js', import.meta.url)]) { try { await import(path); throw new Error('Unexpected cached import'); } catch (e) { if (!e.message.includes('PermissionDenied: js-exec')) throw e; } } pledge({jsExec:{allow:['.']}}); console.log(a.value, (await import('./b.js')).value); pledge({jsExec:false}); pledge({jsExec:{allow:['.']}}); try { await import('./b.js'); throw new Error('Restored authority'); } catch (e) { if (!e.message.includes('PermissionDenied: js-exec')) throw e; } console.log('restricted'); "#, ) .unwrap(); let output = run(dir.path(), &["main.js"]); assert!( output.status.success(), "{}", String::from_utf8_lossy(&output.stderr) ); assert_eq!(output.stdout, b"42 7\nrestricted\n"); } #[test] fn invalid_pledges_do_not_partially_commit_and_captured_intrinsics_keep_working() { let dir = tempfile::tempdir().unwrap(); fs::write(dir.path().join("dep.js"), "export const value = 42;").unwrap(); fs::write( dir.path().join("main.js"), r#" import { pledge } from 'avril:std'; for (const request of [{jsExec:false,net:{deny:['a,b']}}, {jsExec:false,read:{deny:['\0']}}, {jsExec:false,unknown:false}, {jsExec:{allow:'not-an-array'}}, {jsExec:true}, null]) { try { pledge(request); throw new Error('Expected invalid pledge'); } catch(e) { if (!(e instanceof TypeError)) throw e; } } console.log((await import('./dep.js')).value); const strings = String.prototype; const poison = () => { throw new Error('Poisoned intrinsic'); }; globalThis.String = poison; strings.toWellFormed = poison; Array.prototype.push = poison; Object.defineProperty(Array.prototype,'1',{set:poison,configurable:true}); pledge({net:{allow:['a.example','b.example']},jsExec:{deny:['dep.js']}}); delete Array.prototype[1]; try { await import('./dep.js'); throw new Error('Unexpected import'); } catch(e) { if (!e.message.includes('PermissionDenied: js-exec')) throw e; } console.log('atomic'); "#, ) .unwrap(); let output = run(dir.path(), &["main.js"]); assert!( output.status.success(), "{}", String::from_utf8_lossy(&output.stderr) ); assert_eq!(output.stdout, b"42\natomic\n"); } #[cfg(unix)] #[test] fn symlink_aliases_cannot_escape_grants_or_execution_denials() { use std::os::unix::fs::symlink; let dir = tempfile::tempdir().unwrap(); fs::create_dir(dir.path().join("app")).unwrap(); fs::create_dir(dir.path().join("private")).unwrap(); fs::write(dir.path().join("private/no.js"), "export {};").unwrap(); symlink("../private/no.js", dir.path().join("app/alias.js")).unwrap(); fs::write( dir.path().join("main.js"), "await import('./app/alias.js');", ) .unwrap(); for args in [ vec!["-0", "--allow-read=app", "main.js"], vec!["--deny-js-exec=private", "main.js"], ] { let output = run(dir.path(), &args); assert!(!output.status.success()); assert!(String::from_utf8_lossy(&output.stderr).contains("PermissionDenied: js-exec")); } let output = run( dir.path(), &["-0", "--allow-js-exec=app/alias.js", "main.js"], ); assert!( output.status.success(), "{}", String::from_utf8_lossy(&output.stderr) ); } #[test] fn cli_rejects_malformed_grants_and_unknown_options() { let dir = tempfile::tempdir().unwrap(); fs::write(dir.path().join("main.js"), "console.log('executed');").unwrap(); for flag in [ "--allow-net=a,b", "--allow-net=host:65536", "--allow-net=", "--allow-net=https://example.com", "--allow-read=", "--allow-js-exec", "--allow-unknown=x", "--unknown=x", ] { let output = run(dir.path(), &[flag, "main.js"]); assert!(!output.status.success(), "{flag}"); assert!(output.stdout.is_empty()); } let output = run( dir.path(), &[ "-0", "--allow-net=host1", "--allow-net=host2:443", "main.js", ], ); assert!(output.status.success()); assert_eq!(output.stdout, b"executed\n"); } #[test] fn queued_import_of_an_unevaluated_cached_module_is_rechecked() { let dir = tempfile::tempdir().unwrap(); fs::write( dir.path().join("graph.js"), "import './throw.js'; import './later.js';", ) .unwrap(); fs::write( dir.path().join("throw.js"), "throw new Error('Stop this graph');", ) .unwrap(); fs::write( dir.path().join("later.js"), "import './dep.js'; globalThis.executed = true;", ) .unwrap(); fs::write( dir.path().join("dep.js"), "globalThis.executedDependency = true;", ) .unwrap(); for (file, source) in [ ( "diamond.js", "import './throw.js'; import './left.js'; import './right.js';", ), ("left.js", "import './shared.js';"), ("right.js", "import './shared.js';"), ( "shared.js", "import './dep.js'; globalThis.executed = true;", ), ("cycle.js", "import './throw.js'; import './x.js';"), ("x.js", "import './y.js'; globalThis.executed = true;"), ("y.js", "import './x.js'; import './dep.js';"), ] { fs::write(dir.path().join(file), source).unwrap(); } for (graph, parent, denied) in [ ("./graph.js", "./later.js", "./later.js"), ("./graph.js", "./later.js", "./dep.js"), ("./diamond.js", "./left.js", "./dep.js"), ("./cycle.js", "./x.js", "./dep.js"), ] { let source = r#" import { pledge } from 'avril:std'; await import('GRAPH').catch(() => {}); const pending = import('PARENT'); pledge({jsExec:{deny:['DENIED']}}); try { await pending; throw new Error('Expected queued denial'); } catch(e) { if (!e.message.includes('PermissionDenied: js-exec')) throw e; } console.log(globalThis.executed === undefined && globalThis.executedDependency === undefined); "#.replace("DENIED", denied).replace("GRAPH", graph).replace("PARENT", parent); fs::write(dir.path().join("main.js"), source).unwrap(); let output = run(dir.path(), &["main.js"]); assert!( output.status.success(), "{}", String::from_utf8_lossy(&output.stderr) ); assert_eq!(output.stdout, b"true\n"); } } #[test] fn denied_imports_do_not_disclose_canonical_targets_or_resolution_errors() { let dir = tempfile::tempdir().unwrap(); fs::write(dir.path().join("existing.js"), "export {};").unwrap(); #[cfg(unix)] std::os::unix::fs::symlink("existing.js", dir.path().join("alias.js")).unwrap(); fs::write( dir.path().join("main.js"), r#" for(const specifier of ['./existing.js', './missing.js', './alias.js']) { try { await import(specifier); throw new Error('Expected denial'); } catch(e) { if (e.message !== 'PermissionDenied: js-exec ' + specifier) throw e; } } console.log('denied'); "#, ) .unwrap(); let output = run(dir.path(), &["-0", "main.js"]); assert!( output.status.success(), "{}", String::from_utf8_lossy(&output.stderr) ); assert_eq!(output.stdout, b"denied\n"); } #[test] fn pledge_is_sealed_and_accepts_module_relative_file_urls() { let dir = tempfile::tempdir().unwrap(); fs::create_dir(dir.path().join("app")).unwrap(); fs::write( dir.path().join("app/blocked.js"), "throw new Error('Should not execute');", ) .unwrap(); fs::write( dir.path().join("app/main.js"), r#" import { pledge } from 'avril:std'; import * as std from 'avril:std'; if (Symbol.for('__avril.base') in globalThis) throw new Error('Leaked base'); try { std.pledge = () => {}; throw new Error('Replaced pledge'); } catch(e) { if (!(e instanceof TypeError)) throw e; } pledge({jsExec:{deny:[new URL('./blocked.js',import.meta.url)]}}); try { await import('./blocked.js'); throw new Error('Expected denial'); } catch(e) { if (!e.message.includes('PermissionDenied: js-exec')) throw e; } console.log('sealed'); "#, ) .unwrap(); let output = run(dir.path(), &["app/main.js"]); assert!( output.status.success(), "{}", String::from_utf8_lossy(&output.stderr) ); assert_eq!(output.stdout, b"sealed\n"); } #[test] fn inherited_byte_streams_remain_available_with_zero_and_after_pledging() { use std::io::Write; use std::process::Stdio; let dir = tempfile::tempdir().unwrap(); fs::write( dir.path().join("main.js"), r#" import { pledge, stdin, stdout, stderr } from 'avril:std'; pledge({net:false,read:false,jsExec:false}); await stderr.getWriter().write(new Uint8Array([5,6])); await stdin.pipeTo(stdout); "#, ) .unwrap(); let mut child = Command::new(env!("CARGO_BIN_EXE_avril")) .arg("run") .current_dir(dir.path()) .args(["-0", "main.js"]) .stdin(Stdio::piped()) .stdout(Stdio::piped()) .stderr(Stdio::piped()) .spawn() .unwrap(); let input = [0, 1, 127, 128, 255]; child.stdin.take().unwrap().write_all(&input).unwrap(); let output = child.wait_with_output().unwrap(); assert!( output.status.success(), "{}", String::from_utf8_lossy(&output.stderr) ); assert_eq!(output.stdout, input); assert_eq!(output.stderr, [5, 6]); } #[test] fn write_grants_are_independent_and_missing_files_are_exact() { let dir = tempfile::tempdir().unwrap(); let target = |name: &str| PathTarget::for_creation(dir.path().join(name)).unwrap(); let caps = policy( true, &[ (Category::Write, false, "new.txt"), (Category::Write, false, "future/"), (Category::Write, true, "future/private/"), (Category::Write, false, "a, b"), (Category::Write, false, "second"), ], dir.path(), ); for path in ["new.txt", "future", "future/file", "a, b", "second"] { assert!(caps.check_write(&target(path)).is_ok(), "{path}"); } for path in [ "new.txt/child", "future/private", "future/private/file", "a", "b", "other", ] { assert!(caps.check_write(&target(path)).is_err(), "{path}"); } assert!(caps.check_read(&target("new.txt")).is_err()); assert!(caps.check_js_exec(&target("new.txt")).is_err()); assert!( policy(false, &[], dir.path()) .check_write(&target("anything")) .is_ok() ); assert!( policy(true, &[], dir.path()) .check_write(&target("anything")) .is_err() ); } #[test] fn write_pledges_are_atomic_monotonic_and_snapshot_for_children() { let dir = tempfile::tempdir().unwrap(); let target = PathTarget::for_creation(dir.path().join("new.txt")).unwrap(); let caps = policy(false, &[], dir.path()); let child = Capabilities::inherit(caps.clone()); assert!( caps.pledge(vec![ (Category::Write, Some(vec![]), vec![]), (Category::Read, None, vec!["\0".into()]) ]) .is_err() ); assert!(child.check_write(&target).is_ok()); caps.pledge(vec![( Category::Write, Some(vec!["new.txt".into()]), vec![], )]) .unwrap(); assert!(child.check_write(&target).is_ok()); child .pledge(vec![(Category::Write, Some(vec![]), vec![])]) .unwrap(); child .pledge(vec![( Category::Write, Some(vec!["new.txt".into()]), vec![], )]) .unwrap(); assert!(child.check_write(&target).is_err()); // Existing workers keep their snapshot; later ones inherit the denial. let other = Capabilities::inherit(caps.clone()); assert!(other.check_write(&target).is_ok()); caps.pledge(vec![(Category::Write, None, vec!["new.txt".into()])]) .unwrap(); assert!(caps.check_write(&target).is_err()); assert!(other.check_write(&target).is_ok()); let narrowed = Capabilities::inherit(caps.clone()); assert!(narrowed.check_write(&target).is_err()); } #[test] fn creation_targets_resolve_existing_prefixes_without_inventing_parent_traversal() { let dir = tempfile::tempdir().unwrap(); fs::create_dir(dir.path().join("real")).unwrap(); let path = dir.path().join("real/missing/deeper/file"); assert_eq!(PathTarget::for_creation(&path).unwrap().as_path(), path); assert!(PathTarget::for_creation(dir.path().join("real/missing/../file")).is_err()); let mut initial = Initial::new(true); assert!( initial .grant(Category::Write, false, "real/missing/../file", dir.path()) .is_err() ); #[cfg(unix)] { std::os::unix::fs::symlink("real", dir.path().join("alias")).unwrap(); assert_eq!( PathTarget::for_creation(dir.path().join("alias/missing/file")) .unwrap() .as_path(), dir.path().join("real/missing/file") ); let caps = policy(true, &[(Category::Write, false, "alias/new")], dir.path()); assert!( caps.check_write(&PathTarget::for_creation(dir.path().join("real/new")).unwrap()) .is_ok() ); } } #[test] fn cli_and_public_pledge_accept_write_without_granting_module_execution() { let dir = tempfile::tempdir().unwrap(); fs::write( dir.path().join("dep.js"), "throw Error('Unexpected execution')", ) .unwrap(); fs::write(dir.path().join("main.js"),r#" import {pledge} from 'avril:std'; pledge({write:{allow:['a, b','future/'],deny:['future/private/']}}); try{pledge({write:false,unknown:false});throw Error('Invalid pledge');}catch(e){if(!(e instanceof TypeError))throw e;} pledge({write:false}); try{await import('./dep.js');throw Error('Write grants authorize execution');}catch(e){if(!e.message.includes('PermissionDenied: js-exec'))throw e;} console.log('write capability'); "#).unwrap(); let output = run( dir.path(), &[ "-0", "--allow-write=a, b", "--allow-write=future/", "--deny-write=future/private/", "main.js", ], ); assert!( output.status.success(), "{}", String::from_utf8_lossy(&output.stderr) ); assert_eq!(output.stdout, b"write capability\n"); } #[test] fn path_denials_cover_descendants_after_missing_or_file_targets_become_directories() { let dir = tempfile::tempdir().unwrap(); fs::write(dir.path().join("file"), "").unwrap(); let caps = policy( false, &[ (Category::Write, true, "future"), (Category::Write, true, "file"), (Category::Read, true, "file"), ], dir.path(), ); fs::remove_file(dir.path().join("file")).unwrap(); fs::create_dir(dir.path().join("file")).unwrap(); for path in ["future/secret", "file/secret"] { let target = PathTarget::for_creation(dir.path().join(path)).unwrap(); assert!(caps.check_write(&target).is_err()); } assert!( caps.check_read(&PathTarget::for_creation(dir.path().join("file/secret")).unwrap()) .is_err() ); let other = policy(false, &[], dir.path()); other .pledge(vec![(Category::Write, None, vec!["missing".into()])]) .unwrap(); assert!( other .check_write(&PathTarget::for_creation(dir.path().join("missing/child")).unwrap()) .is_err() ); } #[test] fn remote_execution_grants_are_origin_scoped_canonical_and_monotonic() { let dir = tempfile::tempdir().unwrap(); let mut initial = Initial::new(true); initial .grant( Category::JsExec, false, "https://jsr.io/@test/pkg/", dir.path(), ) .unwrap(); initial .grant( Category::JsExec, true, "https://jsr.io/@test/pkg/1.0.0/blocked.ts", dir.path(), ) .unwrap(); let caps = Capabilities::new(initial, dir.path().to_owned()); let allowed = url::Url::parse("https://jsr.io/@test/pkg/1.0.0/mod.ts").unwrap(); caps.check_remote_exec(&allowed).unwrap(); caps.check_remote_acquire(&url::Url::parse("https://jsr.io/@test/pkg/").unwrap()) .unwrap(); for name in [ "http://jsr.io/@test/pkg/1.0.0/mod.ts", "https://jsr.io:444/@test/pkg/1.0.0/mod.ts", "https://jsr.io/@test/pkg-other/1.0.0/mod.ts", "https://jsr.io/@test/pkg/1.0.0/blocked.ts", "https://jsr.io/@test/pkg/1.0.0/blocked%2Ets", "https://jsr.io/@test/pkg/1.0.0/blocked.ts/child", "https://jsr.io/@test/pkg/1.0.0/%2fblocked.ts", ] { assert!( caps.check_remote_exec(&url::Url::parse(name).unwrap()) .is_err(), "{name}" ); } let child = Capabilities::inherit(caps.clone()); assert!( caps.pledge(vec![( Category::JsExec, Some(vec!["https://jsr.io/".into(), "https://[invalid".into()]), vec![] )]) .is_err() ); child.check_remote_exec(&allowed).unwrap(); caps.pledge(vec![( Category::JsExec, Some(vec![dir.path().to_string_lossy().into_owned()]), vec![], )]) .unwrap(); assert!(caps.check_remote_exec(&allowed).is_err()); assert!(child.check_remote_exec(&allowed).is_ok()); assert!( caps.check_remote_acquire(&url::Url::parse("https://jsr.io/@test/pkg/").unwrap()) .is_err() ); assert!( child .check_remote_acquire(&url::Url::parse("https://jsr.io/@test/pkg/").unwrap()) .is_ok() ); caps.pledge(vec![( Category::JsExec, Some(vec!["https://jsr.io/".into()]), vec![], )]) .unwrap(); assert!(caps.check_remote_exec(&allowed).is_err()); assert!(child.check_remote_exec(&allowed).is_ok()); let later = Capabilities::inherit(caps.clone()); assert!(later.check_remote_exec(&allowed).is_err()); } #[test] fn executable_grants_are_independent_monotonic_and_snapshot_for_workers() { let dir = tempfile::tempdir().unwrap(); fs::write(dir.path().join("tool"), "").unwrap(); fs::write(dir.path().join("other"), "").unwrap(); let tool = PathTarget::resolve(dir.path().join("tool")).unwrap(); let other = PathTarget::resolve(dir.path().join("other")).unwrap(); let read = policy(true, &[(Category::Read, false, ".")], dir.path()); let js = policy(true, &[(Category::JsExec, false, ".")], dir.path()); assert!(read.check_run(&tool).is_err()); assert!(js.check_run(&tool).is_err()); let caps = policy(true, &[(Category::Run, false, "tool")], dir.path()); assert!(caps.check_run(&tool).is_ok()); assert!(caps.check_run(&other).is_err()); assert!(caps.check_read(&tool).is_err()); assert!(caps.check_js_exec(&tool).is_err()); let child = Capabilities::inherit(caps.clone()); assert!(child.check_run(&tool).is_ok()); assert!( caps.pledge(vec![ (Category::Run, Some(vec![]), vec![]), (Category::Net, None, vec!["invalid,host".into()]) ]) .is_err() ); assert!(caps.check_run(&tool).is_ok()); caps.pledge(vec![(Category::Run, None, vec!["tool".into()])]) .unwrap(); assert!(caps.check_run(&tool).is_err()); assert!(child.check_run(&tool).is_ok()); caps.pledge(vec![(Category::Run, Some(vec!["tool".into()]), vec![])]) .unwrap(); assert!(caps.check_run(&tool).is_err()); assert!(child.check_run(&tool).is_ok()); let later = Capabilities::inherit(caps.clone()); assert!(later.check_run(&tool).is_err()); } #[test] fn missing_executable_grants_are_exact_and_file_urls_work() { let dir = tempfile::tempdir().unwrap(); let name = dir.path().join("tool"); let file_url = url::Url::from_file_path(&name).unwrap(); let caps = policy( true, &[(Category::Run, false, file_url.as_str())], dir.path(), ); fs::create_dir(&name).unwrap(); fs::write(name.join("child"), "").unwrap(); assert!(caps.check_run(&PathTarget::resolve(&name).unwrap()).is_ok()); assert!( caps.check_run(&PathTarget::resolve(name.join("child")).unwrap()) .is_err() ); } #[test] fn worker_snapshots_preserve_restrictions_and_isolate_descendants() { let dir = tempfile::tempdir().unwrap(); fs::create_dir(dir.path().join("app")).unwrap(); for file in ["app/one.js", "app/two.js", "tool", "other"] { fs::write(dir.path().join(file), "").unwrap(); } let one = PathTarget::resolve(dir.path().join("app/one.js")).unwrap(); let two = PathTarget::resolve(dir.path().join("app/two.js")).unwrap(); let tool = PathTarget::resolve(dir.path().join("tool")).unwrap(); let other = PathTarget::resolve(dir.path().join("other")).unwrap(); let write_target = |name: &str| PathTarget::for_creation(dir.path().join(name)).unwrap(); let caps = policy( true, &[ (Category::Net, false, "a.example"), (Category::Net, false, "b.example"), (Category::Read, false, "app"), (Category::Write, false, "app"), (Category::JsExec, false, "app"), (Category::Env, false, "KEEP"), (Category::Env, false, "DROP"), (Category::Sys, false, "hostname"), (Category::Run, false, "tool"), (Category::Ffi, false, "tool"), ], dir.path(), ); caps.pledge(vec![ ( Category::Net, Some(vec!["a.example".into(), "b.example".into()]), vec![], ), (Category::Read, Some(vec!["app".into()]), vec![]), (Category::Write, Some(vec!["app".into()]), vec![]), ]) .unwrap(); caps.pledge(vec![ ( Category::Net, Some(vec!["a.example".into()]), vec!["a.example:443".into()], ), (Category::Env, None, vec!["DROP".into()]), (Category::Read, None, vec!["app/two.js".into()]), (Category::Write, None, vec!["app/two.js".into()]), (Category::JsExec, None, vec!["app/two.js".into()]), ]) .unwrap(); let worker = Capabilities::inherit(caps.clone()); let sibling = Capabilities::inherit(caps.clone()); assert!(worker.check_net("a.example", 80).is_ok()); assert!(worker.check_net("a.example", 443).is_err()); assert!(worker.check_net("b.example", 80).is_err()); assert!(worker.check_read(&one).is_ok()); assert!(worker.check_read(&two).is_err()); assert!(worker.check_write(&write_target("app/one.js")).is_ok()); assert!(worker.check_write(&write_target("app/two.js")).is_err()); assert!(worker.check_js_exec(&one).is_ok()); assert!(worker.check_js_exec(&two).is_err()); assert!(worker.check_env("KEEP").is_ok()); assert!(worker.check_env("DROP").is_err()); assert!(worker.check_sys("hostname").is_ok()); assert!(worker.check_sys("uptime").is_err()); assert!(worker.check_run(&tool).is_ok()); assert!(worker.check_run(&other).is_err()); assert!(worker.check_ffi(&tool).is_ok()); assert!(worker.check_ffi(&other).is_err()); // Intersected allows cannot be widened back. worker .pledge(vec![( Category::Net, Some(vec!["a.example".into(), "b.example".into()]), vec![], )]) .unwrap(); assert!(worker.check_net("a.example", 80).is_ok()); assert!(worker.check_net("b.example", 80).is_err()); worker .pledge(vec![(Category::Env, None, vec!["KEEP".into()])]) .unwrap(); assert!(worker.check_env("KEEP").is_err()); assert!(sibling.check_env("KEEP").is_ok()); assert!(caps.check_env("KEEP").is_ok()); let nested = Capabilities::inherit(worker.clone()); assert!(nested.check_net("a.example", 80).is_ok()); assert!(nested.check_net("b.example", 80).is_err()); assert!(nested.check_env("KEEP").is_err()); assert!(nested.check_env("DROP").is_err()); assert!(nested.check_read(&one).is_ok()); caps.pledge(vec![(Category::Net, None, vec!["a.example:80".into()])]) .unwrap(); assert!(caps.check_net("a.example", 80).is_err()); assert!(worker.check_net("a.example", 80).is_ok()); assert!(sibling.check_net("a.example", 80).is_ok()); assert!(nested.check_net("a.example", 80).is_ok()); let later = Capabilities::inherit(caps.clone()); assert!(later.check_net("a.example", 80).is_err()); assert!(later.check_net("a.example", 8443).is_ok()); assert!(later.check_read(&one).is_ok()); assert!(later.check_env("KEEP").is_ok()); } #[test] fn nested_workers_snapshot_their_direct_parent() { let dir = tempfile::tempdir().unwrap(); let caps = policy(false, &[], dir.path()); let worker = Capabilities::inherit(caps.clone()); caps.pledge(vec![(Category::Net, None, vec!["a.example:80".into()])]) .unwrap(); assert!(caps.check_net("a.example", 80).is_err()); // Created after the root pledge, yet keeps its direct parent's snapshot. let nested = Capabilities::inherit(worker.clone()); assert!(worker.check_net("a.example", 80).is_ok()); assert!(nested.check_net("a.example", 80).is_ok()); // A later pledge by the direct parent leaves the existing nested worker // alone but narrows subsequent ones. worker .pledge(vec![(Category::Net, None, vec!["a.example:443".into()])]) .unwrap(); assert!(worker.check_net("a.example", 443).is_err()); assert!(nested.check_net("a.example", 443).is_ok()); let later = Capabilities::inherit(worker.clone()); assert!(later.check_net("a.example", 443).is_err()); assert!(later.check_net("a.example", 80).is_ok()); }