transfer from private server/git server
This commit is contained in:
89
web/tests/test_security.py
Normal file
89
web/tests/test_security.py
Normal file
@@ -0,0 +1,89 @@
|
||||
"""
|
||||
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 !"
|
||||
Reference in New Issue
Block a user