cache current binary path much sooner (#45)

* use ctor to cache starting executable

* clean up symlink checking logic

* changefile

* use wrapper for the static, put it in tauri_utils

* cargo +nightly fmt

* add license header to `StartingBinary`

* fix clippy warning

* fix: test

* simplify macOS dangerous flag detection

* update restart test to allow expected failure on macOS

* finish documentation

Co-authored-by: Lucas Nogueira <lucas@tauri.studio>
This commit is contained in:
chip 2022-02-01 18:30:52 -08:00 committed by Lucas Nogueira
parent b82e2b5cf7
commit 7c3db7a381
No known key found for this signature in database
GPG Key ID: C54D79C4018F1D19
14 changed files with 584 additions and 582 deletions

View File

@ -0,0 +1,6 @@
---
tauri: patch
tauri-utils: patch
---
The path returned from `tauri::api::process::current_binary` is now cached when loading the binary.

View File

@ -30,6 +30,7 @@ aes-gcm = { version = "0.9", optional = true }
ring = { version = "0.16", optional = true, features = ["std"] }
once_cell = { version = "1.8", optional = true }
serialize-to-javascript = { git = "https://github.com/chippers/serialize-to-javascript" }
ctor = "0.1"
[target."cfg(target_os = \"linux\")".dependencies]
heck = "0.4"
@ -39,3 +40,4 @@ build = [ "proc-macro2", "quote" ]
compression = [ "zstd" ]
schema = ["schemars"]
isolation = [ "aes-gcm", "ring", "once_cell" ]
process-relaunch-dangerous-allow-symlink-macos = []

View File

@ -1260,6 +1260,12 @@ pub struct ProcessAllowlistConfig {
/// Enables the relaunch API.
#[serde(default)]
pub relaunch: bool,
/// Dangerous option that allows macOS to relaunch even if the binary contains a symlink.
///
/// This is due to macOS having less symlink protection. Highly recommended to not set this flag
/// unless you have a very specific reason too, and understand the implications of it.
#[serde(default)]
pub relaunch_dangerous_allow_symlink_macos: bool,
/// Enables the exit API.
#[serde(default)]
pub exit: bool,
@ -1270,6 +1276,7 @@ impl Allowlist for ProcessAllowlistConfig {
let allowlist = Self {
all: false,
relaunch: true,
relaunch_dangerous_allow_symlink_macos: false,
exit: true,
};
let mut features = allowlist.to_features();
@ -1283,6 +1290,12 @@ impl Allowlist for ProcessAllowlistConfig {
} else {
let mut features = Vec::new();
check_feature!(self, features, relaunch, "process-relaunch");
check_feature!(
self,
features,
relaunch_dangerous_allow_symlink_macos,
"process-relaunch-dangerous-allow-symlink-macos"
);
check_feature!(self, features, exit, "process-exit");
features
}

View File

@ -68,14 +68,15 @@ impl Default for Env {
// an AppImage is mounted to `/$TEMPDIR/.mount_${appPrefix}${hash}`
// see https://github.com/AppImage/AppImageKit/blob/1681fd84dbe09c7d9b22e13cdb16ea601aa0ec47/src/runtime.c#L501
// note that it is safe to use `std::env::current_exe` here since we just loaded an AppImage.
if !std::env::current_exe()
let is_temp = std::env::current_exe()
.map(|p| {
p.display()
.to_string()
.starts_with(&format!("{}/.mount_", std::env::temp_dir().display()))
})
.unwrap_or(true)
{
.unwrap_or(true);
if !is_temp {
panic!("`APPDIR` or `APPIMAGE` environment variable found but this application was not detected as an AppImage; this might be a security issue.");
}
}

View File

@ -8,16 +8,82 @@ use std::path::{PathBuf, MAIN_SEPARATOR};
use crate::{Env, PackageInfo};
/// Gets the path to the current executable, resolving symbolic links for security reasons.
mod starting_binary;
/// Retrieves the currently running binary's path, taking into account security considerations.
///
/// See https://doc.rust-lang.org/std/env/fn.current_exe.html#security for
/// an example of what to be careful of when using `current_exe` output.
/// The path is cached as soon as possible (before even `main` runs) and that value is returned
/// repeatedly instead of fetching the path every time. It is possible for the path to not be found,
/// or explicitly disabled (see following macOS specific behavior).
///
/// We canonicalize the path we received from `current_exe` to resolve any
/// soft links. it avoids the usual issue of needing the file to exist at
/// the passed path because a valid `current_exe` result should always exist.
/// # Platform-specific behavior
///
/// On `macOS`, this function will return an error if the original path contained any symlinks
/// due to less protection on macOS regarding symlinks. This behavior can be disabled by setting the
/// `process-relaunch-dangerous-allow-symlink-macos` feature, although it is *highly discouraged*.
///
/// # Security
///
/// If the above platform-specific behavior does **not** take place, this function uses the
/// following resolution.
///
/// We canonicalize the path we received from [`std::env::current_exe`] to resolve any soft links.
/// This avoids the usual issue of needing the file to exist at the passed path because a valid
/// current executable result for our purpose should always exist. Notably,
/// [`std::env::current_exe`] also has a security section that goes over a theoretical attack using
/// hard links. Let's cover some specific topics that relate to different ways an attacker might
/// try to trick this function into returning the wrong binary path.
///
/// ## Symlinks ("Soft Links")
///
/// [`std::path::Path::canonicalize`] is used to resolve symbolic links to the original path,
/// including nested symbolic links (`link2 -> link1 -> bin`). On macOS, any results that include
/// a symlink are rejected by default due to lesser symlink protections. This can be disabled,
/// **although discouraged**, with the `process-relaunch-dangerous-allow-symlink-macos` feature.
///
/// ## Hard Links
///
/// A [Hard Link] is a named entry that points to a file in the file system.
/// On most systems, this is what you would think of as a "file". The term is
/// used on filesystems that allow multiple entries to point to the same file.
/// The linked [Hard Link] Wikipedia page provides a decent overview.
///
/// In short, unless the attacker was able to create the link with elevated
/// permissions, it should generally not be possible for them to hard link
/// to a file they do not have permissions to - with exception to possible
/// operating system exploits.
///
/// There are also some platform-specific information about this below.
///
/// ### Windows
///
/// Windows requires a permission to be set for the user to create a symlink
/// or a hard link, regardless of ownership status of the target. Elevated
/// permissions users have the ability to create them.
///
/// ### macOS
///
/// macOS allows for the creation of symlinks and hard links to any file.
/// Accessing through those links will fail if the user who owns the links
/// does not have the proper permissions on the original file.
///
/// ### Linux
///
/// Linux allows for the creation of symlinks to any file. Accessing the
/// symlink will fail if the user who owns the symlink does not have the
/// proper permissions on the original file.
///
/// Linux additionally provides a kernel hardening feature since version
/// 3.6 (30 September 2012). Most distributions since then have enabled
/// the protection (setting `fs.protected_hardlinks = 1`) by default, which
/// means that a vast majority of desktop Linux users should have it enabled.
/// **The feature prevents the creation of hardlinks that the user does not own
/// or have read/write access to.** [See the patch that enabled this].
///
/// [Hard Link]: https://en.wikipedia.org/wiki/Hard_link
/// [See the patch that enabled this]: https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=800179c9b8a1e796e441674776d11cd4c05d61d7
pub fn current_exe() -> std::io::Result<PathBuf> {
std::env::current_exe().and_then(|path| path.canonicalize())
self::starting_binary::STARTING_BINARY.cloned()
}
/// Try to determine the current target triple.

View File

@ -0,0 +1,76 @@
// Copyright 2019-2021 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use ctor::ctor;
use std::{
io::{Error, ErrorKind, Result},
path::{Path, PathBuf},
};
/// A cached version of the current binary using [`ctor`] to cache it before even `main` runs.
#[ctor]
#[used]
pub(super) static STARTING_BINARY: StartingBinary = StartingBinary::new();
/// Represents a binary path that was cached when the program was loaded.
pub(super) struct StartingBinary(std::io::Result<PathBuf>);
impl StartingBinary {
/// Find the starting executable as safely as possible.
fn new() -> Self {
// see notes on current_exe() for security implications
let dangerous_path = match std::env::current_exe() {
Ok(dangerous_path) => dangerous_path,
error @ Err(_) => return Self(error),
};
// note: this only checks symlinks on problematic platforms, see implementation below
if let Some(symlink) = Self::has_symlink(&dangerous_path) {
return Self(Err(Error::new(
ErrorKind::InvalidData,
format!("StartingBinary found current_exe() that contains a symlink on a non-allowed platform: {}", symlink.display()),
)));
}
// we canonicalize the path to resolve any symlinks to the real exe path
Self(dangerous_path.canonicalize())
}
/// A clone of the [`PathBuf`] found to be the starting path.
///
/// Because [`Error`] is not clone-able, it is recreated instead.
pub(super) fn cloned(&self) -> Result<PathBuf> {
self
.0
.as_ref()
.map(Clone::clone)
.map_err(|e| Error::new(e.kind(), e.to_string()))
}
/// We only care about checking this on macOS currently, as it has the least symlink protections.
#[cfg(any(
not(target_os = "macos"),
feature = "process-relaunch-dangerous-allow-symlink-macos"
))]
fn has_symlink(_: &Path) -> Option<&Path> {
None
}
/// We only care about checking this on macOS currently, as it has the least symlink protections.
#[cfg(all(
target_os = "macos",
not(feature = "process-relaunch-dangerous-allow-symlink-macos")
))]
fn has_symlink(path: &Path) -> Option<&Path> {
path.ancestors().find(|ancestor| {
matches!(
ancestor
.symlink_metadata()
.as_ref()
.map(std::fs::Metadata::is_symlink),
Ok(true)
)
})
}
}

