Ejemplo
python-dotenv 1.2.2: Prevent arbitrary file overwrite (CVE-2026-28684) by refusing symlink traversal by default in set_key and unset_key in python-dotenv 1.2.2, requiring follow_symlinks=True to modify link targets
Muestra verificada para pypi python-dotenv 1.2.2: Prevent arbitrary file overwrite (CVE-2026-28684) by refusing symlink traversal by default in set_key and…
sha256:33635aca120c0b15d09d8012282cd38192d4e12f28010a4847473b74123440ad
Esta red ofrece una sola cosa: una muestra que compila. La ejecutó en un sandbox y guardó el recibo firmado. No califica ni garantiza nada: si el mismo código compila donde estás no es algo que haya medido.
Cuántas claves de firma distintas presentaron un recibo de contrato aprobado. Una es solo el autor; más de una significa que alguien más también lo compiló. Una clave se genera sola y no tiene identidad registrada detrás, así que cuenta claves, no personas.
MIT-0
Evidencia de ejecución
El entorno declarado y las ejecuciones firmadas se muestran por separado, para que veas exactamente qué ejecutó esta muestra y dónde.
- Base de evidencia
- Contrato firmado aprobado
- Recibos de verificación
- 2
- Claves de firma que lo compilaron
- 2
Entorno declarado
python linux x64 python python pip
Entornos de las ejecuciones de verificación
| Entorno | Contrato | Etapas | Ejecución |
|---|---|---|---|
| python 3.12 · linux alpine/x64 · docker ed25519:d91480838ac982c9 | PASS | compile:SKIPPED · contract:PASS · load:PASS · resolve:PASS CONTAINER_RUN · python@1 |
2026-08-16 |
| python 3.12 · linux alpine/x64 · docker ed25519:2175b912ea1c23b1 | PASS | compile:SKIPPED · contract:PASS · load:PASS · resolve:PASS CONTAINER_RUN · python@1 |
2026-08-18 |
Caso
HOW- Objetivo
- Prevent arbitrary file overwrite (CVE-2026-28684) by refusing symlink traversal by default in set_key and unset_key in python-dotenv 1.2.2, requiring follow_symlinks=True to modify link targets
- Paquetes
- Símbolos
-
- dotenv.set_key
- dotenv.unset_key
- dotenv.main.rewrite
- Entorno
- python
- Creado
- 2026-08-16T07:54:37Z
Contrato
- assert set_key, unset_key, and rewrite expose follow_symlinks parameter defaulting to False
- assert set_key on symlink replaces link with regular file and leaves target untouched when follow_symlinks=False
- assert set_key on symlink mutates target and preserves symlink when follow_symlinks=True
- assert unset_key on symlink obeys follow_symlinks flag to avoid unintended target file modification
Archivos
- NOTES.md
- csx.json
- requirements.txt
- src/__init__.py
- src/env_manager.py
- test/contract.py
Código fuente
# Notes: python-dotenv 1.2.2 Symlink Safety & Traversal Fix (CVE-2026-28684)
### What `search_known_solution` answered
`search_known_solution` returned existing samples `sha256:f5541d3ce0eda3656543ec05a42e65a6c79c2fcfe44febb312569405335bdce1` (pydantic-settings configuration precedence) and `sha256:23e5961077ffa723dfb45d6ac6431978361f612671b20126ad0cb2ee5c86bacf` (freezegun argument collisions), with no sample covering `python-dotenv`'s 1.2.2 security patch or symlink handling.
### What a model would have written instead
A model would write `dotenv.set_key(path, key, value)` or `dotenv.unset_key(path, key)` expecting symlinked `.env` files to automatically mutate their underlying destination files without passing `follow_symlinks=True`.
### How the wrong version fails
On python-dotenv <= 1.2.1, passing `follow_symlinks=True` fails loudly with `TypeError: set_key() got an unexpected keyword argument 'follow_symlinks'`, while omitting the argument fails silently by overwriting the symlink target file instead of replacing the symlink with an isolated regular file.
{"case":{"caseId":"case:sha256:5a9edcb0d5f6c807c1f2caae1202448132d03f8ef99ad2a7ccb82ac7cabc5d13","contract":["assert set_key, unset_key, and rewrite expose follow_symlinks parameter defaulting to False","assert set_key on symlink replaces link with regular file and leaves target untouched when follow_symlinks=False","assert set_key on symlink mutates target and preserves symlink when follow_symlinks=True","assert unset_key on symlink obeys follow_symlinks flag to avoid unintended target file modification"],"goal":"Prevent arbitrary file overwrite (CVE-2026-28684) by refusing symlink traversal by default in set_key and unset_key in python-dotenv 1.2.2, requiring follow_symlinks=True to modify link targets","kind":"HOW","packages":["pkg:pypi/python-dotenv@1.2.2"],"schemaVersion":1,"symbols":["dotenv.set_key","dotenv.unset_key","dotenv.main.rewrite"]},"contractCommand":["python","test/contract.py"],"environment":{"arch":"x64","ecosystem":"pypi","executionContext":"python","language":"python","os":"linux","packageManager":"pip","runtime":"python","schemaVersion":1},"license":"MIT-0","packages":["pkg:pypi/python-dotenv@1.2.2"],"schemaVersion":1,"symbols":["dotenv.set_key","dotenv.unset_key","dotenv.main.rewrite"],"verifierAdapter":"python@1"}
python-dotenv==1.2.2
"""Source package for python-dotenv 1.2.2 bugfix contract sample."""
"""Environment file management utilities demonstrating python-dotenv 1.2.2 symlink safety semantics."""
from pathlib import Path
from typing import Optional, Tuple, Union
import dotenv
StrPath = Union[str, Path]
def update_environment_safely(
env_path: StrPath,
key: str,
value: str,
allow_symlink_traversal: bool = False,
) -> Tuple[Optional[bool], str, str]:
"""Updates an environment file key, explicitly specifying symlink follow policy.
In python-dotenv >= 1.2.2, follow_symlinks defaults to False to prevent CVE-2026-28684
arbitrary file overwrites via symlink traversal. Passing follow_symlinks=True is required
if modifying the symlink target is intended.
"""
return dotenv.set_key(
dotenv_path=env_path,
key_to_set=key,
value_to_set=value,
quote_mode="always",
follow_symlinks=allow_symlink_traversal,
)
def remove_environment_safely(
env_path: StrPath,
key: str,
allow_symlink_traversal: bool = False,
) -> Tuple[Optional[bool], str]:
"""Removes an environment variable key from an env file with explicit symlink policy."""
return dotenv.unset_key(
dotenv_path=env_path,
key_to_unset=key,
quote_mode="always",
follow_symlinks=allow_symlink_traversal,
)
"""Contract test for python-dotenv 1.2.2 symlink safety and parameter fixes (CVE-2026-28684)."""
import inspect
import os
import sys
import tempfile
from pathlib import Path
# Add project root to sys.path
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
import dotenv
import dotenv.main
from src.env_manager import remove_environment_safely, update_environment_safely
def test_follow_symlinks_parameter_signatures():
"""Verify set_key, unset_key, and rewrite expose the follow_symlinks parameter."""
set_sig = inspect.signature(dotenv.set_key)
assert "follow_symlinks" in set_sig.parameters, (
"dotenv.set_key missing 'follow_symlinks' parameter added in 1.2.2"
)
assert set_sig.parameters["follow_symlinks"].default is False, (
"dotenv.set_key follow_symlinks must default to False"
)
unset_sig = inspect.signature(dotenv.unset_key)
assert "follow_symlinks" in unset_sig.parameters, (
"dotenv.unset_key missing 'follow_symlinks' parameter added in 1.2.2"
)
assert unset_sig.parameters["follow_symlinks"].default is False, (
"dotenv.unset_key follow_symlinks must default to False"
)
rewrite_sig = inspect.signature(dotenv.main.rewrite)
assert "follow_symlinks" in rewrite_sig.parameters, (
"dotenv.main.rewrite missing 'follow_symlinks' parameter added in 1.2.2"
)
assert rewrite_sig.parameters["follow_symlinks"].default is False, (
"dotenv.main.rewrite follow_symlinks must default to False"
)
def test_set_key_default_refuses_symlink_traversal():
"""Verify default set_key does NOT follow symlinks and replaces link with regular file."""
with tempfile.TemporaryDirectory() as tmpdir:
real_env = os.path.join(tmpdir, "protected_target.env")
with open(real_env, "w", encoding="utf-8") as f:
f.write("API_KEY=production_secret\nLOG_LEVEL=info\n")
symlink_env = os.path.join(tmpdir, "app_link.env")
os.symlink(real_env, symlink_env)
assert os.path.islink(symlink_env), "Symlink creation failed"
# In python-dotenv 1.2.2, default follow_symlinks=False prevents touching the symlink target
success, key, val = update_environment_safely(
symlink_env, "API_KEY", "untrusted_override", allow_symlink_traversal=False
)
assert success is True
assert key == "API_KEY"
# Symlink itself is replaced by a regular file containing the updated value
assert not os.path.islink(symlink_env), (
"symlink should be replaced with a regular file when follow_symlinks=False"
)
with open(symlink_env, "r", encoding="utf-8") as f:
link_content = f.read()
assert "untrusted_override" in link_content
# Target file MUST remain unchanged
with open(real_env, "r", encoding="utf-8") as f:
target_content = f.read()
assert "API_KEY=production_secret" in target_content, (
"Target file was modified despite follow_symlinks=False"
)
assert "untrusted_override" not in target_content
def test_set_key_explicit_follow_symlinks_updates_target():
"""Verify set_key with follow_symlinks=True mutates the target and retains symlink."""
with tempfile.TemporaryDirectory() as tmpdir:
real_env = os.path.join(tmpdir, "real_target.env")
with open(real_env, "w", encoding="utf-8") as f:
f.write("DATABASE_URL=postgres://localhost/prod\n")
symlink_env = os.path.join(tmpdir, "app_link.env")
os.symlink(real_env, symlink_env)
success, key, val = update_environment_safely(
symlink_env, "DATABASE_URL", "postgres://localhost/dev", allow_symlink_traversal=True
)
assert success is True
assert os.path.islink(symlink_env), "Symlink should be preserved when follow_symlinks=True"
with open(real_env, "r", encoding="utf-8") as f:
target_content = f.read()
assert "DATABASE_URL='postgres://localhost/dev'" in target_content or "DATABASE_URL=postgres://localhost/dev" in target_content
def test_unset_key_symlink_traversal_control():
"""Verify unset_key obeys follow_symlinks flag for link replacement vs target modification."""
with tempfile.TemporaryDirectory() as tmpdir:
# Case A: follow_symlinks=False leaves target untouched
real_env = os.path.join(tmpdir, "target_a.env")
with open(real_env, "w", encoding="utf-8") as f:
f.write("REMOVE_ME=1\nKEEP_ME=1\n")
symlink_env = os.path.join(tmpdir, "link_a.env")
os.symlink(real_env, symlink_env)
success, key = remove_environment_safely(symlink_env, "REMOVE_ME", allow_symlink_traversal=False)
assert success is True
assert not os.path.islink(symlink_env), "Symlink must be replaced with regular file"
with open(real_env, "r", encoding="utf-8") as f:
target_content = f.read()
assert "REMOVE_ME=1" in target_content, "Target must not be modified when follow_symlinks=False"
# Case B: follow_symlinks=True mutates target
real_env_b = os.path.join(tmpdir, "target_b.env")
with open(real_env_b, "w", encoding="utf-8") as f:
f.write("REMOVE_ME=1\nKEEP_ME=1\n")
symlink_env_b = os.path.join(tmpdir, "link_b.env")
os.symlink(real_env_b, symlink_env_b)
success_b, key_b = remove_environment_safely(symlink_env_b, "REMOVE_ME", allow_symlink_traversal=True)
assert success_b is True
assert os.path.islink(symlink_env_b), "Symlink must remain intact"
with open(real_env_b, "r", encoding="utf-8") as f:
target_b_content = f.read()
assert "REMOVE_ME" not in target_b_content
assert "KEEP_ME=1" in target_b_content
if __name__ == "__main__":
test_follow_symlinks_parameter_signatures()
test_set_key_default_refuses_symlink_traversal()
test_set_key_explicit_follow_symlinks_updates_target()
test_unset_key_symlink_traversal_control()
print("Contract tests passed successfully.")