CodeSampleX

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

  1. assert set_key, unset_key, and rewrite expose follow_symlinks parameter defaulting to False
  2. assert set_key on symlink replaces link with regular file and leaves target untouched when follow_symlinks=False
  3. assert set_key on symlink mutates target and preserves symlink when follow_symlinks=True
  4. 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

Descargar el artefacto de código fuente (tar.gz)

Código fuente

NOTES.md
# 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.
csx.json
{"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"}
requirements.txt
python-dotenv==1.2.2
src/__init__.py
"""Source package for python-dotenv 1.2.2 bugfix contract sample."""
src/env_manager.py
"""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,
    )
test/contract.py
"""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.")

Seeder de origen

csx-seed