CVE-2026-28684

ADVISORY - github

Summary

Summary

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:

  1. shutil.move() first attempts os.rename(), which fails with an OSError because atomic renames cannot cross device boundaries.
  2. On failure, shutil.move() falls back to shutil.copy2() followed by os.unlink().
  3. shutil.copy2() calls shutil.copyfile() with follow_symlinks=True by default.
  4. 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).

import os
import sys
import tempfile
from dotenv import set_key

# Pre-condition: /tmp must be on a different device than the target directory.
tmp_dev = os.stat("/tmp").st_dev
home_dev = os.stat(os.path.expanduser("~")).st_dev
assert tmp_dev != home_dev, "Skipped: /tmp and ~ are on the same device (no cross-device move)"

with tempfile.TemporaryDirectory(dir=os.path.expanduser("~")) as workdir:
    # File an attacker wants to overwrite
    target = os.path.join(workdir, "victim_config.txt")
    with open(target, "w") as f:
        f.write("DB_PASSWORD=supersecret\n")

    # Attacker pre-places a symlink at the path the application will use as .env
    env_symlink = os.path.join(workdir, ".env")
    os.symlink(target, env_symlink)

    before = open(target).read()

    # Application writes a new key -- triggers the cross-device fallback
    set_key(env_symlink, "INJECTED", "attacker_value")

    after = open(target).read()

    print("Before:", repr(before))
    print("After: ", repr(after))
    print("Symlink target overwritten:", target)

Expected output:

Before: 'DB_PASSWORD=supersecret\n'
After:  "DB_PASSWORD=supersecret\nINJECTED='attacker_value'\n"
Symlink target overwritten: /home/user/tmp806nut2g/victim_config.txt

Remediation

The fix changes the rewrite() context manager in the following ways:

  1. 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.
  2. A follow_symlinks: bool = False parameter is added to set_key() and unset_key() for users who explicitly need the old behavior.
  3. 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.
  4. 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()
2026-01-10 Co-maintainer acknowledged report, requested clarification
2026-01-11 Initial report assessed as not exploitable and closed
2026-02-24 Reporter identified new, distinct cross-device symlink attack vector with deterministic exploitation
2026-02-26 Co-maintainer confirmed vulnerability and shared draft patch
2026-02-26 Reporter validated fix with monkeypatched PoC, proposed CVSS
2026-03-01 Patch merged to main
2026-03-01 Patched version released to PyPI
2026-04-20 Advisory published

Patches

Upgrade to v.1.2.2 or use the patch from https://github.com/theskumar/python-dotenv/commit/790c5c02991100aa1bf41ee5330aca75edc51311.patch

Common Weakness Enumeration (CWE)

ADVISORY - nist

Improper Link Resolution Before File Access ('Link Following')

UNIX Symbolic Link (Symlink) Following

ADVISORY - github

Improper Link Resolution Before File Access ('Link Following')

UNIX Symbolic Link (Symlink) Following


GitHub

CREATED

UPDATED

EXPLOITABILITY SCORE

1.3

EXPLOITS FOUND
-
COMMON WEAKNESS ENUMERATION (CWE)

CVSS SCORE

6.6medium
PackageTypeOS NameOS VersionAffected RangesFix Versions
python-dotenvpypi--<1.2.21.2.2

CVSS:3 Severity and metrics

The CVSS metrics represent different qualitative aspects of a vulnerability that impact the overall score, as defined by the CVSS Specification.

The vulnerable component is not bound to the network stack and the attacker's path is via read/write/execute capabilities. Either: The attacker exploits the vulnerability by accessing the target system locally (e.g., keyboard, console), or remotely (e.g., SSH); or the attacker relies on User Interaction by another person to perform actions required to exploit the vulnerability (e.g., using social engineering techniques to trick a legitimate user into opening a malicious document).

Specialized access conditions or extenuating circumstances do not exist. An attacker can expect repeatable success when attacking the vulnerable component.

The attacker requires privileges that provide basic user capabilities that could normally affect only settings and files owned by a user. Alternatively, an attacker with Low privileges has the ability to access only non-sensitive resources.

Successful exploitation of this vulnerability requires a user to take some action before the vulnerability can be exploited. For example, a successful exploit may only be possible during the installation of an application by a system administrator.

An exploited vulnerability can only affect resources managed by the same security authority. In this case, the vulnerable component and the impacted component are either the same, or both are managed by the same security authority.

There is no loss of confidentiality.

There is a total loss of integrity, or a complete loss of protection. For example, the attacker is able to modify any or all files protected by the impacted component. Alternatively, only some files can be modified, but malicious modification would present a direct, serious consequence to the impacted component.

There is a total loss of availability, resulting in the attacker being able to fully deny access to resources in the impacted component; this loss is either sustained (while the attacker continues to deliver the attack) or persistent (the condition persists even after the attack has completed). Alternatively, the attacker has the ability to deny some availability, but the loss of availability presents a direct, serious consequence to the impacted component.

NIST

CREATED

UPDATED

EXPLOITABILITY SCORE

1.3

EXPLOITS FOUND
-
COMMON WEAKNESS ENUMERATION (CWE)

CVSS SCORE

6.6medium

Debian

CREATED

UPDATED

EXPLOITABILITY SCORE

-

EXPLOITS FOUND
-
COMMON WEAKNESS ENUMERATION (CWE)-
RATING UNAVAILABLE FROM ADVISORY

Ubuntu

CREATED

UPDATED

EXPLOITABILITY SCORE

-

EXPLOITS FOUND
-
COMMON WEAKNESS ENUMERATION (CWE)-

CVSS SCORE

N/Amedium