security.rs
89 lignes · 4488 octets
//! Contenu de sécurité injecté dans le plan : garde-fous transversaux (statiques) //! et contrat de sécurité par écran (dérivé du catalogue). use crate::catalogue::{Channel, Functionality}; /// Identifiant de section pour le bloc statique (transversal + conventions). pub const GUIDES_ID: &str = "screen-select:security-guides"; /// Identifiant de section pour le contrat de sécurité par écran. pub const PER_SCREEN_ID: &str = "screen-select:security-per-screen"; /// Corps statique : sécurité transversale (obligatoire, hors écran) + conventions /// de rendu sûr. Indépendant de la sélection. pub fn guides_body() -> String { let mut s = String::new(); s.push_str("## Sécurité transversale (obligatoire, hors écran)\n\n"); s.push_str("- [ ] Middleware de headers sur TOUTES les réponses : `Content-Security-Policy` (viser sans `unsafe-inline`/`unsafe-eval`), `X-Frame-Options: DENY`, `X-Content-Type-Options: nosniff`, `Referrer-Policy: strict-origin-when-cross-origin`.\n"); s.push_str("- [ ] Validation de configuration au démarrage : refuser `ADMIN_PASSWORD=password` ; avertir si `APP_DEBUG=true`, `COOKIE_SECURE=false` ou `RUST_LOG=debug` en profil production ; secrets uniquement via variables d'environnement.\n"); s.push_str("- [ ] Cookies de session : `HttpOnly`, `Secure` en production, `SameSite`.\n"); s.push_str("- [ ] `cargo audit` régulier ; confirmer que le JWT est signé en **HMAC** (HS256/384/512), pas RSA (neutralise RUSTSEC-2023-0071) ; `accept_invalid_certs` SMTP/IMAP à `false` en production.\n\n"); s.push_str("## Conventions de rendu sûr\n\n"); s.push_str("- **Escaping contextuel** : ne jamais interpoler une valeur contrôlée par l'utilisateur dans un contexte JavaScript (`<script>`). L'échappement HTML d'Askama n'échappe PAS le contexte JS (le backslash notamment). Passer la valeur par un attribut `data-*` et la lire via `dataset`.\n"); s.push_str("- Données dynamiques d'API côté client : `textContent`, jamais `innerHTML`.\n"); s.push_str("- URLs : `encodeURIComponent` / helper `urlenc()` du core ; aucune donnée personnelle en query string.\n"); s.push_str("- Aucun filtre `|safe` Askama sur une valeur contrôlée par l'utilisateur.\n"); s } /// Corps dérivé du catalogue : un paragraphe par écran des fonctionnalités /// sélectionnées (niveau d'auth, CSRF si browser, pièges spécifiques). pub fn reference_body(selected: &[&'static Functionality]) -> String { let mut s = String::new(); s.push_str("## Contrat de sécurité par écran\n\n"); for f in selected { for sc in f.screens { s.push_str(&format!( "### {} (`{}`) — {}\n\n", sc.name, sc.suggested_route, sc.auth_level.label() )); if matches!(sc.channel, Channel::Browser) { s.push_str("- Canal *browser* ⇒ valider le token CSRF sur chaque `POST` (`validate_csrf_token`).\n"); } for note in sc.security_notes { s.push_str(&format!("- {note}\n")); } s.push('\n'); } } s } #[cfg(test)] mod tests { use super::*; use crate::catalogue; #[test] fn guides_couvre_les_axes_transversaux() { let g = guides_body(); assert!(g.contains("Sécurité transversale")); assert!(g.contains("Content-Security-Policy")); assert!(g.contains("X-Frame-Options")); assert!(g.contains("ADMIN_PASSWORD")); assert!(g.contains("HMAC")); assert!(g.contains("Conventions de rendu sûr")); assert!(g.contains("textContent")); } #[test] fn reference_rend_le_contrat_par_ecran() { let two_factor = catalogue::by_key("two_factor").unwrap(); let r = reference_body(&[two_factor]); assert!(r.contains("## Contrat de sécurité par écran")); assert!(r.contains("### TwoFactorSetup (`/settings/2fa`) — Authentifié")); // canal browser ⇒ rappel CSRF automatique assert!(r.contains("CSRF")); // piège spécifique repris du catalogue assert!(r.to_lowercase().contains("rawquery")); } #[test] fn reference_marque_le_niveau_admin() { let admin = catalogue::by_key("admin_users").unwrap(); let r = reference_body(&[admin]); assert!(r.contains("— Administrateur")); assert!(r.contains("ensure_admin")); } }
GitRust