Skip to content

Commit 8ab49a1

Browse files
authored
Merge pull request #3 from topheman/feat/permissions
Add permission system for WebAssembly plugin sandboxing
2 parents dd50938 + 605c3a2 commit 8ab49a1

File tree

14 files changed

+334
-47
lines changed

14 files changed

+334
-47
lines changed

README.md

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,16 @@ Those hosts then run the same codebase which is compiled to WebAssembly:
2525
- the REPL logic
2626
- the plugins
2727

28-
The plugins like `ls` or `cat` can interact with the filesystem using the primitives of the languages they are written in.
28+
Security model: the REPL cli implements a security model inspired by [deno](https://docs.deno.com/runtime/fundamentals/security/#permissions):
29+
30+
- `--allow-net`: allows network access to the plugins, you can specify a list of domains comma separated (by default, no network access is allowed)
31+
- `--allow-read`: allows read access to the filesystem
32+
- `--allow-write`: allows write access to the filesystem
33+
- `--allow-all`: allows all permissions (same as all the flags above), short: `-A`
34+
35+
Plugins are sandboxed by default - they cannot access the filesystem or network unless explicitly permitted. This allows safe execution of untrusted plugins while maintaining the flexibility to grant specific permissions when needed.
36+
37+
Plugins like `ls` or `cat` can interact with the filesystem using the primitives of the languages they are written in.
2938

3039
- on the CLI, a folder from the disk is mounted via the `--dir` flag
3140
- on the browser, a virtual filesystem is mounted, the I/O operations are forwarded via the `@bytecodealliance/preview2-shim/filesystem` shim, which shims the `wasi:filesystem` filesystem interface
@@ -35,6 +44,13 @@ The plugins like `ls` or `cat` can interact with the filesystem using the primit
3544
Check the online demo at<br/><a href="https://topheman.github.io/webassembly-component-model-experiments/">topheman.github.io/webassembly-component-model-experiments</a>
3645
</p>
3746

47+
<p align="center">
48+
Example of running the CLI <code>pluginlab</code>
49+
<a href="https://asciinema.org/a/DWYAgrjSpwlejvRJQY8AHCEfD?speed=1.5" title="Click to watch the demo">
50+
<img src="./crates/pluginlab/demo-preview.png" alt="pluginlab demo" />
51+
</a>
52+
</p>
53+
3854
## Previous work with WebAssembly
3955

4056
In the last seven years I've done a few projects involving rust and WebAssembly:
@@ -65,12 +81,17 @@ pluginlab\
6581
--plugins https://topheman.github.io/webassembly-component-model-experiments/plugins/plugin_ls.wasm\
6682
--plugins https://topheman.github.io/webassembly-component-model-experiments/plugins/plugin_echo.wasm\
6783
--plugins https://topheman.github.io/webassembly-component-model-experiments/plugins/plugin_weather.wasm\
68-
--plugins https://topheman.github.io/webassembly-component-model-experiments/plugins/plugin_cat.wasm
84+
--plugins https://topheman.github.io/webassembly-component-model-experiments/plugins/plugin_cat.wasm\
85+
--allow-all
6986
```
7087

7188
Other flags:
7289

7390
- `--dir`: directory to be preopened (by default, the current directory)
91+
- `--allow-net`: allows network access to the plugins, you can specify a list of domains comma separated (by default, no network access is allowed)
92+
- `--allow-read`: allows read access to the filesystem
93+
- `--allow-write`: allows write access to the filesystem
94+
- `--allow-all`: allows all permissions (same as all the flags above), short: `-A`
7495
- `--help`: displays manual
7596
- `--debug`: run the host in debug mode (by default, the host runs in release mode)
7697

@@ -83,7 +104,8 @@ pluginlab\
83104
--plugins https://topheman.github.io/webassembly-component-model-experiments/plugins/plugin_ls.wasm\
84105
--plugins https://topheman.github.io/webassembly-component-model-experiments/plugins/plugin_echo.wasm\
85106
--plugins https://topheman.github.io/webassembly-component-model-experiments/plugins/plugin_weather.wasm\
86-
--plugins https://topheman.github.io/webassembly-component-model-experiments/plugins/plugin_cat.wasm
107+
--plugins https://topheman.github.io/webassembly-component-model-experiments/plugins/plugin_cat.wasm\
108+
--allow-all
87109
[Host] Starting REPL host...
88110
[Host] Loading REPL logic from: https://topheman.github.io/webassembly-component-model-experiments/plugins/repl_logic_guest.wasm
89111
[Host] Loading plugin: https://topheman.github.io/webassembly-component-model-experiments/plugins/plugin_greet.wasm
@@ -167,7 +189,8 @@ This will (see [justfile](./justfile)):
167189
--plugins ./target/wasm32-wasip1/debug/plugin_ls.wasm\
168190
--plugins ./target/wasm32-wasip1/debug/plugin_echo.wasm\
169191
--plugins ./target/wasm32-wasip1/debug/plugin_weather.wasm\
170-
--plugins ./target/wasm32-wasip1/debug/plugin_cat.wasm
192+
--plugins ./target/wasm32-wasip1/debug/plugin_cat.wasm\
193+
--allow-all
171194
```
172195

173196
This will run the `pluginlab` binary which will itself:
@@ -187,7 +210,8 @@ Other example:
187210
--repl-logic ./target/wasm32-wasip1/debug/repl_logic_guest.wasm\
188211
--plugins ./target/wasm32-wasip1/debug/plugin_ls.wasm\
189212
--plugins ./target/wasm32-wasip1/debug/plugin_echo.wasm\
190-
--dir /tmp
213+
--dir /tmp\
214+
--allow-all
191215
```
192216

193217
#### Test

crates/pluginlab/README.md

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,29 @@ Those hosts then run the same codebase which is compiled to WebAssembly:
2020
- the REPL logic
2121
- the plugins
2222

23-
The plugins like `ls` or `cat` can interact with the filesystem using the primitives of the languages they are written in.
23+
Security model: the REPL cli implements a security model inspired by [deno](https://docs.deno.com/runtime/fundamentals/security/#permissions):
24+
25+
- `--allow-net`: allows network access to the plugins, you can specify a list of domains comma separated (by default, no network access is allowed)
26+
- `--allow-read`: allows read access to the filesystem
27+
- `--allow-write`: allows write access to the filesystem
28+
- `--allow-all`: allows all permissions (same as all the flags above), short: `-A`
29+
30+
Plugins are sandboxed by default - they cannot access the filesystem or network unless explicitly permitted. This allows safe execution of untrusted plugins while maintaining the flexibility to grant specific permissions when needed.
31+
32+
Plugins like `ls` or `cat` can interact with the filesystem using the primitives of the languages they are written in.
2433

2534
- on the CLI, a folder from the disk is mounted via the `--dir` flag
2635
- on the browser, a virtual filesystem is mounted, the I/O operations are forwarded via the `@bytecodealliance/preview2-shim/filesystem` shim, which shims the `wasi:filesystem` filesystem interface
2736

2837
More details on the github repo: [topheman/webassembly-component-model-experiments](https://github.com/topheman/webassembly-component-model-experiments).
2938

39+
<p align="center">
40+
Example of running the CLI <code>pluginlab</code>
41+
<a href="https://asciinema.org/a/DWYAgrjSpwlejvRJQY8AHCEfD?speed=1.5" title="Click to watch the demo">
42+
<img src="./demo-preview.png" alt="pluginlab demo" />
43+
</a>
44+
</p>
45+
3046
## Install
3147

3248
```bash
@@ -44,12 +60,17 @@ pluginlab\
4460
--plugins https://topheman.github.io/webassembly-component-model-experiments/plugins/plugin_ls.wasm\
4561
--plugins https://topheman.github.io/webassembly-component-model-experiments/plugins/plugin_echo.wasm\
4662
--plugins https://topheman.github.io/webassembly-component-model-experiments/plugins/plugin_weather.wasm\
47-
--plugins https://topheman.github.io/webassembly-component-model-experiments/plugins/plugin_cat.wasm
63+
--plugins https://topheman.github.io/webassembly-component-model-experiments/plugins/plugin_cat.wasm\
64+
--allow-all
4865
```
4966

5067
Other flags:
5168

5269
- `--dir`: directory to be preopened (by default, the current directory)
70+
- `--allow-net`: allows network access to the plugins, you can specify a list of domains comma separated (by default, no network access is allowed)
71+
- `--allow-read`: allows read access to the filesystem
72+
- `--allow-write`: allows write access to the filesystem
73+
- `--allow-all`: allows all permissions (same as all the flags above), short: `-A`
5374
- `--help`: displays manual
5475
- `--debug`: run the host in debug mode (by default, the host runs in release mode)
5576

@@ -63,7 +84,8 @@ pluginlab\
6384
--plugins https://topheman.github.io/webassembly-component-model-experiments/plugins/plugin_ls.wasm\
6485
--plugins https://topheman.github.io/webassembly-component-model-experiments/plugins/plugin_echo.wasm\
6586
--plugins https://topheman.github.io/webassembly-component-model-experiments/plugins/plugin_weather.wasm\
66-
--plugins https://topheman.github.io/webassembly-component-model-experiments/plugins/plugin_cat.wasm
87+
--plugins https://topheman.github.io/webassembly-component-model-experiments/plugins/plugin_cat.wasm\
88+
--allow-all
6789
[Host] Starting REPL host...
6890
[Host] Loading REPL logic from: https://topheman.github.io/webassembly-component-model-experiments/plugins/repl_logic_guest.wasm
6991
[Host] Loading plugin: https://topheman.github.io/webassembly-component-model-experiments/plugins/plugin_greet.wasm
21.2 KB
Loading

crates/pluginlab/demo-preview.png

113 KB
Loading

crates/pluginlab/demo.gif

1.34 MB
Loading

crates/pluginlab/src/cli.rs

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
use clap::Parser;
2+
use std::path::PathBuf;
3+
4+
#[derive(Parser, Debug)]
5+
#[command(author, version, about, long_about = None)]
6+
pub struct Cli {
7+
/// Paths or URLs to WebAssembly plugin files
8+
#[arg(long)]
9+
pub plugins: Vec<String>,
10+
11+
/// Path or URL to WebAssembly REPL logic file
12+
#[arg(long)]
13+
pub repl_logic: String,
14+
15+
#[arg(long, default_value_t = false)]
16+
pub debug: bool,
17+
18+
/// Path to the directory to mount (the runtime will only have access to this directory) - default is the current directory
19+
#[arg(long, default_value = ".")]
20+
pub dir: PathBuf,
21+
22+
/// Allow network access
23+
#[arg(long, num_args = 0..=1, default_missing_value = "@")]
24+
// How it works:
25+
// no flag -> None
26+
// --allow-net -> Some("@") - because "@" is not a valid value for a domain nor an IP address
27+
// --allow-net google.com,example.com -> Some("google.com,example.com")
28+
pub allow_net: Option<String>,
29+
30+
/// Allow file system read access
31+
#[arg(long, default_value_t = false)]
32+
pub allow_read: bool,
33+
34+
/// Allow file system write access
35+
#[arg(long, default_value_t = false)]
36+
pub allow_write: bool,
37+
38+
/// Allow all permissions
39+
#[arg(
40+
short = 'A',
41+
long,
42+
default_value_t = false,
43+
conflicts_with = "allow_net",
44+
conflicts_with = "allow_read",
45+
conflicts_with = "allow_write"
46+
)]
47+
pub allow_all: bool,
48+
}

crates/pluginlab/src/engine.rs

Lines changed: 38 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1+
use crate::cli::Cli;
2+
use crate::permissions::NetworkPermissions;
13
use anyhow::Result;
24
use std::collections::HashMap;
3-
use std::path::{Path, PathBuf};
5+
use std::path::Path;
46
use wasmtime::component::{Component, Linker as ComponentLinker, ResourceTable};
57
use wasmtime::{Config, Engine, Store};
68
use wasmtime_wasi::p2::{WasiCtx, WasiCtxBuilder};
@@ -78,29 +80,52 @@ impl WasmEngine {
7880
Ok(component)
7981
}
8082

81-
pub fn build_wasi_ctx(path: &PathBuf) -> Result<WasiCtx> {
82-
let wasi_ctx = WasiCtxBuilder::new()
83-
.inherit_stdio()
84-
.inherit_args()
85-
.inherit_env()
86-
.preopened_dir(
87-
path,
88-
".",
83+
pub fn build_wasi_ctx(cli: &Cli) -> Result<WasiCtx> {
84+
let host_path = cli.dir.clone();
85+
let guest_path = ".";
86+
87+
let (dir_perms, file_perms) = if cli.allow_all || cli.allow_read && cli.allow_write {
88+
(
89+
wasmtime_wasi::DirPerms::all(),
90+
wasmtime_wasi::FilePerms::all(),
91+
)
92+
} else if cli.allow_read {
93+
(
8994
wasmtime_wasi::DirPerms::READ,
9095
wasmtime_wasi::FilePerms::READ,
91-
)?
92-
.build();
96+
)
97+
} else if cli.allow_write {
98+
(
99+
wasmtime_wasi::DirPerms::MUTATE,
100+
wasmtime_wasi::FilePerms::WRITE,
101+
)
102+
} else {
103+
(
104+
wasmtime_wasi::DirPerms::empty(),
105+
wasmtime_wasi::FilePerms::empty(),
106+
)
107+
};
108+
109+
let mut wasi_builder = WasiCtxBuilder::new();
110+
// .inherit_stdio()
111+
// .inherit_args()
112+
// .inherit_env()
113+
wasi_builder.preopened_dir(host_path, guest_path, dir_perms, file_perms)?;
114+
115+
let wasi_ctx = wasi_builder.build();
93116
Ok(wasi_ctx)
94117
}
95118

96119
/// Create a new store with WASI context
97-
pub fn create_store(&self, wasi_ctx: WasiCtx) -> Store<WasiState> {
120+
pub fn create_store(&self, wasi_ctx: WasiCtx, cli: &Cli) -> Store<WasiState> {
98121
Store::new(
99122
&self.engine,
100123
WasiState {
101124
ctx: wasi_ctx,
102125
table: ResourceTable::new(),
103-
plugin_host: PluginHost {},
126+
plugin_host: PluginHost {
127+
network_permissions: NetworkPermissions::from(cli),
128+
},
104129
repl_vars: HashMap::new(),
105130
plugins_names: Vec::new(),
106131
},

crates/pluginlab/src/helpers.rs

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,81 @@ impl StdoutHandler {
1919
repl_vars.insert("0".to_string(), result);
2020
}
2121
}
22+
23+
pub fn extract_hostname(url: &str) -> String {
24+
let url = url.trim();
25+
let url = url.trim_start_matches("http://");
26+
let url = url.trim_start_matches("https://");
27+
28+
// Find the first occurrence of '/', '?', or '#' to get just the hostname
29+
let hostname = if let Some(pos) = url.find(|c| c == '/' || c == '?' || c == '#') {
30+
&url[..pos]
31+
} else {
32+
url
33+
};
34+
35+
// Remove trailing slash if present
36+
let hostname = hostname.trim_end_matches('/');
37+
38+
hostname.to_string()
39+
}
40+
41+
#[cfg(test)]
42+
mod tests {
43+
use super::*;
44+
45+
#[test]
46+
fn test_extract_hostname() {
47+
assert_eq!(extract_hostname("https://google.com"), "google.com");
48+
assert_eq!(extract_hostname("https://google.com/"), "google.com");
49+
assert_eq!(extract_hostname("https://google.com/test"), "google.com");
50+
assert_eq!(extract_hostname("https://google.com/test/"), "google.com");
51+
assert_eq!(
52+
extract_hostname("https://google.com/test/test"),
53+
"google.com"
54+
);
55+
assert_eq!(
56+
extract_hostname("https://google.com/test/test/"),
57+
"google.com"
58+
);
59+
assert_eq!(
60+
extract_hostname("https://google.com/test/test/test"),
61+
"google.com"
62+
);
63+
assert_eq!(
64+
extract_hostname("https://google.com/test/test/test/"),
65+
"google.com"
66+
);
67+
assert_eq!(
68+
extract_hostname("https://google.com/test/test/test/test"),
69+
"google.com"
70+
);
71+
assert_eq!(
72+
extract_hostname("https://google.com?test=test"),
73+
"google.com"
74+
);
75+
assert_eq!(extract_hostname("https://google.com#test"), "google.com");
76+
assert_eq!(extract_hostname("https://192.168.1.10"), "192.168.1.10");
77+
assert_eq!(extract_hostname("https://192.168.1.10/"), "192.168.1.10");
78+
assert_eq!(
79+
extract_hostname("https://192.168.1.10/test"),
80+
"192.168.1.10"
81+
);
82+
assert_eq!(
83+
extract_hostname("https://192.168.1.10/test/"),
84+
"192.168.1.10"
85+
);
86+
assert_eq!(
87+
extract_hostname("https://192.168.1.10/test/"),
88+
"192.168.1.10"
89+
);
90+
assert_eq!(
91+
extract_hostname("https://192.168.1.10?test=test"),
92+
"192.168.1.10"
93+
);
94+
assert_eq!(
95+
extract_hostname("https://192.168.1.10#test"),
96+
"192.168.1.10"
97+
);
98+
}
99+
}

crates/pluginlab/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
pub mod api;
2+
pub mod cli;
23
mod engine;
34
pub mod helpers;
5+
pub mod permissions;
46
mod store;
57
mod wasm_host;
68

0 commit comments

Comments
 (0)