Files
gesthub/web/tests/test_security.py
2026-08-21 09:12:02 +02:00

90 lines
3.7 KiB
Python

"""
Tests de sécurité — TS-01 à TS-06 (Bloc 1 - C8 ; OWASP Top 10 2021).
Approche adversariale : se mettre dans la peau d'un attaquant pour chaque
vecteur identifié dans le tableau de risques du dossier technique.
"""
import io
import os
import yaml
from werkzeug.datastructures import FileStorage
from models import annonce_model
from services import annonce_service, fichier_service
# --- TS-01 : Injection SQL (A03:2021) ----------------------------------------
def test_ts01_injection_sql_dans_titre_est_neutralisee(db):
payload = "Test'; DROP TABLE annonces; --"
new_id = annonce_service.create_annonce(db, payload, "contenu", "sub-attaquant")
# La requête paramétrée traite le payload comme une simple chaîne : la
# table annonces existe toujours et contient bien la ligne créée.
row = annonce_model.get(db, new_id)
assert row["titre"] == payload
all_rows = annonce_model.fetch_all(db)
assert len(all_rows) >= 1 # la table n'a pas été supprimée
# --- TS-02 : Accès sans token (A01:2021 — Broken Access Control) ------------
def test_ts02_acces_sans_token_refuse(client):
resp = client.get("/api/fichiers")
assert resp.status_code == 401
resp2 = client.post("/api/annonces", json={"titre": "x", "contenu": "y"})
assert resp2.status_code == 401
# --- TS-03 : Cookie de session falsifié (token forgé) -----------------------
def test_ts03_cookie_session_falsifie_refuse(client):
client.set_cookie("session", "valeur-totalement-forgee-non-signee")
resp = client.get("/api/annonces")
# Flask ne parvient pas à décoder un cookie non signé par SECRET_KEY :
# la session est traitée comme vide -> 401, pas d'accès accordé.
assert resp.status_code == 401
# --- TS-04 : XSS stocké (A03:2021) ------------------------------------------
def test_ts04_contenu_avec_script_est_echappe_au_rendu(app, db):
payload = "<script>alert('xss')</script>"
annonce_service.create_annonce(db, "Titre", payload, "sub-1")
with app.test_request_context():
from flask import render_template_string
rendered = render_template_string("{{ contenu }}", contenu=payload)
# Jinja2 (autoescape actif par défaut sur .html) transforme les balises
# en entités HTML : le script n'est jamais exécutable tel quel.
assert "<script>" not in rendered
assert "&lt;script&gt;" in rendered
# --- TS-05 : Accès aux ressources d'autrui (A01:2021) -----------------------
def test_ts05_telechargement_fichier_dautrui_refuse(db, tmp_path, monkeypatch):
from config import Config
monkeypatch.setattr(Config, "UPLOAD_DIR", str(tmp_path))
fs = FileStorage(stream=io.BytesIO(b"contenu prive"), filename="prive.pdf", content_type="application/pdf")
fichier_id = fichier_service.handle_upload(db, fs, "sub-proprietaire", ip="127.0.0.1")
try:
fichier_service.prepare_download(db, fichier_id, "sub-attaquant", is_admin=False)
assert False, "le téléchargement aurait dû être refusé"
except PermissionError:
pass
# Le propriétaire, lui, peut télécharger son propre fichier.
path, name = fichier_service.prepare_download(db, fichier_id, "sub-proprietaire", is_admin=False)
assert os.path.exists(path)
# --- TS-06 : Exposition des ports internes (A05:2021 — Security Misconfig) -
def test_ts06_seul_caddy_expose_des_ports_publics():
compose_path = os.path.join(os.path.dirname(__file__), "..", "..", "docker-compose.yml")
with open(compose_path) as f:
compose = yaml.safe_load(f)
for service_name, service_def in compose["services"].items():
if service_name == "caddy":
assert "ports" in service_def
else:
assert "ports" not in service_def, f"{service_name} expose un port public !"