View File

@ -101,7 +101,7 @@ tokio-test = "0.4.2"
tokio = { version = "1.15", features = [ "full" ] }
[target."cfg(windows)".dev-dependencies.windows]
version = "0.29.0"
version = "0.30.0"
features = [
"Win32_Foundation",
]
@ -172,6 +172,7 @@ path-all = []
process-all = ["process-relaunch", "process-exit"]
process-exit = []
process-relaunch = []
process-relaunch-dangerous-allow-symlink-macos = ["tauri-utils/process-relaunch-dangerous-allow-symlink-macos"]
protocol-all = ["protocol-asset"]
protocol-asset = []
shell-all = ["shell-execute", "shell-sidecar", "shell-open"]

View File

@ -90,6 +90,7 @@ fn main() {
// process
process_all: { any(api_all, feature = "process-all") },
process_relaunch: { any(protocol_all, feature = "process-relaunch") },
process_relaunch_dangerous_allow_symlink_macos: { feature = "process-relaunch-dangerous-allow-symlink-macos" },
process_exit: { any(protocol_all, feature = "process-exit") },
// clipboard

View File

@ -17,83 +17,46 @@ pub use command::*;
/// Finds the current running binary's path.
///
/// With exception to any following platform-specific behavior, the path is cached as soon as
/// possible, and then used repeatedly instead of querying for a new path every time this function
/// is called.
///
/// # Platform-specific behavior
///
/// On the `Linux` platform, this function will also **attempt** to detect if
/// it's currently running from a valid [AppImage] and use that path instead.
/// ## Linux
///
/// On Linux, this function will **attempt** to detect if it's currently running from a
/// valid [AppImage] and use that path instead.
///
/// ## macOS
///
/// On `macOS`, this function will return an error if the original path contained any symlinks
/// due to less protection on macOS regarding symlinks. This behavior can be disabled by setting the
/// `process-relaunch-dangerous-allow-symlink-macos` feature, although it is *highly discouraged*.
///
/// # Security
///
/// If the above Platform-specific behavior does not take place, this function
/// uses [`std::env::current_exe`]. Notably, it also has a security section
/// that goes over a theoretical attack using hard links. Let's cover some
/// specific topics that relate to different ways an attacker might try to
/// trick this function into returning the wrong binary path.
///
/// ## Symlinks ("Soft Links")
///
/// [`std::path::Path::canonicalize`] is used to resolve symbolic links to the
/// original path, including nested symbolic links (`link2 -> link1 -> bin`).
///
/// ## Hard Links
///
/// A [Hard Link] is a named entry that points to a file in the file system.
/// On most systems, this is what you would think of as a "file". The term is
/// used on filesystems that allow multiple entries to point to the same file.
/// The linked [Hard Link] Wikipedia page provides a decent overview.
///
/// In short, unless the attacker was able to create the link with elevated
/// permissions, it should generally not be possible for them to hard link
/// to a file they do not have permissions to - with exception to possible
/// operating system exploits.
///
/// There are also some platform-specific information about this below.
///
/// ### Windows
///
/// Windows requires a permission to be set for the user to create a symlink
/// or a hard link, regardless of ownership status of the target. Elevated
/// permissions users have the ability to create them.
///
/// ### macOS
///
/// macOS allows for the creation of symlinks and hard links to any file.
/// Accessing through those links will fail if the user who owns the links
/// does not have the proper permissions on the original file.
///
/// ### Linux
///
/// Linux allows for the creation of symlinks to any file. Accessing the
/// symlink will fail if the user who owns the symlink does not have the
/// proper permissions on the original file.
///
/// Linux additionally provides a kernel hardening feature since version
/// 3.6 (30 September 2012). Most distributions since then have enabled
/// the protection (setting `fs.protected_hardlinks = 1`) by default, which
/// means that a vast majority of desktop Linux users should have it enabled.
/// **The feature prevents the creation of hardlinks that the user does not own
/// or have read/write access to.** [See the patch that enabled this.](https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=800179c9b8a1e796e441674776d11cd4c05d61d7)
/// See [`tauri_utils::platform::current_exe`] for possible security implications.
///
/// [AppImage]: https://appimage.org/
/// [Hard Link]: https://en.wikipedia.org/wiki/Hard_link
#[allow(unused_variables)]
pub fn current_binary(env: &Env) -> Option<PathBuf> {
pub fn current_binary(_env: &Env) -> std::io::Result<PathBuf> {
// if we are running from an AppImage, we ONLY want the set AppImage path
#[cfg(target_os = "linux")]
if let Some(app_image_path) = &env.appimage {
return Some(PathBuf::from(app_image_path));
if let Some(app_image_path) = &_env.appimage {
return Ok(PathBuf::from(app_image_path));
}
tauri_utils::platform::current_exe().ok()
tauri_utils::platform::current_exe()
}
/// Restarts the process.
/// Restarts the currently running binary.
///
/// See [`current_binary`] for the possible security implications.
/// See [`current_binary`] for platform specific behavior, and
/// [`tauri_utils::platform::current_exe`] for possible security implications.
pub fn restart(env: &Env) {
use std::process::{exit, Command};
if let Some(path) = current_binary(env) {
if let Ok(path) = current_binary(env) {
Command::new(path)
.spawn()
.expect("application failed to start");

View File

@ -146,13 +146,15 @@ impl Scope {
}
})
.collect(),
(Some(list), arg) if arg.is_empty() && list.iter().all(ShellScopeAllowedArg::is_fixed) => list
(Some(list), arg) if arg.is_empty() && list.iter().all(ShellScopeAllowedArg::is_fixed) => {
list
.iter()
.map(|arg| match arg {
ShellScopeAllowedArg::Fixed(fixed) => Ok(fixed.to_string()),
_ => unreachable!(),
})
.collect(),
.collect()
}
(Some(list), _) if list.is_empty() => Err(ScopeError::InvalidInput(command_name.into())),
(Some(_), _) => Err(ScopeError::InvalidInput(command_name.into())),
}?;

View File

@ -37,6 +37,14 @@ fn compile_restart_test_binary() -> io::Result<PathBuf> {
cargo.arg("--manifest-path");
cargo.arg(project.join("Cargo.toml"));
// enable the dangerous macos flag on tauri if the test runner has the feature enabled
if cfg!(feature = "process-relaunch-dangerous-allow-symlink-macos") {
cargo.args([
"--features",
"tauri/process-relaunch-dangerous-allow-symlink-macos",
]);
}
let status = cargo.status()?;
if !status.success() {
return Err(io::Error::new(
@ -81,14 +89,32 @@ fn symlink_runner(create_symlinks: impl Fn(&Path) -> io::Result<Symlink>) -> Res
// add the restart parameter so that the invocation will call tauri::api::process::restart
cmd.arg("restart");
// gather the output into a string
let output = String::from_utf8(cmd.output()?.stdout)?;
let output = cmd.output()?;
// run destructors to prevent resource leaking if the assertion fails
// run `TempDir` destructors to prevent resource leaking if the assertion fails
drop(temp);
if output.status.success() {
// gather the output into a string
let stdout = String::from_utf8(output.stdout)?;
// we expect the output to be the bin path, twice
assert_eq!(output, format!("{bin}\n{bin}\n", bin = bin.display()));
assert_eq!(stdout, format!("{bin}\n{bin}\n", bin = bin.display()));
} else if cfg!(all(
target_os = "macos",
not(feature = "process-relaunch-dangerous-allow-symlink-macos")
)) {
// we expect this to fail on macOS without the dangerous symlink flag set
let stderr = String::from_utf8(output.stderr)?;
// make sure it's the error that we expect
assert!(stderr.contains(
"StartingBinary found current_exe() that contains a symlink on a non-allowed platform"
));
} else {
// we didn't expect the program to fail in this configuration, just panic
panic!("restart integration test runner failed for unknown reason");
}
}
Ok(())

File diff suppressed because it is too large Load Diff

View File

@ -20,7 +20,7 @@ version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b613b8e1e3cf911a086f53f03bf286f52fd7a7258e4fa606f0ef220d39d8877"
dependencies = [
"generic-array 0.14.5",
"generic-array 0.14.4",
]
[[package]]
@ -153,7 +153,7 @@ version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1d36a02058e76b040de25a4464ba1c80935655595b661505c8b39b664828b95"
dependencies = [
"generic-array 0.14.5",
"generic-array 0.14.4",
]
[[package]]
@ -454,6 +454,16 @@ dependencies = [
"syn",
]
[[package]]
name = "ctor"
version = "0.1.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccc0a48a9b826acdf4028595adc9db92caea352f7af011a3034acd172a52a0aa"
dependencies = [
"quote",
"syn",
]
[[package]]
name = "ctr"
version = "0.8.0"
@ -548,7 +558,7 @@ version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066"
dependencies = [
"generic-array 0.14.5",
"generic-array 0.14.4",
]
[[package]]
@ -559,7 +569,7 @@ checksum = "b697d66081d42af4fba142d56918a3cb21dc8eb63372c6b85d14f44fb9c5979b"
dependencies = [
"block-buffer 0.10.0",
"crypto-common",
"generic-array 0.14.5",
"generic-array 0.14.4",
"subtle",
]
@ -2384,7 +2394,7 @@ dependencies = [
"regex",
"serde",
"serde_json",
"sha2 0.10.1",
"sha2 0.9.9",
"strsim",
"tar",
"tempfile",
@ -2445,6 +2455,7 @@ version = "1.0.0-beta.3"
dependencies = [
"aes-gcm",
"base64",
"ctor",
"heck",
"html5ever",
"kuchiki",
@ -2657,7 +2668,7 @@ version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f214e8f697e925001e66ec2c6e37a4ef93f0f78c2eed7814394e10c62025b05"
dependencies = [
"generic-array 0.14.5",
"generic-array 0.14.4",
"subtle",
]

View File

@ -65,9 +65,7 @@
"removeDir": false,
"removeFile": false,
"renameFile": false,
"scope": [
"$APP/**"
],
"scope": [],
"writeFile": false
},
"globalShortcut": {
@ -90,14 +88,13 @@
"process": {
"all": false,
"exit": false,
"relaunch": false
"relaunch": false,
"relaunchDangerousAllowSymlinkMacos": false
},
"protocol": {
"all": false,
"asset": false,
"assetScope": [
"$APP/**"
]
"assetScope": []
},
"shell": {
"all": false,
@ -244,9 +241,7 @@
"removeDir": false,
"removeFile": false,
"renameFile": false,
"scope": [
"$APP/**"
],
"scope": [],
"writeFile": false
},
"allOf": [
@ -317,7 +312,8 @@
"default": {
"all": false,
"exit": false,
"relaunch": false
"relaunch": false,
"relaunchDangerousAllowSymlinkMacos": false
},
"allOf": [
{
@ -330,9 +326,7 @@
"default": {
"all": false,
"asset": false,
"assetScope": [
"$APP/**"
]
"assetScope": []
},
"allOf": [
{
@ -1003,9 +997,7 @@
},
"scope": {
"description": "The access scope for the filesystem APIs.",
"default": [
"$APP/**"
],
"default": [],
"allOf": [
{
"$ref": "#/definitions/FsAllowlistScope"
@ -1066,7 +1058,7 @@
"additionalProperties": false
},
"HttpAllowlistScope": {
"description": "HTTP API scope definition. It is a list of URLs that can be accessed by the webview when using the HTTP APIs.",
"description": "HTTP API scope definition. It is a list of URLs that can be accessed by the webview when using the HTTP APIs. The URL path is matched against the request URL using a glob pattern.",
"type": "array",
"items": {
"type": "string",
@ -1259,6 +1251,11 @@
"description": "Enables the relaunch API.",
"default": false,
"type": "boolean"
},
"relaunchDangerousAllowSymlinkMacos": {
"description": "Dangerous option that allows macOS to relaunch even if the binary contains a symlink.\n\nThis is due to macOS having less symlink protection. Highly recommended to not set this flag unless you have a very specific reason too, and understand the implications of it.",
"default": false,
"type": "boolean"
}
},
"additionalProperties": false
@ -1279,9 +1276,7 @@
},
"assetScope": {
"description": "The access scope for the asset protocol.",
"default": [
"$APP/**"
],
"default": [],
"allOf": [
{
"$ref": "#/definitions/FsAllowlistScope"
@ -1505,9 +1500,7 @@
"removeDir": false,
"removeFile": false,
"renameFile": false,
"scope": [
"$APP/**"
],
"scope": [],
"writeFile": false
},
"globalShortcut": {
@ -1530,14 +1523,13 @@
"process": {
"all": false,
"exit": false,
"relaunch": false
"relaunch": false,
"relaunchDangerousAllowSymlinkMacos": false
},
"protocol": {
"all": false,
"asset": false,
"assetScope": [
"$APP/**"
]
"assetScope": []
},
"shell": {
"all": false,