blob: 947196e3fd5619366a09906c7854180b9ef2b2fa [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
use super::{Access, Compiled, Policy, Rule};
use crate::{Error, path};
use std::{
collections::BTreeSet,
fs, io,
path::{Path, PathBuf},
};
impl Compiled {
/// Materialize existing deny-glob matches for native mechanisms that cannot
/// match names dynamically. This is a launch snapshot, never a replacement
/// for the policy used by Host file operations or future launches.
///
/// Scans include hidden/ignored entries but do not follow directory links.
/// A matching link denies its canonical target too. Incomplete scans fail;
/// no partial result is returned and no filesystem changes occur here.
pub fn process_snapshot(&self) -> Result<Policy, Error> {
if self.policy.deny_globs.is_empty() {
return Ok(self.policy.clone());
}
let mut roots = BTreeSet::new();
let mut denied = BTreeSet::new();
for pattern in &self.policy.deny_globs {
let mut root = PathBuf::new();
let mut wildcard = false;
for part in Path::new(pattern).components() {
if part
.as_os_str()
.to_string_lossy()
.contains(['*', '?', '[', '{'])
{
wildcard = true;
break;
}
root.push(part);
}
if !wildcard {
// A literal denial can protect a missing path with the native
// guard machinery; it does not require a directory scan.
match fs::symlink_metadata(&root) {
Ok(_) => {
denied.insert(materialized(&root)?);
}
Err(error) if error.kind() == io::ErrorKind::NotFound => {
denied.insert(root);
}
Err(error) => return Err(error.into()),
}
} else if root.parent().is_none() {
return Err(Error::Unsupported(
"deny glob needs a literal directory prefix below the filesystem root".into(),
));
} else {
roots.insert(root);
}
}
let mut scanned = BTreeSet::new();
let mut pending: Vec<_> = roots.into_iter().collect();
while let Some(directory) = pending.pop() {
if !scanned.insert(directory.clone()) {
continue;
}
let entries = match fs::read_dir(&directory) {
Ok(entries) => entries,
Err(error) if error.kind() == io::ErrorKind::NotFound => continue,
Err(error) => return Err(error.into()),
};
// Do not scan through an unrecorded ancestor alias. Resolving only
// the eventual match would silently miss the policy's logical root.
if !path::compare(&materialized(&directory)?, &directory).is_eq() {
return Err(Error::Unsupported(
"deny glob directory prefix must be materialized on the execution Host".into(),
));
}
for entry in entries {
let entry = entry?;
let candidate = entry.path();
path::validate(&candidate)?;
let kind = entry.file_type()?;
let text = path::glob_text(candidate.to_str().ok_or_else(|| {
Error::Invalid("deny glob scan encountered a non-Unicode path".into())
})?)?;
if self.denied.is_match(text) {
// Binding a symlink itself is not safe on either native
// backend. Mask its resolved target, including aliases.
denied.insert(materialized(&candidate)?);
} else if kind.is_dir() {
pending.push(candidate);
}
}
}
let mut policy = self.policy.clone();
policy.deny_globs.clear();
// Glob denials cannot be reopened by a more specific ordinary rule.
policy
.rules
.retain(|rule| !denied.iter().any(|root| path::within(&rule.path, root)));
policy.rules.extend(
denied
.into_iter()
.map(|root| Rule::subtree(root, Access::Deny)),
);
Ok(policy.compile()?.policy)
}
}
fn materialized(path: &Path) -> Result<PathBuf, Error> {
let canonical = fs::canonicalize(path)?;
#[cfg(windows)]
let canonical = {
use std::path::{Component, Prefix};
let mut parts = canonical.components();
let mut result = match parts.next() {
Some(Component::Prefix(prefix)) => match prefix.kind() {
Prefix::VerbatimDisk(drive) => PathBuf::from(format!("{}:\\", char::from(drive))),
Prefix::VerbatimUNC(server, share) => {
let mut root = PathBuf::from(r"\\");
root.push(server);
root.push(share);
root
}
_ => return Err(Error::Invalid("unsupported canonical Windows path".into())),
},
_ => {
return Err(Error::Invalid(
"canonical Windows path lacks a volume".into(),
));
}
};
for part in parts {
if let Component::Normal(name) = part {
result.push(name);
}
}
result
};
path::validate(&canonical)?;
Ok(canonical)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn process_snapshot_denies_hidden_directories_and_reopenings_without_mutating_authority() {
let temp = tempfile::tempdir().unwrap();
let root = materialized(temp.path()).unwrap();
fs::create_dir_all(root.join(".hidden/private/nested")).unwrap();
fs::write(root.join(".hidden/private/nested/file"), "secret").unwrap();
fs::write(root.join("ordinary"), "public").unwrap();
let original = Policy {
default: Access::Read,
rules: vec![Rule::subtree(
root.join(".hidden/private/nested"),
Access::Write,
)],
deny_globs: vec![format!("{}/**/private", root.display())],
}
.compile()
.unwrap();
let snapshot = original.process_snapshot().unwrap().compile().unwrap();
assert_eq!(
snapshot.access(&root.join(".hidden/private/nested/file")),
Access::Deny
);
assert_eq!(snapshot.access(&root.join("ordinary")), Access::Read);
fs::create_dir(root.join("private")).unwrap();
assert_eq!(snapshot.access(&root.join("private/new")), Access::Read);
assert_eq!(original.access(&root.join("private/new")), Access::Deny);
assert_eq!(
original
.process_snapshot()
.unwrap()
.compile()
.unwrap()
.access(&root.join("private/new")),
Access::Deny
);
}
#[cfg(unix)]
#[test]
fn process_snapshot_denies_matching_link_targets_and_requires_a_literal_root() {
let temp = tempfile::tempdir().unwrap();
let root = materialized(temp.path()).unwrap();
fs::write(root.join("actual"), "secret").unwrap();
std::os::unix::fs::symlink(root.join("actual"), root.join("key.secret")).unwrap();
let mut policy = Policy {
default: Access::Read,
rules: vec![],
deny_globs: vec![format!("{}/*.secret", root.display())],
};
assert_eq!(
policy
.compile()
.unwrap()
.process_snapshot()
.unwrap()
.compile()
.unwrap()
.access(&root.join("actual")),
Access::Deny
);
policy.deny_globs = vec!["/**/*.secret".into()];
assert!(matches!(
policy.compile().unwrap().process_snapshot(),
Err(Error::Unsupported(_))
));
}
}