Skip to content
This repository was archived by the owner on Jul 7, 2025. It is now read-only.

Commit 0419b15

Browse files
authored
fix: do not leak username to untrusted modules (#736)
* fix: source code paths relative to module root We must not leak the filesystem structure when the code is running inside a sandboxed environment. Only path relative to the project root should be available to the module. Signed-off-by: Miroslav Bajtoš <[email protected]> * fix tests on Windows + add virtual root C:\ZINNIA or /ZINNIA Signed-off-by: Miroslav Bajtoš <[email protected]> * fixup! path separator on windows Signed-off-by: Miroslav Bajtoš <[email protected]> * fixup! tests on windows Signed-off-by: Miroslav Bajtoš <[email protected]> * fixup! build errors Signed-off-by: Miroslav Bajtoš <[email protected]> --------- Signed-off-by: Miroslav Bajtoš <[email protected]>
1 parent 2d40376 commit 0419b15

File tree

3 files changed

+100
-13
lines changed

3 files changed

+100
-13
lines changed

runtime/module_loader.rs

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,8 @@ impl ModuleLoader for ZinniaModuleLoader {
108108
ModuleLoaderError::from(JsErrorBox::generic(msg))
109109
})?;
110110

111+
let sandboxed_path: PathBuf;
112+
111113
// Check that the module path is inside the module root directory
112114
if let Some(canonical_root) = &module_root {
113115
// Resolve any symlinks inside the path to prevent modules from escaping our sandbox
@@ -120,19 +122,28 @@ impl ModuleLoader for ZinniaModuleLoader {
120122
ModuleLoaderError::from(JsErrorBox::generic(msg))
121123
})?;
122124

123-
if !canonical_module.starts_with(canonical_root) {
124-
let msg = format!(
125-
"Cannot import files outside of the module root directory.\n\
125+
let relative_path =
126+
canonical_module.strip_prefix(canonical_root).map_err(|_| {
127+
let msg = format!(
128+
"Cannot import files outside of the module root directory.\n\
126129
Root directory (canonical): {}\n\
127130
Module file path (canonical): {}\
128131
{}",
129-
canonical_root.display(),
130-
canonical_module.display(),
131-
details()
132-
);
133-
134-
return Err(ModuleLoaderError::from(JsErrorBox::generic(msg)));
135-
}
132+
canonical_root.display(),
133+
canonical_module.display(),
134+
details()
135+
);
136+
ModuleLoaderError::from(JsErrorBox::generic(msg))
137+
})?;
138+
139+
let virtual_root = if cfg!(target_os = "windows") {
140+
r"C:\ZINNIA"
141+
} else {
142+
r"/ZINNIA"
143+
};
144+
sandboxed_path = Path::new(virtual_root).join(relative_path).to_owned();
145+
} else {
146+
sandboxed_path = module_path.to_owned();
136147
};
137148

138149
// Based on https://github.com/denoland/roll-your-own-javascript-runtime
@@ -211,10 +222,20 @@ impl ModuleLoader for ZinniaModuleLoader {
211222
})?
212223
.insert(spec_str.to_string(), code.clone());
213224

225+
let sandboxed_module_specifier = ModuleSpecifier::from_file_path(&sandboxed_path)
226+
.map_err(|_| {
227+
let msg = format!(
228+
"Internal error: cannot convert relative path to module specifier: {}\n{}",
229+
sandboxed_path.display(),
230+
details()
231+
);
232+
ModuleLoaderError::from(JsErrorBox::generic(msg))
233+
})?;
234+
214235
let module = ModuleSource::new(
215236
module_type,
216237
ModuleSourceCode::String(code.into()),
217-
&module_specifier,
238+
&sandboxed_module_specifier,
218239
None,
219240
);
220241

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
console.log("import.meta.filename:", import.meta.filename);
2+
console.log("import.meta.dirname:", import.meta.dirname);
3+
console.log("error stack:", new Error().stack.split("\n")[1].trim());

runtime/tests/runtime_integration_tests.rs

