Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
141 changes: 103 additions & 38 deletions src/visudo/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,19 @@ mod cli;
mod help;

use std::{
env, ffi,
fs::{File, Permissions},
io::{self, Read, Seek, Write},
os::unix::prelude::{MetadataExt, PermissionsExt},
path::{Path, PathBuf},
process::Command,
str,
};

use crate::{
common::resolve::CurrentUser,
sudo::{candidate_sudoers_file, diagnostic},
sudoers::Sudoers,
sudoers::{self, Sudoers},
system::{
can_execute,
file::{create_temporary_dir, Chown, FileLock},
Expand Down Expand Up @@ -265,7 +267,7 @@ fn edit_sudoers_file(
.spawn()?
.wait_with_output()?;

let (_sudoers, errors) = File::open(tmp_path)
let (sudoers, errors) = File::open(tmp_path)
.and_then(|reader| Sudoers::read(reader, tmp_path))
.map_err(|err| {
io_msg!(
Expand All @@ -276,46 +278,42 @@ fn edit_sudoers_file(
)
})?;

if errors.is_empty() {
break;
}

writeln!(stderr, "The provided sudoers file format is not recognized or contains syntax errors. Please review:\n")?;

for crate::sudoers::Error {
message,
source,
location,
} in errors
{
let path = source.as_deref().unwrap_or(sudoers_path);
diagnostic::diagnostic!("syntax error: {message}", path @ location);
}

writeln!(stderr)?;

let stdin = io::stdin();
let stdout = io::stdout();
if !errors.is_empty() {
writeln!(stderr, "The provided sudoers file format is not recognized or contains syntax errors. Please review:\n")?;

for crate::sudoers::Error {
message,
source,
location,
} in errors
{
let path = source.as_deref().unwrap_or(sudoers_path);
diagnostic::diagnostic!("syntax error: {message}", path @ location);
}

let mut stdin_handle = stdin.lock();
let mut stdout_handle = stdout.lock();
writeln!(stderr)?;

loop {
stdout_handle
.write_all("What now? e(x)it without saving / (e)dit again: ".as_bytes())?;
stdout_handle.flush()?;

let mut input = [0u8];
if let Err(err) = stdin_handle.read_exact(&mut input) {
writeln!(stderr, "visudo: cannot read user input: {err}")?;
return Ok(());
match ask_response(b"What now? e(x)it without saving / (e)dit again: ", b"xe")? {
b'x' => return Ok(()),
_ => continue,
}

match &input {
b"e" => break,
b"x" => return Ok(()),
input => writeln!(stderr, "Invalid option: {:?}\n", std::str::from_utf8(input))?,
} else {
if sudo_visudo_is_allowed(sudoers, &host_name) == Some(false) {
writeln!(
stderr,
"It looks like you have removed your ability to run 'sudo visudo' again.\n"
)?;
match ask_response(
b"What now? e(x)it without saving / (e)dit again / lock me out and (S)ave: ",
b"xeS",
)? {
b'x' => return Ok(()),
b'S' => {}
_ => continue,
}
}

break;
}
}

Expand Down Expand Up @@ -354,3 +352,70 @@ fn editor_path_fallback() -> io::Result<PathBuf> {
"cannot find text editor",
))
}

// To detect potential lock-outs if the user called "sudo visudo".
// Note that SUDO_USER will normally be set by sudo.
//
// This returns Some(false) if visudo is forbidden under the given config;
// Some(true) if it is allowed; and None if it cannot be determined, which
// will be the case if e.g. visudo was simply run as root.
fn sudo_visudo_is_allowed(mut sudoers: Sudoers, host_name: &Hostname) -> Option<bool> {
let sudo_user =
User::from_name(&ffi::CString::new(env::var("SUDO_USER").ok()?).ok()?).ok()??;

let super_user = User::from_uid(UserId::ROOT).ok()??;

let request = sudoers::Request {
user: &super_user,
group: &super_user.primary_group().ok()?,
command: &env::current_exe().ok()?,
arguments: &[],
};

Some(matches!(
sudoers
.check(&sudo_user, host_name, request)
.authorization(),
sudoers::Authorization::Allowed { .. }
))
}

// Make sure that the first valid response is the "safest" choice
fn ask_response(prompt: &[u8], valid_responses: &[u8]) -> io::Result<u8> {
let stdin = io::stdin();
let stdout = io::stdout();
let mut stderr = io::stderr();

let mut stdin_handle = stdin.lock();
let mut stdout_handle = stdout.lock();

loop {
stdout_handle.write_all(prompt)?;
stdout_handle.flush()?;

let mut input = [0u8];
if let Err(err) = stdin_handle.read_exact(&mut input) {
writeln!(stderr, "visudo: cannot read user input: {err}")?;
return Ok(valid_responses[0]);
}

// read the trailing newline
loop {
let mut skipped = [0u8];
match stdin_handle.read_exact(&mut skipped) {
Ok(()) if &skipped != b"\n" => continue,
_ => break,
}
}

if valid_responses.contains(&input[0]) {
return Ok(input[0]);
} else {
writeln!(
stderr,
"Invalid option: '{}'\n",
str::from_utf8(&input).unwrap_or("<INVALID>")
)?;
}
}
}
Loading