90 lines
3.7 KiB
Python
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 "<script>" 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 !"
|