summaryrefslogtreecommitdiff
path: root/runtime/ops
diff options
context:
space:
mode:
authorDavid Sherret <dsherret@users.noreply.github.com>2024-09-04 14:51:24 +0200
committerGitHub <noreply@github.com>2024-09-04 14:51:24 +0200
commit74fc66da110ec20d12751e7a0922cea300314399 (patch)
treeb0b057b7539b506b8db39287cd799e7c9cbd526f /runtime/ops
parent334c842392e2587b8ca1d7cc7cc7d9231fc15286 (diff)
fix: lock down allow-run permissions more (#25370)
`--allow-run` even with an allow list has essentially been `--allow-all`... this locks it down more. 1. Resolves allow list for `--allow-run=` on startup to an absolute path, then uses these paths when evaluating if a command can execute. Also, adds these paths to `--deny-write` 1. Resolves the environment (cwd and env vars) before evaluating permissions and before executing a command. Then uses this environment to evaluate the permissions and then evaluate the command.
Diffstat (limited to 'runtime/ops')
-rw-r--r--runtime/ops/process.rs237
1 files changed, 160 insertions, 77 deletions
diff --git a/runtime/ops/process.rs b/runtime/ops/process.rs
index 11e439051..eb53151ce 100644
--- a/runtime/ops/process.rs
+++ b/runtime/ops/process.rs
@@ -21,6 +21,9 @@ use serde::Deserialize;
use serde::Serialize;
use std::borrow::Cow;
use std::cell::RefCell;
+use std::collections::HashMap;
+use std::path::Path;
+use std::path::PathBuf;
use std::process::ExitStatus;
use std::rc::Rc;
use tokio::process::Command;
@@ -228,63 +231,15 @@ fn create_command(
mut args: SpawnArgs,
api_name: &str,
) -> Result<CreateCommand, AnyError> {
- fn get_requires_allow_all_env_var(args: &SpawnArgs) -> Option<Cow<str>> {
- fn requires_allow_all(key: &str) -> bool {
- let key = key.trim();
- // we could be more targted here, but there are quite a lot of
- // LD_* and DYLD_* env variables
- key.starts_with("LD_") || key.starts_with("DYLD_")
- }
-
- /// Checks if the user set this env var to an empty
- /// string in order to clear it.
- fn args_has_empty_env_value(args: &SpawnArgs, key_name: &str) -> bool {
- args
- .env
- .iter()
- .find(|(k, _)| k == key_name)
- .map(|(_, v)| v.trim().is_empty())
- .unwrap_or(false)
- }
-
- if let Some((key, _)) = args
- .env
- .iter()
- .find(|(k, v)| requires_allow_all(k) && !v.trim().is_empty())
- {
- return Some(key.into());
- }
-
- if !args.clear_env {
- if let Some((key, _)) = std::env::vars().find(|(k, v)| {
- requires_allow_all(k)
- && !v.trim().is_empty()
- && !args_has_empty_env_value(args, k)
- }) {
- return Some(key.into());
- }
- }
-
- None
- }
-
- {
- let permissions = state.borrow_mut::<PermissionsContainer>();
- permissions.check_run(&args.cmd, api_name)?;
- if permissions.check_run_all(api_name).is_err() {
- // error the same on all platforms
- if let Some(name) = get_requires_allow_all_env_var(&args) {
- // we don't allow users to launch subprocesses with any LD_ or DYLD_*
- // env vars set because this allows executing code (ex. LD_PRELOAD)
- return Err(deno_core::error::custom_error(
- "PermissionDenied",
- format!("Requires --allow-all permissions to spawn subprocess with {} environment variable.", name)
- ));
- }
- }
- }
-
- let mut command = std::process::Command::new(args.cmd);
+ let (cmd, run_env) = compute_run_cmd_and_check_permissions(
+ &args.cmd,
+ args.cwd.as_deref(),
+ &args.env,
+ args.clear_env,
+ state,
+ api_name,
+ )?;
+ let mut command = std::process::Command::new(cmd);
#[cfg(windows)]
if args.windows_raw_arguments {
@@ -298,14 +253,9 @@ fn create_command(
#[cfg(not(windows))]
command.args(args.args);
- if let Some(cwd) = args.cwd {
- command.current_dir(cwd);
- }
-
- if args.clear_env {
- command.env_clear();
- }
- command.envs(args.env);
+ command.current_dir(run_env.cwd);
+ command.env_clear();
+ command.envs(run_env.envs);
#[cfg(unix)]
if let Some(gid) = args.gid {
@@ -554,6 +504,133 @@ fn close_raw_handle(handle: deno_io::RawBiPipeHandle) {
}
}
+fn compute_run_cmd_and_check_permissions(
+ arg_cmd: &str,
+ arg_cwd: Option<&str>,
+ arg_envs: &[(String, String)],
+ arg_clear_env: bool,
+ state: &mut OpState,
+ api_name: &str,
+) -> Result<(PathBuf, RunEnv), AnyError> {
+ let run_env = compute_run_env(arg_cwd, arg_envs, arg_clear_env)
+ .with_context(|| format!("Failed to spawn '{}'", arg_cmd))?;
+ let cmd = resolve_cmd(arg_cmd, &run_env)
+ .with_context(|| format!("Failed to spawn '{}'", arg_cmd))?;
+ check_run_permission(state, &cmd, &run_env, api_name)?;
+ Ok((cmd, run_env))
+}
+
+struct RunEnv {
+ envs: HashMap<String, String>,
+ cwd: PathBuf,
+}
+
+/// Computes the current environment, which will then be used to inform
+/// permissions and finally spawning. This is very important to compute
+/// ahead of time so that the environment used to verify permissions is
+/// the same environment used to spawn the sub command. This protects against
+/// someone doing timing attacks by changing the environment on a worker.
+fn compute_run_env(
+ arg_cwd: Option<&str>,
+ arg_envs: &[(String, String)],
+ arg_clear_env: bool,
+) -> Result<RunEnv, AnyError> {
+ #[allow(clippy::disallowed_methods)]
+ let cwd = std::env::current_dir().context("failed resolving cwd")?;
+ let cwd = arg_cwd
+ .map(|cwd_arg| resolve_path(cwd_arg, &cwd))
+ .unwrap_or(cwd);
+ let envs = if arg_clear_env {
+ arg_envs.iter().cloned().collect()
+ } else {
+ let mut envs = std::env::vars().collect::<HashMap<_, _>>();
+ for (key, value) in arg_envs {
+ envs.insert(key.clone(), value.clone());
+ }
+ envs
+ };
+ Ok(RunEnv { envs, cwd })
+}
+
+fn resolve_cmd(cmd: &str, env: &RunEnv) -> Result<PathBuf, AnyError> {
+ let is_path = cmd.contains('/');
+ #[cfg(windows)]
+ let is_path = is_path || cmd.contains('\\') || Path::new(&cmd).is_absolute();
+ if is_path {
+ Ok(resolve_path(cmd, &env.cwd))
+ } else {
+ let path = env.envs.get("PATH").or_else(|| {
+ if cfg!(windows) {
+ env.envs.iter().find_map(|(k, v)| {
+ if k.to_uppercase() == "PATH" {
+ Some(v)
+ } else {
+ None
+ }
+ })
+ } else {
+ None
+ }
+ });
+ match which::which_in(cmd, path, &env.cwd) {
+ Ok(cmd) => Ok(cmd),
+ Err(which::Error::CannotFindBinaryPath) => {
+ Err(std::io::Error::from(std::io::ErrorKind::NotFound).into())
+ }
+ Err(err) => Err(err.into()),
+ }
+ }
+}
+
+fn resolve_path(path: &str, cwd: &Path) -> PathBuf {
+ deno_core::normalize_path(cwd.join(path))
+}
+
+fn check_run_permission(
+ state: &mut OpState,
+ cmd: &Path,
+ run_env: &RunEnv,
+ api_name: &str,
+) -> Result<(), AnyError> {
+ let permissions = state.borrow_mut::<PermissionsContainer>();
+ if !permissions.query_run_all(api_name) {
+ // error the same on all platforms
+ let env_var_names = get_requires_allow_all_env_vars(run_env);
+ if !env_var_names.is_empty() {
+ // we don't allow users to launch subprocesses with any LD_ or DYLD_*
+ // env vars set because this allows executing code (ex. LD_PRELOAD)
+ return Err(deno_core::error::custom_error(
+ "PermissionDenied",
+ format!(
+ "Requires --allow-all permissions to spawn subprocess with {} environment variable{}.",
+ env_var_names.join(", "),
+ if env_var_names.len() != 1 { "s" } else { "" }
+ )
+ ));
+ }
+ permissions.check_run(cmd, api_name)?;
+ }
+ Ok(())
+}
+
+fn get_requires_allow_all_env_vars(env: &RunEnv) -> Vec<&str> {
+ fn requires_allow_all(key: &str) -> bool {
+ let key = key.trim();
+ // we could be more targted here, but there are quite a lot of
+ // LD_* and DYLD_* env variables
+ key.starts_with("LD_") || key.starts_with("DYLD_")
+ }
+
+ let mut found_envs = env
+ .envs
+ .iter()
+ .filter(|(k, v)| requires_allow_all(k) && !v.trim().is_empty())
+ .map(|(k, _)| k.as_str())
+ .collect::<Vec<_>>();
+ found_envs.sort();
+ found_envs
+}
+
#[op2]
#[serde]
fn op_spawn_child(
@@ -634,6 +711,8 @@ fn op_spawn_kill(
}
mod deprecated {
+ use deno_core::anyhow;
+
use super::*;
#[derive(Deserialize)]
@@ -681,20 +760,24 @@ mod deprecated {
#[serde] run_args: RunArgs,
) -> Result<RunInfo, AnyError> {
let args = run_args.cmd;
- state
- .borrow_mut::<PermissionsContainer>()
- .check_run(&args[0], "Deno.run()")?;
- let env = run_args.env;
- let cwd = run_args.cwd;
-
- let mut c = Command::new(args.first().unwrap());
- (1..args.len()).for_each(|i| {
- let arg = args.get(i).unwrap();
+ let cmd = args.first().ok_or_else(|| anyhow::anyhow!("Missing cmd"))?;
+ let (cmd, run_env) = compute_run_cmd_and_check_permissions(
+ cmd,
+ run_args.cwd.as_deref(),
+ &run_args.env,
+ /* clear env */ false,
+ state,
+ "Deno.run()",
+ )?;
+
+ let mut c = Command::new(cmd);
+ for arg in args.iter().skip(1) {
c.arg(arg);
- });
- cwd.map(|d| c.current_dir(d));
+ }
+ c.current_dir(run_env.cwd);
- for (key, value) in &env {
+ c.env_clear();
+ for (key, value) in run_env.envs {
c.env(key, value);
}