Aller au contenu principal
TechProjet

Sécuriser un portfolio : défense en profondeur

Comment j'ai implémenté une architecture de sécurité complète pour ce portfolio. OWASP Top 10, threat modeling STRIDE, et preuves d'attaques bloquées.

SecuriteOWASPNext.jsDevSecOps
Sécuriser un portfolio : défense en profondeur

Le contexte

Ce portfolio n'est pas juste une vitrine. C'est une démonstration de compétences en sécurité applicative. L'objectif : implémenter une architecture de sécurité complète, documentée et testable.

Ce post documente la conception défensive. Pour la validation offensive (j'ai pentest mon propre site quelques mois plus tard pour identifier les angles morts), voir le retour de pentest. Pour l'observabilité côté homelab qui ingère les events, voir le mini-SOC Wazuh.

Architecture de defense

3 couches de protection

Internet
    |
    v
[Cloudflare] -----> DDoS protection, TLS 1.3, WAF
    |
    v
[Next.js Middleware] --> Rate limiting, honeypots, validation
    |
    v
[Application] ----> Input sanitization, CSP, security headers

Chaque couche a un rôle spécifique. Si une couche est compromise, les autres restent actives.

Conformite OWASP Top 10

RisqueProtection
A01 - Broken Access ControlCSP strict + frame-ancestors 'none'
A02 - Cryptographic FailuresHSTS force + TLS 1.3 via Cloudflare
A03 - InjectionInput sanitization + pattern detection
A05 - Security MisconfigurationHeaders sécurité complets
A07 - Auth FailuresRate limiting (5 contacts/15min)
A09 - Logging FailuresLogs JSON structurés pour tous les events sécurité

Threat Model (STRIDE)

J'ai appliqué la méthodologie STRIDE pour identifier les menaces :

  • Spoofing : Honeypot dans le formulaire pour piéger les bots
  • Tampering : Sanitization de tous les inputs
  • Repudiation : Logging structuré de toutes les requêtes suspectes
  • Information Disclosure : Pas de stack traces, headers X-Powered-By désactivés
  • Denial of Service : Rate limiting multi-niveau (global + formulaire)
  • Elevation of Privilege : Pas d'admin, architecture statique

Implementations techniques

Middleware de sécurité (Next.js)

// Rate limiting global : 100 req/min
if (!checkRateLimit(ip)) {
  logSecurityEvent("RATE_LIMIT_EXCEEDED", ip, pathname);
  return new NextResponse("Too Many Requests", { status: 429 });
}

// Rate limiting contact : 5 tentatives/15min
if (!checkContactRateLimit(ip)) {
  logSecurityEvent("CONTACT_RATE_LIMIT", ip, pathname);
  return new NextResponse("Too Many Requests", { status: 429 });
}

Honeypots

Des endpoints pièges détectent les scans malveillants :

const HONEYPOT_PATHS = [
  "/wp-admin", "/.env", "/phpinfo.php",
  "/phpmyadmin", "/.git/config", "/backup.sql"
];

if (isHoneypotPath(pathname)) {
  logSecurityEvent("HONEYPOT_TRIGGERED", ip, pathname);
  blockedIPs.set(ip, Date.now() + BLOCK_DURATION);
  return new NextResponse("Not Found", { status: 404 });
}

Quand un scanner teste /wp-admin, l'IP est bloquée 15 minutes.

Détection de patterns malveillants

const SUSPICIOUS_PATTERNS = [
  // SQL Injection
  /('|")\s*(or|and)\s*('|")?1('|")?\s*=\s*('|")?1/i,
  /union\s+(all\s+)?select/i,

  // XSS
  /<script[\s>]/i,
  /javascript:/i,

  // Path Traversal
  /\.\.\//,
  /%2e%2e%2f/i,
];

if (hasSuspiciousPattern(fullUrl)) {
  logSecurityEvent("SUSPICIOUS_REQUEST", ip, pathname);
  return new NextResponse("Bad Request", { status: 400 });
}

Honeypot formulaire

Un champ invisible piège les bots qui remplissent tous les champs :

// Honeypot check - if filled, it's a bot
if (honeypot) {
  // Silently "succeed" to not reveal the trap
  setFormStatus("success");
  return;
}

Validation et sanitization des inputs

// Sanitize HTML entities
function sanitizeHtml(input: string): string {
  return input.replace(/[&<>"'`=/]/g, (char) => HTML_ENTITIES[char]);
}

// Validate before sending
const validation = validateContactInput(formData);
if (!validation.valid) {
  setFormError(validation.errors.join(". "));
  return;
}

Security Headers

// Headers configurés dans next.config.ts
{
  "Strict-Transport-Security": "max-age=63072000; includeSubDomains; preload",
  "X-Frame-Options": "SAMEORIGIN",
  "X-Content-Type-Options": "nosniff",
  "X-XSS-Protection": "1; mode=block",
  "Referrer-Policy": "strict-origin-when-cross-origin",
  "Cross-Origin-Opener-Policy": "same-origin",
  "Cross-Origin-Resource-Policy": "same-origin",
  "Content-Security-Policy": "default-src 'self'; frame-ancestors 'none'..."
}

Exemples d'attaques bloquees

AttaquePayloadResultat
SQL Injection' OR '1'='1400 Bad Request
XSS<script>alert(1)</script>Input sanitized
Path Traversal../../../etc/passwd400 Bad Request
Rate Limit100+ req/min429 + Retry-After
ScannerGET /wp-adminLogged + 404 + IP blocked
Bot spamHoneypot filledSilent success (no email)

Logging securite

Chaque événement de sécurité est logué en JSON structuré :

Événements tracés :

  • RATE_LIMIT_EXCEEDED
  • HONEYPOT_TRIGGERED
  • SUSPICIOUS_REQUEST
  • IP_BLOCKED
  • CONTACT_RATE_LIMIT

Choix techniques justifiés

Pourquoi Next.js middleware ? Exécution avant le rendu, dans le même runtime que l'app. Bloque les attaques au plus tôt côté origine (Cloudflare en amont filtre déjà le gros du bruit, le middleware durcit ce qui passe).

Pourquoi Formspree ? Service externe = pas de backend à sécuriser, pas de DB exposée. Moins de surface d'attaque, plus simple à auditer.

Pourquoi rate limiting in-memory ? Le portfolio tourne en single container Coolify (un seul process Node), donc une Map in-memory suffit. Redis serait overkill : Cloudflare gère déjà le rate limit volumétrique en amont, le middleware ne fait que la couche fine au-dessus.

Limites connues

  • Rate limit reset au redémarrage du container (deploy ou OOM)
  • Pas de WAF applicatif complet (Cloudflare en amont assure cette couche)
  • In-memory storage = pas de persistence cross-restart
  • Fenêtre fixe (pas sliding window) : burst théorique 2x à la frontière de minute. Mitigé par CF en amont et par les Wazuh aggregate rules en aval (cf pentest qui valide ce trade-off)

Ces limites sont acceptables pour un portfolio personnel avec Cloudflare devant et un mini-SOC derrière.

Integration avec mon mini-SOC homelab

Depuis la migration Vercel vers self-hosted (mai 2026), les logs de sécurité du portfolio remontent dans ma stack d'observabilité homelab :

  • Vector sur le serveur Hetzner ship les container logs Coolify vers Loki centralisé (LXC dédié sur mon Proxmox local)
  • Grafana affiche un dashboard temps réel des events HONEYPOT_TRIGGERED, RATE_LIMIT_EXCEEDED, SUSPICIOUS_REQUEST
  • Les patterns suspects critiques peuvent déclencher une alerte Telegram via la même infra Wazuh qui monitore tout le homelab

Le pipeline complet : Next.js middleware → console.log JSON → Docker stdout → Vector → Loki → Grafana → Telegram (sur seuils définis).

Détails techniques de cette stack dans le post dédié au mini-SOC homelab avec les rules custom et l'architecture complète.

Ce que j'ai appris

  1. Défense en profondeur : plusieurs couches indépendantes
  2. STRIDE : méthodologie structurée pour identifier les menaces
  3. OWASP Top 10 : checklist concrète de vulnérabilités à couvrir
  4. Security logging : les logs structurés changent tout pour le monitoring
  5. Minimal attack surface : pas de backend = moins de risques
  6. Observabilité centralisée : ship les events sécurité vers une stack qui les analyse, sinon les logs restent dans le container et meurent au prochain deploy

La sécurité est intégrée nativement dans tous les sites que je livre, avec observabilité optionnelle vers une stack centralisée si tu veux du monitoring continu. Consultez mon parcours technique pour voir mes autres projets en production, ou demandez un devis pour un site sécurisé de A à Z.

Besoin d'un site vitrine professionnel ?

Je crée des sites sur mesure pour les entreprises à Caen et partout en France. Devis gratuit, réponse sous 24h.