fix(security): durcir les placeholders URL documentaires
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -37,7 +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"(?x)^(?:\{[^{}\r\n]+\}|<[^<>\r\n]+>|\.+|[•…*]+)$")
|
||||
_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"})
|
||||
|
||||
|
||||
@@ -186,8 +194,11 @@ def find_secrets(
|
||||
)
|
||||
if is_literal_secret:
|
||||
findings.append(SecretFinding(relative_path, number, "affectation-litterale"))
|
||||
url_match = _URL_SECRET_RE.search(line)
|
||||
if url_match and not _URL_PLACEHOLDER_RE.fullmatch(url_match.group(1)):
|
||||
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))
|
||||
|
||||
|
||||
@@ -145,6 +145,48 @@ def test_main_ignores_documentation_url_placeholders(
|
||||
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:
|
||||
|
||||
Reference in New Issue
Block a user