You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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.
Details
The rewrite() context manager in dotenv/main.py is used by both set_key() and unset_key() to safely modify .env files. It works by writing to a temporary file (created in the system's default temp directory, typically /tmp) and then using shutil.move() to replace the original file.
When the .env path is a symbolic link and the temp directory resides on a different filesystem than the target (a common configuration on Linux systems using tmpfs for /tmp), the following sequence occurs:
shutil.move() first attempts os.rename(), which fails with an OSError because atomic renames cannot cross device boundaries.
On failure, shutil.move() falls back to shutil.copy2() followed by os.unlink().
shutil.copy2() calls shutil.copyfile() with follow_symlinks=True by default.
This causes the content to be written to the symlink target rather than replacing the symlink itself.
An attacker who has write access to the directory containing a .env file can pre-place a symlink pointing to any file that the application process has write access to. When the application (or a privileged process such as a deploy script, Docker entrypoint, or CI pipeline) calls set_key() or unset_key(), the symlink target is overwritten with the new .env content.
This vulnerability does not require a race condition and is fully deterministic once the preconditions are met.
Impact
The primary impacts are to integrity and availability:
File overwrite / destruction (DoS): An attacker can cause an application or privileged process to corrupt or destroy configuration files, database configs, or other sensitive files it would not normally have access to modify.
Integrity violation: The target file's original content is replaced with .env-formatted content controlled by the attacker.
Potential privilege escalation: In scenarios where a privileged process (running as root or a service account) calls set_key(), the attacker can leverage this to write to files beyond their own access level.
The scope of impact depends on the application using python-dotenv and the privileges under which it runs.
Proof of Concept
The following script demonstrates the vulnerability. It requires /tmp and the user's home directory to reside on different devices (common on systemd-based Linux systems with tmpfs).
importosimportsysimporttempfilefromdotenvimportset_key##### Pre-condition: /tmp must be on a different device than the target directory.tmp_dev=os.stat("/tmp").st_devhome_dev=os.stat(os.path.expanduser("~")).st_devasserttmp_dev!=home_dev, "Skipped: /tmp and ~ are on the same device (no cross-device move)"withtempfile.TemporaryDirectory(dir=os.path.expanduser("~")) asworkdir:
# File an attacker wants to overwritetarget=os.path.join(workdir, "victim_config.txt")
withopen(target, "w") asf:
f.write("DB_PASSWORD=supersecret\n")
# Attacker pre-places a symlink at the path the application will use as .envenv_symlink=os.path.join(workdir, ".env")
os.symlink(target, env_symlink)
before=open(target).read()
# Application writes a new key -- triggers the cross-device fallbackset_key(env_symlink, "INJECTED", "attacker_value")
after=open(target).read()
print("Before:", repr(before))
print("After: ", repr(after))
print("Symlink target overwritten:", target)
The fix changes the rewrite() context manager in the following ways:
Symlinks are no longer followed by default. When the .env path is a symlink, rewrite() now resolves it to the real path before proceeding, or (by default) operates on the symlink entry itself rather than the target.
A follow_symlinks: bool = False parameter is added to set_key() and unset_key() for users who explicitly need the old behavior.
Temp files are written in the same directory as the target .env file (instead of the system temp directory), eliminating the cross-device rename condition entirely.
os.replace() is used instead of shutil.move(), providing atomic replacement without symlink-following fallback behavior.
Users are advised to upgrade to the patched version as soon as it is available on PyPI.
Timeline
Date
Event
2026-01-09
Initial report received from Giorgos Tsigourakos regarding a separate, unrelated issue also located in rewrite()
Support for Python 3.14, including the free-threaded (3.14t) build. (#588)
Changed
The dotenv run command now forwards flags directly to the specified command by [@bbc2] in [#607]
Improved documentation clarity regarding override behavior and the reference page.
Updated PyPy support to version 3.11.
Documentation for FIFO file support.
Dropped Support for Python 3.9.
Fixed
Improved set_key and unset_key behavior when interacting with symlinks by [@bbc2] in [790c5c0]
Corrected the license specifier and added missing Python 3.14 classifiers in package metadata by [@JYOuyang] in [#590]
Breaking Changes
dotenv.set_key and dotenv.unset_key used to follow symlinks in some
situations. This is no longer the case. For that behavior to be restored in
all cases, follow_symlinks=True should be used.
In the CLI, set and unset used to follow symlinks in some situations. This
is no longer the case.
dotenv.set_key, dotenv.unset_key and the CLI commands set and unset
used to reset the file mode of the modified .env file to 0o600 in some
situations. This is no longer the case: The original mode of the file is now
preserved. Is the file needed to be created or wasn't a regular file, mode 0o600 is used.
Renovate failed to update an artifact related to this branch. You probably do not want to merge this PR as-is.
♻ Renovate will retry this branch, including artifacts, only when one of the following happens:
any of the package files in this branch needs updating, or
the branch becomes conflicted, or
you click the rebase/retry checkbox if found above, or
you rename this PR's title to start with "rebase!" to trigger it manually
The artifact failure details are included below:
File name: services/aimer-web/uv.lock
Command failed: uv lock --upgrade-package python-dotenv
Using CPython 3.13.13 interpreter at: /opt/containerbase/tools/python/3.13.13/bin/python3
× No solution found when resolving dependencies:
╰─▶ Because litellm==1.83.3 depends on python-dotenv==1.0.1 and
openrag==0.4.1 depends on litellm==1.83.3, we can conclude that
openrag==0.4.1 depends on python-dotenv==1.0.1.
And because only the following versions of openrag are available:
openrag<=0.4.1
openrag>0.5.0
we can conclude that openrag>=0.4.1 depends on python-dotenv==1.0.1.
And because your project depends on openrag>=0.4.1 and
python-dotenv==1.2.2, we can conclude that your project's requirements
are unsatisfiable.
Coverage variation is the difference between the coverage for the head and common ancestor commits of the pull request branch: <coverage of head commit> - <coverage of common ancestor commit>
Diff coverage is the percentage of lines that are covered by tests out of the coverable lines that the pull request added or modified: <covered lines added or modified>/<coverable lines added or modified> * 100%
NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer TIP This summary will be updated as you push new changes.
Because you closed this PR without merging, Renovate will ignore this update (==1.2.2). You will get a PR once a newer version is released. To ignore this dependency forever, add it to the ignoreDeps array of your Renovate config.
If you accidentally closed this PR, or if you changed your mind: rename this PR to get a fresh replacement PR.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This PR contains the following updates:
==1.0.1→==1.2.2python-dotenv: Symlink following in set_key allows arbitrary file overwrite via cross-device rename fallback
CVE-2026-28684 / GHSA-mf9w-mj56-hr94
More information
Details
Summary
set_key()andunset_key()in python-dotenv follow symbolic links when rewriting.envfiles, allowing a local attacker to overwrite arbitrary files via a crafted symlink when a cross-device rename fallback is triggered.Details
The
rewrite()context manager indotenv/main.pyis used by bothset_key()andunset_key()to safely modify.envfiles. It works by writing to a temporary file (created in the system's default temp directory, typically/tmp) and then usingshutil.move()to replace the original file.When the
.envpath is a symbolic link and the temp directory resides on a different filesystem than the target (a common configuration on Linux systems using tmpfs for/tmp), the following sequence occurs:shutil.move()first attemptsos.rename(), which fails with anOSErrorbecause atomic renames cannot cross device boundaries.shutil.move()falls back toshutil.copy2()followed byos.unlink().shutil.copy2()callsshutil.copyfile()withfollow_symlinks=Trueby default.An attacker who has write access to the directory containing a
.envfile can pre-place a symlink pointing to any file that the application process has write access to. When the application (or a privileged process such as a deploy script, Docker entrypoint, or CI pipeline) callsset_key()orunset_key(), the symlink target is overwritten with the new.envcontent.This vulnerability does not require a race condition and is fully deterministic once the preconditions are met.
Impact
The primary impacts are to integrity and availability:
.env-formatted content controlled by the attacker.set_key(), the attacker can leverage this to write to files beyond their own access level.The scope of impact depends on the application using python-dotenv and the privileges under which it runs.
Proof of Concept
The following script demonstrates the vulnerability. It requires
/tmpand the user's home directory to reside on different devices (common on systemd-based Linux systems with tmpfs).Expected output:
Remediation
The fix changes the
rewrite()context manager in the following ways:.envpath is a symlink,rewrite()now resolves it to the real path before proceeding, or (by default) operates on the symlink entry itself rather than the target.follow_symlinks: bool = Falseparameter is added toset_key()andunset_key()for users who explicitly need the old behavior..envfile (instead of the system temp directory), eliminating the cross-device rename condition entirely.os.replace()is used instead ofshutil.move(), providing atomic replacement without symlink-following fallback behavior.Users are advised to upgrade to the patched version as soon as it is available on PyPI.
Timeline
rewrite()Patches
Upgrade to v.1.2.2 or use the patch from https://github.com/theskumar/python-dotenv/commit/790c5c02991100aa1bf41ee5330aca75edc51311.patch
Severity
CVSS:3.1/AV:L/AC:L/PR:L/UI:R/S:U/C:N/I:H/A:HReferences
This data is provided by the GitHub Advisory Database (CC-BY 4.0).
Release Notes
theskumar/python-dotenv (python-dotenv)
v1.2.2Compare Source
Added
Changed
dotenv runcommand now forwards flags directly to the specified command by [@bbc2] in [#607]Fixed
set_keyandunset_keybehavior when interacting with symlinks by [@bbc2] in [790c5c0]Breaking Changes
dotenv.set_keyanddotenv.unset_keyused to follow symlinks in somesituations. This is no longer the case. For that behavior to be restored in
all cases,
follow_symlinks=Trueshould be used.In the CLI,
setandunsetused to follow symlinks in some situations. Thisis no longer the case.
dotenv.set_key,dotenv.unset_keyand the CLI commandssetandunsetused to reset the file mode of the modified .env file to
0o600in somesituations. This is no longer the case: The original mode of the file is now
preserved. Is the file needed to be created or wasn't a regular file, mode
0o600is used.v1.2.1Compare Source
pyproject.toml, removedsetup.cfg.envfrom FIFOs (Unix) by [@sidharth-sudhir] in [#586]v1.2.0Compare Source
buildandpyproject.tomlby [@EpicWink] in [#583]load_dotenv()usingPYTHON_DOTENV_DISABLEDenv var. by [@matthewfranglen] in [#569]v1.1.1Compare Source
Fixed
find_dotenvwork reliably on python 3.13 by [@theskumar] in [#563]v1.1.0Compare Source
Added
dotenv run, switch toexecvpefor better resource management and signal handling ([#523]) by [@eekstunt]Fixed
find_dotenvandload_dotenvnow correctly looks up at the current directory when running in debugger or pdb ([#553] by [@randomseed42])Misc
Configuration
📅 Schedule: (in timezone Etc/UTC)
🚦 Automerge: Disabled by config. Please merge this manually once you are satisfied.
♻ Rebasing: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.
🔕 Ignore: Close this PR and you won't be reminded about this update again.
This PR was generated by Mend Renovate. View the repository job log.