Bonnes pratiques de sécurité¶
Secure by design, validation des entrees, encodage des sorties et headers HTTP — les fondations d'un code résistant par construction.
Secure by design¶
Sécuriser par conception signifie intégrer la sécurité dans l'architecture avant d'écrire le code, pas après.
graph TD
A[Conception] -->|"threat model"| B[Architecture securisee]
B -->|"validation, encodage"| C[Developpement]
C -->|"SAST, revue"| D[Tests]
D -->|"DAST, pentest"| E[Production]
E -->|"monitoring, alertes"| A Principes fondateurs :
- Minimalisme : n'implémenter que ce qui est nécessaire (pas de fonctionnalités "au cas où")
- Separations des responsabilités : chaque composant a un périmètre clair et limite
- Defaults sécurisés : la configuration par défaut doit être la plus sure
- Échec sécurisé : en cas d'erreur, refuser l'accès plutôt que l'autoriser
Validation des entrees¶
Whitelist vs blacklist¶
La validation par whitelist (ce qui est autorise) est toujours supérieure à la blacklist (ce qui est interdit).
import re
from typing import Optional
# VULNERABLE — blacklist incomplete
def validate_username_bad(username: str) -> bool:
forbidden = ["<", ">", "'", '"', ";", "--"]
return not any(c in username for c in forbidden)
# Oubli de `\n`, `\r`, null bytes, caracteres unicode confusables...
# SECURISE — whitelist stricte
def validate_username(username: str) -> bool:
# Seulement alphanumerique, tiret et underscore, 3-32 caracteres
return bool(re.match(r'^[a-zA-Z0-9_\-]{3,32}$', username))
# SECURISE — validation avec Pydantic (recommande)
from pydantic import BaseModel, constr, validator
class UserCreate(BaseModel):
username: constr(pattern=r'^[a-zA-Z0-9_\-]{3,32}$')
email: str # Pydantic valide le format email automatiquement
age: int
@validator('age')
def age_must_be_positive(cls, v):
if v < 0 or v > 150:
raise ValueError("Age invalide")
return v
Validation côté serveur obligatoire¶
// VULNERABLE — validation cote client uniquement
// HTML: <input type="email" required maxlength="100">
// La validation HTML est triviale a contourner avec curl ou devtools
// SECURISE — toujours valider cote serveur
const { body, validationResult } = require('express-validator');
app.post('/register', [
body('email').isEmail().normalizeEmail(),
body('username').isAlphanumeric().isLength({ min: 3, max: 32 }),
body('age').isInt({ min: 0, max: 150 })
], (req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
// Traitement...
});
Encodage des sorties¶
L'encodage contextuel empêche l'interpretation des données comme du code.
from markupsafe import escape
import json
user_input = "<script>alert('xss')</script>"
# Contexte HTML — echapper les caracteres speciaux HTML
html_safe = escape(user_input)
# <script>alert('xss')</script>
# Contexte JavaScript — serialiser en JSON
js_safe = json.dumps(user_input)
# "\"<script>alert('xss')</script>\""
# Contexte SQL — toujours des parametres lies, jamais de concatenation
cursor.execute("SELECT * FROM users WHERE name = %s", (user_input,))
# Contexte URL — encoder les parametres
from urllib.parse import quote
url_safe = quote(user_input, safe='')
# %3Cscript%3Ealert%28%27xss%27%29%3C%2Fscript%3E
Requêtes parametrees¶
Jamais de concatenation de chaînes pour construire des requêtes. Toujours des parametres.
# VULNERABLE — tous types de DB
query = f"SELECT * FROM products WHERE category = '{category}' AND price < {max_price}"
# SECURISE — SQLite / SQLAlchemy raw
cursor.execute(
"SELECT * FROM products WHERE category = ? AND price < ?",
(category, max_price)
)
# SECURISE — SQLAlchemy ORM (prefere)
from sqlalchemy import select, and_
stmt = select(Product).where(
and_(Product.category == category, Product.price < max_price)
)
results = session.execute(stmt).scalars().all()
# SECURISE — PostgreSQL avec psycopg2
cursor.execute(
"SELECT * FROM products WHERE category = %s AND price < %s",
(category, max_price)
)
Headers de sécurité HTTP¶
Les headers HTTP sont la première ligne de defense côté navigateur.
# Flask — middleware de headers de securite
from flask import Flask
from flask_talisman import Talisman
app = Flask(__name__)
Talisman(app,
content_security_policy={
'default-src': "'self'",
'script-src': ["'self'", "'nonce-{nonce}'"],
'style-src': ["'self'", "'unsafe-inline'"],
'img-src': ["'self'", "data:", "https:"],
'connect-src': "'self'",
'frame-ancestors': "'none'"
},
force_https=True,
strict_transport_security=True,
strict_transport_security_max_age=31536000,
referrer_policy='strict-origin-when-cross-origin'
)
// Express — helmet.js (recommande)
const helmet = require('helmet');
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", "data:", "https:"],
frameAncestors: ["'none'"]
}
},
hsts: { maxAge: 31536000, includeSubDomains: true, preload: true },
referrerPolicy: { policy: "strict-origin-when-cross-origin" }
}));
Headers essentiels¶
| Header | Valeur recommandee | Ce qu'il previent |
|---|---|---|
Strict-Transport-Security | max-age=31536000; includeSubDomains | Downgrade HTTP, interception TLS |
Content-Security-Policy | Politique stricte par domaine | XSS, injection de scripts tiers |
X-Frame-Options | DENY ou SAMEORIGIN | Clickjacking |
X-Content-Type-Options | nosniff | MIME sniffing, XSS via upload |
Referrer-Policy | strict-origin-when-cross-origin | Fuite d'URL interne via Référer |
Permissions-Policy | Désactiver les APIs non utilisées | Accès camera/micro/geoloc non voulu |
Authentification et sessions¶
import secrets
import hashlib
from datetime import datetime, timedelta
# SECURISE — generation de token de session
def create_session_token() -> str:
return secrets.token_urlsafe(32) # 256 bits d'entropie
# SECURISE — cookie de session
@app.after_request
def set_secure_cookie(response):
# HttpOnly : inaccessible depuis JavaScript
# Secure : HTTPS uniquement
# SameSite=Strict : protection CSRF
response.set_cookie(
'session_id',
value=session_token,
httponly=True,
secure=True,
samesite='Strict',
max_age=3600 # 1h
)
return response
# SECURISE — invalidation de session a la deconnexion
def logout(session_id: str):
db.delete_session(session_id)
# Regenerer le cookie avec une valeur invalide et expiration passee
Erreurs d'authentification courantes
- Stocker le mot de passe en clair ou avec MD5/SHA1
- Ne pas invalider la session côté serveur à la déconnexion
- Permettre des sessions sans expiration
- Utiliser un identifiant predictable (incremental) comme token de session
- Ne pas régénérer le token de session après une elevation de privilege
Synthèse : pratique et prevention¶
| Pratique | Ce qu'elle previent |
|---|---|
| Validation whitelist | Injection, XSS, overflow, données incoherentes |
| Validation côté serveur | Bypass des contrôles client-side |
| Encodage HTML contextuel | XSS reflechi, stocke et DOM |
| Requêtes parametrees | SQL injection, NoSQL injection |
| CSP stricte | XSS, chargement de ressources malveillantes |
| HSTS | Downgrade HTTPS, attaques MITM |
| X-Frame-Options: DENY | Clickjacking |
| HttpOnly + Secure + SameSite | Vol de cookie, CSRF |
| Fail secure | Escalade de privileges sur erreur |
| Moindre privilege | Mouvement lateral, impact des compromissions |
Chapitre suivant : Cas avances — audit de code, pentest, conformité et bug bounty.