sudo-rs' first security audit

Published on 4 min read

    Thanks to funding from NLNet and ISRG, the sudo-rs team was able to request an audit from Radically Open Security (ROS). In this post, we'll share the findings of the audit and our response to those findings.

    ROS performed crystal-box penetration testing on sudo-rs with the goal of verifying that it was not possible to perform privileged actions without proper authentication. The audit was carried out between September 4 and September 15 on revision b5eb2c6 of the sudo-rs codebase.

    Overview of the findings

    The ROS team found one moderate severity issue and two low severity issues:

    • CLN-001: relative path traversal vulnerability (moderate)
    • CLN-003: Cargo configuration does not strip symbols (low)
    • CLN-004: incorrect default permissions on chown call (low)

    In addition to these findings, ROS performed fuzzing on different components of the sudo-rs code base but found no issues. You can read the full report here.

    CLN-001: relative path traversal vulnerability (moderate)

    Background information

    To save the user from re-entering their password on every invocation within a terminal session, sudo maintains a timestamp file that records when the last successful authentication occurred. The timestamp files for a user are stored in the directory /var/run/sudo-rs/ts/$USERNAME.

    The command sudo --remove-timestamp removes all timestamp files associated with the invoking user by removing the entire /var/run/sudo-rs/ts/$USERNAME directory.

    Attack vector

    An attacker able to use sudo and create users with names that contain special characters like dots (.) and slashes (/) can, for example, create a user named ../../../../usr/bin/cp and then invoke sudo -K as that user to remove the /usr/bin/cp command.

    sudo-rs team response

    During the audit, it came to light that the original sudo implementation was also affected by this issue, although with a lower security severity due to their use of the openat function.

    The team took the following actions:

    • The team informed Todd Miller, the maintainer of the original sudo implementation, about the issue.
    • The team prepared a fix and requested a CVE number.
    • The team notified packagers of sudo-rs about the vulnerability and the bug fix release plan.
    • On September 21, the team lifted the embargo on CVE-2023-42456 and released bug fix release v0.2.1

    Release 1.9.15 of the original sudo will include a similar fix. There's no CVE associated to the issue in the original sudo.

    CLN-003: release configuration does not strip symbols (low)

    To quote ROS:

    Description: The cargo release build does not strip symbols, so they will be included in the final binary. (..) Impact: Since the code is open source, there is not much information to be gained, but removing these symbols might make reverse engineering of the binary harder.

    Our comments:

    We consider this to be a non-issue in practice given that sudo-rs is an open source project. Regardless, we decided to modify the release profile to strip symbols in release builds.

    The main reason we kept symbols in the release build was to run cargo-bloat as part of our CI checks. cargo-bloat was useful during early development as it helped us identify which external dependencies were worthwhile to remove to decrease binary size. Fast forward to today, sudo-rs only has 3 dependencies in its dependency graph and plenty of work has gone into reducing binary size so cargo-bloat has lost its relevance and we were OK to part with it.

    CLN-004: incorrect default permissions on chown call (low)

    To quote ROS:

    A function used to invoke the libc chown function uses a default user ID value instead of failing.

    The code in question was

    type UserId = u16;
    type GroupId = u16;
    pub fn chown<S: AsRef<CStr>>(
        path: &S,
        uid: impl Into<Option<UserId>>,
        gid: impl Into<Option<GroupId>>,
    ) -> io::Result<()> {
        let path = path.as_ref().as_ptr();
        let uid = uid.into().unwrap_or(UserId::MAX);
        let gid = gid.into().unwrap_or(GroupId::MAX);
        cerr(unsafe { libc::chown(path, uid, gid) }).map(|_| ())

    Our comments:

    Indeed, this function was due for some refactoring: this chown wrapper was always called with a Some uid argument. Ultimately, the function was refactored to not take any Option arguments.

    We would like to thank ROS for conducting the first audit on sudo-rs. We have learned a lot from it and look forward to future audits and third party reviews. As ROS put in the report: "security is an ongoing process"; we agree that even after a piece of software has achieved a desired number of features, work must continue to ensure the software is free from vulnerabilities and other defects.

    We would like to use this opportunity to invite security researchers, penetration testers and the general developer community to review the sudo-rs code base. Should you find a security issue, please follow our security policy to disclose it in a responsible manner. For non-security issues, you can use our bug report template on GitHub.

    Thanks, the sudo-rs team