Aller au contenu

Security

Version française
Back to index


HTTP security headers

The following headers are applied to all responses via the Next.js configuration (next.config.ts):

Header Value Protection
X-Content-Type-Options nosniff Prevents MIME sniffing
X-Frame-Options DENY Prevents clickjacking (iframes)
Referrer-Policy strict-origin-when-cross-origin Controls referrer information
Permissions-Policy camera=(), geolocation=() Disables unused APIs

Note on Permissions-Policy: the microphone is allowed (required for the speaker interface). Only the camera and geolocation are explicitly forbidden.


Content Security Policy (CSP)

The CSP is configured dynamically in src/middleware.ts and src/proxy.ts. It includes the audio CDN domain.

Key directives

Content-Security-Policy:
  default-src 'self';
  script-src 'self' 'unsafe-inline';    ← unsafe-inline required for Next.js
  style-src 'self' 'unsafe-inline';
  img-src 'self' https: data:;
  media-src 'self' [AUDIO_CDN_DOMAIN] blob:;
  connect-src 'self' [AUDIO_CDN_DOMAIN];
  worker-src 'self' blob:;              ← for the Service Worker
  frame-ancestors 'none';              ← equivalent to X-Frame-Options: DENY

AUDIO_CDN_DOMAIN is injected at runtime from the environment variable. If this domain is absent from media-src, audio is blocked by the browser without any visible error to the user.

CSP diagnosis

If audio won't play, look in the browser console for:

Refused to connect to 'https://racines-s3.id2real.net/audios/...' 
because it violates the following Content Security Policy directive: "connect-src 'self'"

Solution: verify that AUDIO_CDN_DOMAIN is correctly set.


Authentication

See full documentation: 07-authentification.md

Protection summary: - Passwords hashed with PBKDF2 (100k iterations) - JWT HS256 in HttpOnly cookie (no XSS possible) - Constant-time comparison (no timing attack)


Input validation

API Routes

All API routes validate their inputs before processing. Typical example:

// Request body validation
const { name, email, subject, message } = await request.json();

if (!name || !email || !subject || !message) {
  return Response.json({ error: 'Missing fields' }, { status: 400 });
}

if (email && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
  return Response.json({ error: 'Invalid email' }, { status: 400 });
}

Parameterized SQL queries

Never string concatenation in SQL queries. Always use parameters:

// ✅ Correct
const result = await pool.query(
  'SELECT * FROM languages WHERE access_code = $1 AND is_active = true',
  [code]  // parameter
);

// ❌ Incorrect (SQL injection possible)
const result = await pool.query(
  `SELECT * FROM languages WHERE access_code = '${code}'`
);

Audio proxy protection — path traversal

The /api/audio/serve/[[...path]] endpoint proxies MinIO files. It validates the path to prevent directory traversal attacks:

// src/app/api/audio/serve/[[...path]]/route.ts
const path = params.path?.join('/') || '';

// Validation: no '..' in the path
if (path.includes('..') || path.startsWith('/')) {
  return Response.json({ error: 'Invalid path' }, { status: 400 });
}

Contact form — anti-spam

Honeypot

A hidden field (website) is present in the form HTML. Bots fill it in automatically; humans never see it.

// If the honeypot field is filled → silent rejection
if (body.website) {
  return Response.json({ success: true }); // Fake success response
}

Rate limiting

Maximum 1 message per hour per IP address: - IP extracted from the X-Forwarded-For header (via Traefik) - Counter stored in memory (or Redis if configured) - 429 response: "Too many messages sent, try again in an hour"


Secrets and environment variables

Absolute rules: - Never put secrets in source code - Never commit .env files to Git (.gitignore configured) - Production secrets are in GitLab CI/CD variables and the .app_env file on the server

Sensitive variables: - JWT_SECRET: if compromised, change it — all active sessions are invalidated - MINIO_SECRET_KEY and MINIO_ACCESS_KEY: full access to audio storage - PG_PASSWORD: database access


CORS

MinIO is configured with strict CORS rules via Traefik middleware: - Only the application URL(s) are allowed in Access-Control-Allow-Origin - Allowed methods: GET, HEAD

The Next.js server has no special CORS configuration (API routes are only accessed from the frontend or from the server itself).


Security checklist

Point Status
Passwords hashed (PBKDF2)
JWT in HttpOnly cookie
CSP configured
X-Frame-Options: DENY
Parameterized SQL queries
Server-side validation
Path traversal protection (audio proxy)
Anti-spam honeypot (contact)
Rate limiting (contact)
Secrets outside source code
No logging of sensitive data

Next steps