Lines changed: 65 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
// ./target/debug/zinnia run runtime/tests/js/timers_tests.js
44
// Most of the tests should pass on Deno too!
55
// deno run runtime/tests/js/timers_tests.js
6-
use std::path::PathBuf;
6+
use std::path::{Path, PathBuf};
77
use std::rc::Rc;
88

99
use anyhow::{anyhow, Context};
@@ -94,8 +94,71 @@ js_tests!(ipfs_retrieval_tests);
9494
test_runner_tests!(passing_tests);
9595
test_runner_tests!(failing_tests expect_failure);
9696

97+
#[tokio::test]
98+
async fn source_code_paths_when_no_module_root() -> Result<(), AnyError> {
99+
let (activities, run_error) =
100+
run_js_test_file_with_module_root("print_source_code_paths.js", None).await?;
101+
if let Some(err) = run_error {
102+
return Err(err);
103+
}
104+
105+
let base_dir = get_base_dir();
106+
let dirname = base_dir.to_str().unwrap().to_string();
107+
let filename = Path::join(&base_dir, "print_source_code_paths.js").to_owned();
108+
let filename = filename.to_str().unwrap().to_string();
109+
let module_url = ModuleSpecifier::from_file_path(&filename).unwrap();
110+
111+
assert_eq!(
112+
[
113+
format!("import.meta.filename: {filename}"),
114+
format!("import.meta.dirname: {dirname}"),
115+
format!("error stack: at {module_url}:3:29"),
116+
]
117+
.map(|msg| { format!("console.info: {msg}\n") }),
118+
activities.as_slice(),
119+
);
120+
Ok(())
121+
}
122+
123+
#[tokio::test]
124+
async fn source_code_paths_when_inside_module_root() -> Result<(), AnyError> {
125+
let module_root = Some(PathBuf::from(env!("CARGO_MANIFEST_DIR")));
126+
let (activities, run_error) =
127+
run_js_test_file_with_module_root("print_source_code_paths.js", module_root).await?;
128+
if let Some(err) = run_error {
129+
return Err(err);
130+
}
131+
132+
let expected = if cfg!(target_os = "windows") {
133+
[
134+
r"import.meta.filename: C:\ZINNIA\tests\js\print_source_code_paths.js",
135+
r"import.meta.dirname: C:\ZINNIA\tests\js",
136+
"error stack: at file:///C:/ZINNIA/tests/js/print_source_code_paths.js:3:29",
137+
]
138+
} else {
139+
[
140+
"import.meta.filename: /ZINNIA/tests/js/print_source_code_paths.js",
141+
"import.meta.dirname: /ZINNIA/tests/js",
142+
"error stack: at file:///ZINNIA/tests/js/print_source_code_paths.js:3:29",
143+
]
144+
};
145+
146+
assert_eq!(
147+
expected.map(|msg| { format!("console.info: {msg}\n") }),
148+
activities.as_slice(),
149+
);
150+
Ok(())
151+
}
152+
97153
// Run all tests in a single JS file
98154
async fn run_js_test_file(name: &str) -> Result<(Vec<String>, Option<AnyError>), AnyError> {
155+
run_js_test_file_with_module_root(name, None).await
156+
}
157+
158+
async fn run_js_test_file_with_module_root(
159+
name: &str,
160+
module_root: Option<PathBuf>,
161+
) -> Result<(Vec<String>, Option<AnyError>), AnyError> {
99162
let _ = env_logger::builder().is_test(true).try_init();
100163

101164
let mut full_path = get_base_dir();
@@ -110,7 +173,7 @@ async fn run_js_test_file(name: &str) -> Result<(Vec<String>, Option<AnyError>),
110173
format!("zinnia_runtime_tests/{}", env!("CARGO_PKG_VERSION")),
111174
reporter.clone(),
112175
helpers::lassie_daemon(),
113-
None,
176+
module_root,
114177
);
115178
let run_result = run_js_module(&main_module, &config).await;
116179
let events = reporter.events.take();

0 commit comments

Comments
 (0)