├── .gitignore ├── LICENSE ├── README.md ├── example-ffi ├── .gitignore ├── Makefile ├── main.ts └── spawn.c ├── seatbelt.ts └── sh-deno.ts /.gitignore: -------------------------------------------------------------------------------- 1 | .vim 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright 2024 Divy Srivastava 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Security Hardened Deno for macOS 2 | 3 | https://littledivy.com/sh-deno 4 | 5 | Overcome the technical limitations of Deno permission system by wrapping it with 6 | macOS's native sanboxing primitives. 7 | 8 | ```diff 9 | - deno run --allow-read=. --allow-ffi main.ts 10 | + sh-deno run --allow-read=. --allow-ffi main.ts 11 | ``` 12 | 13 | Permissions are enforced on child processes, FFI native code and Deno's 14 | internals. 15 | 16 | ```c 17 | // sample FFI dylib 18 | 19 | static void __attribute__((constructor)) 20 | initialize(void) 21 | { 22 | fopen("/etc/passwd", "r"); // blocked by sh-deno 23 | } 24 | ``` 25 | 26 | ## Running without sh-deno 27 | 28 | Pass `--emit-profile` to export the sandbox (Apple seatbelt) profile that 29 | can be used without `sh-deno`. 30 | 31 | ```sh 32 | sh-deno --emit-profile \ 33 | --allow-read=. --allow-ffi > sandbox.sb 34 | 35 | sandbox-exec -f sandbox.sb \ 36 | deno run --allow-read=. --allow-ffi main.ts 37 | ``` 38 | 39 | > **Note**: This project is in early development stage. Please use with caution. 40 | -------------------------------------------------------------------------------- /example-ffi/.gitignore: -------------------------------------------------------------------------------- 1 | *.dylib 2 | -------------------------------------------------------------------------------- /example-ffi/Makefile: -------------------------------------------------------------------------------- 1 | spawn.dylib: 2 | $(CC) -dynamiclib -o spawn.dylib spawn.c 3 | -------------------------------------------------------------------------------- /example-ffi/main.ts: -------------------------------------------------------------------------------- 1 | Deno.dlopen("./spawn.dylib", {}); 2 | -------------------------------------------------------------------------------- /example-ffi/spawn.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | static void __attribute__((constructor)) 4 | initialize(void) 5 | { 6 | system("/bin/ls"); 7 | } 8 | -------------------------------------------------------------------------------- /seatbelt.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Divy Srivastava. All rights reserved. MIT license. 2 | 3 | export function subpath(path: string): SeatbeltConfigValue { 4 | return { 5 | type: "subpath", 6 | value: path, 7 | }; 8 | } 9 | 10 | export function path(path: string): SeatbeltConfigValue { 11 | return { 12 | type: "path", 13 | value: path, 14 | }; 15 | } 16 | 17 | export function allow( 18 | name: string, 19 | values: SeatbeltConfigValue[], 20 | ): SeatbeltConfigValue { 21 | return { 22 | name, 23 | values, 24 | }; 25 | } 26 | 27 | export function remoteIp(host: string): SeatbeltConfigValue { 28 | return { 29 | type: "remote ip", 30 | value: host == "localhost" ? "localhost:*" : `*:*`, 31 | }; 32 | } 33 | 34 | export type SeatbeltConfigValue = string | SeatbeltConfigValue[] | { 35 | type: string; 36 | value: string; 37 | } | { 38 | name: string; 39 | values: SeatbeltConfigValue[]; 40 | }; 41 | 42 | export function buildSeatbeltConfig(config: SeatbeltConfigValue[]) { 43 | let r = "(version 1)\n"; 44 | r += "(deny default)\n\n"; 45 | r += '(import "bsd.sb")\n\n'; 46 | for (const c of config) { 47 | r += writeConfigValue(c); 48 | } 49 | 50 | return r; 51 | } 52 | 53 | function writeConfigValue(value: SeatbeltConfigValue): string { 54 | if (typeof value === "string") { 55 | return `(allow ${value})\n`; 56 | } else if (Array.isArray(value)) { 57 | return value.map(writeConfigValue).join(""); 58 | } else if ("type" in value) { 59 | return `(${value.type} "${value.value}")\n`; 60 | } else if ("name" in value) { 61 | return `(allow ${value.name}\n ${ 62 | value.values.map(writeConfigValue).join(" ") 63 | })\n`; 64 | } 65 | 66 | throw new Error("Invalid value"); 67 | } 68 | -------------------------------------------------------------------------------- /sh-deno.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Divy Srivastava. All rights reserved. MIT license. 2 | // 3 | // https://reverse.put.as/wp-content/uploads/2011/09/Apple-Sandbox-Guide-v1.0.pdf 4 | 5 | import { 6 | allow, 7 | buildSeatbeltConfig, 8 | path, 9 | remoteIp, 10 | type SeatbeltConfigValue, 11 | subpath, 12 | } from "./seatbelt.ts"; 13 | import { parseArgs } from "jsr:@std/cli@1.0.6/parse-args"; 14 | 15 | const rules: Record SeatbeltConfigValue> = { 16 | "allow-run": (...paths: string[]) => 17 | allow("process-exec", [ 18 | ...paths.map((path) => subpath(path)), 19 | ]), 20 | "allow-read": (...paths: string[]) => 21 | allow("file-read*", [ 22 | ...paths.map((path) => subpath(path)), 23 | ]), 24 | "allow-write": (...paths: string[]) => 25 | allow("file-write*", [ 26 | ...paths.map((path) => subpath(path)), 27 | ]), 28 | "allow-net": (...hosts: string[]) => [ 29 | allow("network-bind", hosts.map(remoteIp)), 30 | allow("network-outbound", hosts.map(remoteIp)), 31 | ], 32 | "allow-sys": () => [ 33 | "sysctl-read", 34 | ], 35 | }; 36 | 37 | export async function run(args: string[]) { 38 | const execPath = Deno.execPath(); 39 | 40 | const config = buildSeatbeltConfig([ 41 | "system-fsctl", 42 | "process-fork", 43 | 44 | allow("process-exec", [ 45 | path(execPath), 46 | ]), 47 | 48 | ...createRules(args), 49 | 50 | allow("file-read*", [ 51 | subpath(new URL(".", import.meta.url).pathname), 52 | path(execPath), 53 | 54 | subpath(denoDir()), 55 | ]), 56 | ]); 57 | 58 | if (args.includes("--emit-profile")) { 59 | console.log(config); 60 | return; 61 | } 62 | 63 | const command = new Deno.Command("sandbox-exec", { 64 | args: ["-p", config, "deno", ...args], 65 | stdout: "inherit", 66 | stderr: "inherit", 67 | }); 68 | 69 | await command.output(); 70 | } 71 | 72 | export function* createRules(args: string[]) { 73 | const parsed = parseArgs(args); 74 | 75 | for (const [key, value] of Object.entries(parsed)) { 76 | if (rules[key]) { 77 | const values = value.split(","); 78 | yield rules[key](...values); 79 | } 80 | } 81 | } 82 | 83 | function denoDir(): string { 84 | const command = new Deno.Command(Deno.execPath(), { 85 | args: ["info"], 86 | stdout: "piped", 87 | }); 88 | 89 | const output = command.outputSync(); 90 | 91 | const info = new TextDecoder().decode(output.stdout); 92 | 93 | const lines = info.split("\n"); 94 | 95 | for (const line of lines) { 96 | if (line.includes("DENO_DIR")) { 97 | return line.split(" ")[2]?.trim(); 98 | } 99 | } 100 | 101 | throw new Error("DENO_DIR not found"); 102 | } 103 | 104 | if (import.meta.main) { 105 | await run(Deno.args); 106 | } 107 | --------------------------------------------------------------------------------