Security
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 | ✅ |