HexBrain

Internet Bug Bounty · Report #1520931

Time-of-check to time-of-use vulnerability in the std::fs::remove_dir_all() function of the Rust standard library

Validstatus
highseverity
$4,000bounty
9upvotes
24 Mar 2022disclosed

Summary

Race condition in std::fs::remove_dir_all Description This is a cross-post of the official security advisory. The official advisory contains a signed version with our PGP key, as well.

The Rust Security Response WG was notified that the std::fs::remove_dir_all standard library function is vulnerable a race condition enabling symlink following (CWE-363). An attacker could use this security issue to trick a privileged program into deleting files and directories the attacker couldn't otherwise access or delete.

This issue has been assigned CVE-2022-21658.

Overview Let's suppose an attacker obtained unprivileged access to a system and needed to delete a system directory called sensitive/, but they didn't have the permissions to do so. If std::fs::remove_dir_all followed symbolic links, they could find a privileged program that removes a directory they have access to (called temp/), create a symlink from temp/foo to sensitive/, and wait for the privileged program to delete foo/. The privileged program would follow the symlink from temp/foo to sensitive/ while recursively deleting, resulting in sensitive/ being deleted.

To prevent such attacks, std::fs::remove_dir_all already includes protection to avoid recursively deleting symlinks, as described in its documentation:

This function does not follow symbolic links and it will simply remove the symbolic link itself.

Unfortunately that check was implemented incorrectly in the standard library, resulting in a TOCTOU (Time-of-check Time-of-use) race condition. Instead of telling the system not to follow symlinks, the standard library first checked whether the thing it was about to delete was a symlink, and otherwise it would proceed to recursively delete the directory.

This exposed a race condition: an attacker could create a directory and replace it with a symlink between the check and the actual deletion. While this attack likely won't work the first time it's attempted, in our experimentation we were able to reliably perform it within a couple of seconds.

Affected Versions Rust 1.0.0 through Rust 1.58.0 is affected by this vulnerability. We're going to release Rust 1.58.1 later today, which will include mitigations for this vulnerability. Patches to the Rust standard library are also available for custom-built Rust toolchains here.

Note that the following targets don't have usable APIs to properly mitigate the attack, and are thus still vulnerable even with a patched toolchain:

macOS before version 10.10 (Yosemite) REDOX Mitigations We recommend everyone to update to Rust 1.58.1 as soon as possible, especially people developing programs expected to run in privileged contexts (including system daemons and setuid binaries), as those have the highest risk of being affected by this.

Note that adding checks in your codebase before calling remove_dir_all will not mitigate the vulnerability, as they would also be vulnerable to race conditions like remove_dir_all itself. The existing mitigation is working as intended outside of race conditions.

Acknowledgments We want to thank Hans Kratz for independently discovering and disclosing this issue to us according to the Rust security policy, for developing the fix for UNIX-like targets and for reviewing fixes for other platforms.

We also want to thank Florian Weimer for reviewing the UNIX-like fix and for reporting the same issue back in 2018, even though the Security Response WG didn't realize the severity of the issue at the time.

Finally we want to thank Pietro Albini for coordinating the security response and writing this advisory, Chris Denton for writing the Windows fix, Alex Crichton for writing the WASI fix, and Mara Bos for reviewing the patches.

https://github.com/rust-lang/rust/security/advisories/GHSA-r9cc-f5pr-p3j2

How it was found

The implementation of std::fs::remove_dir_all() in the Rust standard library is vulnerable to a time-of-check to time-of-use link replacement attack. This applies to all versions of Rust before 1.58.1.

Vulnerability details

The documentation of std::fs::remove_dir_all() guarantees that the function does not follow symbolic links:

Removes a directory at this path, after removing all its contents. Use carefully! This function does not follow symbolic links and it will simply remove the symbolic link itself.

The vulnerable implementation for Windows is in library/std/src/sys/windows/fs.rs. For other platforms it is in library/std/src/sys_common/fs.rs. Both use a remove_dir_all_recursive() helper function which does the actual recursion and deletion. It opens directory by the given path and iterates the directory entries. For each directory entry it checks if the entry is a directory and recurses into it if it is. If it is not it is deleted using std::fs::remove_file(). On the way back up the now empty directories are deleted using std::fs::remove_dir()

There are two problems with this implementation if the attacker has write access to a directory which is being deleted by the privileged process:

  1. The type of a directory entry is checked and it is being recursed into if it is a directory. There is a short time window between the check and the opening of the subdirectory which an attacker can exploit by replacing the subdirectory with symlink causing the symlink to be followed.

  2. The path given to std::fs::remove_dir_all() is extended with subentry paths which are then used to process subdirectories and delete directory entries. Paths are resolved by the operating system each time they are passed to a system call. If the attacker can replace a descendent directory of the directory passed to remove_dir_all() while a subdirectory of it is being processed with a symlink he can cause that symlink to be followed in subsequent filesystem operations.

A proof-of-concept code demonstrating the vulnerability is attached.

Mitigation

  • Update to Rust 1.58.1 or later which includes a fixed implementation for all supported platforms except for macOS before version 10.10 and REDOX.
  • Don't use the vulnerable std::fs::remove_dir_all() in a privileged process or any other security-senstitive context.
  • Make sure that std::fs::remove_dir_all() is only used on directories not accessible to processes from other security contexts.

Impact

If the attacker has write access to a directory which is being deleted by the privileged process using remove_dir_all() he can trick the process to delete any sensitive files or directory subtrees that the privileged process can.

This report was disclosed publicly on HackerOne and belongs to its author and the program. See the original