mirror of
https://github.com/openai/codex.git
synced 2026-09-04 15:08:45 +00:00
141 lines
4.8 KiB
Rust
141 lines
4.8 KiB
Rust
use std::collections::BTreeSet;
|
|
use std::io;
|
|
use std::path::Path;
|
|
#[cfg(test)]
|
|
use std::path::PathBuf;
|
|
|
|
use crate::git_command::GitRunner;
|
|
use crate::git_config::read_config_entries_without_includes;
|
|
|
|
mod include_graph;
|
|
mod path_safety;
|
|
mod primary_sources;
|
|
|
|
use include_graph::validate_include_entries;
|
|
use path_safety::normalize_absolute_path;
|
|
use path_safety::resolve_literal_path;
|
|
use primary_sources::default_system_config_source_candidates;
|
|
use primary_sources::is_disabled_primary_config_path;
|
|
use primary_sources::legacy_primary_config_source_candidates;
|
|
use primary_sources::selected_git_home_config_candidates;
|
|
use primary_sources::selected_git_prefix_system_candidate;
|
|
|
|
const INCLUDE_CONFIG_PATTERN: &str = r"^include(\.path|if\..*\.path)$";
|
|
const MAX_CONFIG_INCLUDE_DEPTH: usize = 10;
|
|
const MAX_CONFIG_INCLUDE_FILES: usize = 1024;
|
|
|
|
/// Reject configuration that an untrusted worktree writer can change between
|
|
/// a policy probe and the Git command it guards.
|
|
pub(crate) fn ensure_no_worktree_config_sources(
|
|
git: &GitRunner,
|
|
git_root: &Path,
|
|
git_config_args: &[String],
|
|
) -> io::Result<()> {
|
|
let git_root = normalize_absolute_path(std::fs::canonicalize(git_root)?)?;
|
|
|
|
// Environment, HOME, and XDG paths are attacker-controlled byte strings.
|
|
// Classify their exact OsString spelling before `git var` can open or
|
|
// newline-delimit one of them.
|
|
for (description, candidate) in legacy_primary_config_source_candidates()? {
|
|
if !is_disabled_primary_config_path(&candidate) {
|
|
reject_source(git, &git_root, &candidate, description)?;
|
|
}
|
|
}
|
|
for candidate in selected_git_home_config_candidates(git, &git_root)? {
|
|
reject_source(git, &git_root, &candidate, "selected Git HOME config")?;
|
|
}
|
|
if let Some(candidate) = selected_git_prefix_system_candidate(git, &git_root)? {
|
|
reject_source(
|
|
git,
|
|
&git_root,
|
|
&candidate,
|
|
"selected Git prefix system config",
|
|
)?;
|
|
}
|
|
let entries = read_config_entries_without_includes(
|
|
git,
|
|
&git_root,
|
|
git_config_args,
|
|
INCLUDE_CONFIG_PATTERN,
|
|
"include",
|
|
/*config_file*/ None,
|
|
)?;
|
|
let mut pending = Vec::new();
|
|
validate_include_entries(git, &git_root, entries, /*depth*/ 1, &mut pending)?;
|
|
let mut visited = BTreeSet::new();
|
|
while let Some((config_path, depth)) = pending.pop() {
|
|
if depth > MAX_CONFIG_INCLUDE_DEPTH {
|
|
return Err(path_safety::invalid_config_source(
|
|
"Git config include depth exceeded",
|
|
));
|
|
}
|
|
match std::fs::canonicalize(&config_path) {
|
|
Ok(_) => {}
|
|
Err(error)
|
|
if matches!(
|
|
error.kind(),
|
|
io::ErrorKind::NotFound | io::ErrorKind::NotADirectory
|
|
) =>
|
|
{
|
|
continue;
|
|
}
|
|
Err(error) => return Err(error),
|
|
}
|
|
// The same file reached through two spellings can resolve a relative
|
|
// child include differently. Deduplicate only the exact source
|
|
// spelling, not its canonical target.
|
|
if !visited.insert(config_path.clone()) {
|
|
continue;
|
|
}
|
|
if visited.len() > MAX_CONFIG_INCLUDE_FILES {
|
|
return Err(path_safety::invalid_config_source(
|
|
"too many Git config include files",
|
|
));
|
|
}
|
|
let entries = read_config_entries_without_includes(
|
|
git,
|
|
&git_root,
|
|
&[],
|
|
INCLUDE_CONFIG_PATTERN,
|
|
"include",
|
|
Some(&config_path),
|
|
)?;
|
|
validate_include_entries(git, &git_root, entries, depth + 1, &mut pending)?;
|
|
}
|
|
// `git var` loads repository config before reporting modern default-system
|
|
// paths. Delay that supplemental probe until the complete no-includes
|
|
// source graph above has been classified, so it cannot be tricked into
|
|
// opening an untrusted include or FIFO first.
|
|
for (description, candidate) in default_system_config_source_candidates(git, &git_root)? {
|
|
if !is_disabled_primary_config_path(&candidate) {
|
|
reject_source(git, &git_root, &candidate, description)?;
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn reject_source(
|
|
git: &GitRunner,
|
|
cwd: &Path,
|
|
candidate: &Path,
|
|
description: &str,
|
|
) -> io::Result<()> {
|
|
let absolute = resolve_literal_path(candidate, cwd);
|
|
git.ensure_config_source_is_not_worktree_controlled(&absolute, description)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
use crate::git_config::GitConfigEntry;
|
|
#[cfg(test)]
|
|
use codex_utils_absolute_path::AbsolutePathBuf;
|
|
#[cfg(test)]
|
|
use include_graph::expand_git_config_path;
|
|
#[cfg(test)]
|
|
use include_graph::resolve_include_path;
|
|
#[cfg(test)]
|
|
use path_safety::windows_config_path_is_ambiguous;
|
|
|
|
#[cfg(test)]
|
|
#[path = "git_config_sources_tests.rs"]
|
|
mod tests;
|