1919
2020import logging
2121import os
22+ import re
2223import shlex
2324import tempfile
2425from pathlib import Path
2930 _fcntl = None
3031
3132_FILE_INJECTED_VALUES : dict [str , str ] = {}
33+ _ENV_KEY_RE = re .compile (r"^[A-Za-z_][A-Za-z0-9_]*$" )
3234_log = logging .getLogger (__name__ )
3335
3436
@@ -73,6 +75,7 @@ def load_secrets(path: Path | None = None) -> dict[str, str]:
7375
7476def write_secret (key : str , value : str , * , path : Path | None = None ) -> None :
7577 """Atomically write or replace one KEY=value entry in the secrets file."""
78+ _validate_secret_entry (key , value )
7679 target = path or secrets_path ()
7780 target .parent .mkdir (parents = True , exist_ok = True )
7881 lock_path = target .with_name (f"{ target .name } .lock" )
@@ -120,7 +123,7 @@ def write_secret(key: str, value: str, *, path: Path | None = None) -> None:
120123
121124
122125def _replace_or_append_secret (text : str , key : str , value : str ) -> str :
123- replacement = f" { key } = { shlex . quote ( value )} "
126+ replacement = _format_secret_line ( key , value )
124127 lines = text .splitlines ()
125128 replaced = False
126129 next_lines : list [str ] = []
@@ -137,6 +140,18 @@ def _replace_or_append_secret(text: str, key: str, value: str) -> str:
137140 return "\n " .join (next_lines ) + "\n "
138141
139142
143+ def _format_secret_line (key : str , value : str ) -> str :
144+ _validate_secret_entry (key , value )
145+ return f"{ key } ={ shlex .quote (value )} "
146+
147+
148+ def _validate_secret_entry (key : str , value : str ) -> None :
149+ if not _ENV_KEY_RE .fullmatch (key ):
150+ raise ValueError ("secret key must match [A-Za-z_][A-Za-z0-9_]*" )
151+ if any (char in value for char in ("\n " , "\r " , "\0 " )):
152+ raise ValueError ("secret value must not contain newline, carriage return, or NUL" )
153+
154+
140155def _secret_line_key (raw : str ) -> str | None :
141156 line = raw .strip ()
142157 if not line or line .startswith ("#" ) or "=" not in line :
0 commit comments