fix(security) : ignorer les placeholders URL documentaires #36

Open
Codex wants to merge 2 commits from fix/issue-34-secret-placeholder into main
3 changed files with 90 additions and 1 deletions
+7
View File
@@ -42,6 +42,13 @@ bloquent donc pas le déploiement. Il ne valide ni les valeurs ni les permission
du fichier d'environnement. Pour analyser seulement le contenu indexé avant un
commit, utilisez `scripts/check_secrets.py --staged`.
Dans une URL, le contrôle exempte uniquement les placeholders documentaires
explicitement reconnus : `{token}`/`{jeton}` (et les quelques noms documentaires
équivalents), leur forme `<token>`, les suites de trois points ou les caractères
de masquage `•`/`…`. Les valeurs entre accolades ou chevrons au contenu arbitraire,
les astérisques et toute valeur réelle restent signalés. Un même paramètre URL est
contrôlé même si un autre paramètre sensible de la ligne est déjà un placeholder.
Le dry-run autorise les lectures nécessaires à la simulation, mais n'applique aucune sortie
CalDAV/XMPP et ne modifie aucun état local persistant. L'état RSS reste limité à la mémoire du
processus. Le mode `PRONOTE_AUTH_MODE=qr_token` est incompatible avec cette garantie : la commande
+14 -1
View File
@@ -37,6 +37,15 @@ _URL_SECRET_RE = re.compile(
r"(?ix)[?&](?:api[_-]?key|access[_-]?token|auth(?:orization)?|icalsecurise|password|secret|token)"
r"=([^&#\s]{3,})"
)
_URL_PLACEHOLDER_RE = re.compile(
r"(?ix)^(?:"
r"\{(?:token|jeton|secret|valeur|value|param(?:etre)?|version|prenom|"
r"etablissement|hote|host)\}|"
r"<(?:token|jeton|secret|valeur|value|param(?:etre)?|version|prenom|"
r"etablissement|hote|host)>|"
r"\.{3,}|[•…]+"
r")$"
)
_EXTRA_NAMES = frozenset({"pronote_sync"})
@@ -185,7 +194,11 @@ def find_secrets(
)
if is_literal_secret:
findings.append(SecretFinding(relative_path, number, "affectation-litterale"))
if _URL_SECRET_RE.search(line):
has_real_url_secret = any(
not _URL_PLACEHOLDER_RE.fullmatch(match.group(1))
for match in _URL_SECRET_RE.finditer(line)
)
if has_real_url_secret:
findings.append(SecretFinding(relative_path, number, "parametre-url"))
return sorted(findings, key=lambda finding: (str(finding.path), finding.line, finding.rule))
+69
View File
@@ -118,6 +118,75 @@ def test_main_detects_sensitive_url_parameter(
assert sentinel not in output
def test_main_ignores_documentation_url_placeholders(
secret_checker: ModuleType, tmp_path: Path, capsys: CaptureFixture[str]
) -> None:
"""Ignore les marqueurs de remplacement utilisés dans une documentation.
:param secret_checker: Module du script sous test.
:param tmp_path: Répertoire temporaire représentant un dépôt.
:param capsys: Fixture de capture de sortie.
:return: None
"""
(tmp_path / "guide.md").write_text(
"\n".join(
(
"https://example.invalid/?icalsecurise={jeton}",
"https://example.invalid/?icalsecurise=••••••••",
"https://example.invalid/?icalsecurise=<token>",
"https://example.invalid/?icalsecurise=...",
)
)
+ "\n",
encoding="utf-8",
)
assert secret_checker.main([], root=tmp_path) == 0
assert "OK:" in capsys.readouterr().out
def test_main_detects_a_real_url_secret_after_a_placeholder(
secret_checker: ModuleType, tmp_path: Path, capsys: CaptureFixture[str]
) -> None:
"""Détecte un secret réel placé après un placeholder sur la même URL.
:param secret_checker: Module du script sous test.
:param tmp_path: Répertoire temporaire représentant un dépôt.
:param capsys: Fixture de capture de sortie.
:return: None
"""
sentinel = "m14-url-after-placeholder-sentinel"
(tmp_path / "guide.md").write_text(
f"https://example.invalid/?token={{jeton}}&api_key={sentinel}\n",
encoding="utf-8",
)
assert secret_checker.main([], root=tmp_path) == 1
output = capsys.readouterr().out
assert "guide.md:1 (parametre-url)" in output
assert sentinel not in output
@pytest.mark.parametrize("value", ["<ghp_…>", "***"])
def test_main_rejects_ambiguous_url_placeholders(
secret_checker: ModuleType, tmp_path: Path, capsys: CaptureFixture[str], value: str
) -> None:
"""Refuse les valeurs qui ne sont pas des placeholders documentaires fermés.
:param secret_checker: Module du script sous test.
:param tmp_path: Répertoire temporaire représentant un dépôt.
:param capsys: Fixture de capture de sortie.
:param value: Valeur ambiguë à ne pas exempter.
:return: None
"""
(tmp_path / "guide.md").write_text(
f"https://example.invalid/?token={value}\n", encoding="utf-8"
)
assert secret_checker.main([], root=tmp_path) == 1
assert "guide.md:1 (parametre-url)" in capsys.readouterr().out
def test_staged_mode_inspects_only_paths_provided_by_git(
secret_checker: ModuleType, tmp_path: Path, capsys: CaptureFixture[str]
) -> None: