docs: verify and refactor documentation to match current codebase

This commit is contained in:
Pontoporeia
2026-08-24 11:31:38 +02:00
parent b2cdbd0174
commit e9747edce0
17 changed files with 1006 additions and 1442 deletions
+71 -58
View File
@@ -1,82 +1,95 @@
# Security
Vulnerability analysis and resolution status for posterg-website.
Current security posture for XAMXAM.
> Based on security audit (2026-02-08). All items tracked below.
> This supersedes the earlier `security.md` (2026-02-08 audit). The original
> 16-item audit is closed; the items below reflect the current state.
---
## Resolved
## Authentication — admin
### Infrastructure / Deployment
- **PHP session auth:** `app/src/AdminAuth.php`. Password-only (no username).
Credentials previously in a gitignored PHP file; the current build stores the
bcrypt hash in `site_settings.admin_password_hash` (manageable from
`/admin/account`).
- `AdminAuth::requireLogin()` guards every admin action/route.
- Session cookies hardened: `HttpOnly`, `SameSite=Strict`, `Secure`,
`Path=/admin`; regenerated on login.
- nginx `auth_basic` layer has been removed; the PHP session layer is the only
gate. (LDAP-based login is a proposed future enhancement — see
`LDAP_AUTH_PLAN.md` / `LDAP_SPEC.md`. It is **not** implemented.)
| # | Issue | Severity | Resolution |
|---|-------|----------|------------|
| 1 | No HTTPS — admin credentials exposed in transit | 🔴 CRITICAL | TLS terminated upstream by reverse proxy. nginx.conf doesn't need to handle TLS directly. |
| 3 | Uploaded files stored inside webroot | 🟠 HIGH | Storage moved to `STORAGE_ROOT` (`/var/www/posterg/storage/`), defined in `config/bootstrap.php`. |
| 4 | File path mismatch — media broken & insecure | 🟠 HIGH | DB paths now storage-relative. New `public/media.php` serves files safely. `memoire.php` and `search.php` use `/media.php?path=…`. Cover recording fixed. |
| 5 | Rate limiter bypassed by IP spoofing (`X-Forwarded-For`) | 🟠 HIGH | `src/RateLimit.php` `getClientIdentifier()` uses `REMOTE_ADDR` only. |
| 6 | `.htaccess` rules silently ignored by nginx | 🟠 HIGH | All rules ported to `nginx/posterg.conf`. See `nginx/HTACCESS_TO_NGINX.md`. |
| 13 | Deprecated `X-XSS-Protection` header | 🔵 LOW | Removed from `nginx/posterg.conf`. |
## Transport & headers
### Frontend / Assets
Enforced in `nginx/xamxam.conf` (see `nginx/docs/SECURITY_HEADERS.md`):
| # | Issue | Severity | Resolution |
|---|-------|----------|------------|
| 10 | CDN stylesheet without SRI | 🟡 MEDIUM | CDN will not be used in production. Self-hosted, eliminating supply-chain risk. |
- **HSTS** (`Strict-Transport-Security`, 730 days, preload)
- **CSP** — `default-src 'self'; … frame-ancestors 'none'` on public pages;
`frame-ancestors 'self'` where the app embeds allowed content; admin CSP
includes `script-src 'unsafe-inline'` for the OverType editor. `object-src 'none'`.
- `X-Frame-Options: DENY` (clickjacking)
- `X-Content-Type-Options: nosniff`
- `Referrer-Policy: strict-origin-when-cross-origin`
- `Permissions-Policy` (geo/mic/camera disabled)
- `Cross-Origin-Opener-Policy` / `Cross-Origin-Resource-Policy: same-origin`
- `X-Robots-Tag: noindex, nofollow` on `/admin/`
### Code Quality / Defence in Depth
`X-XSS-Protection` is intentionally **omitted** (deprecated; see
`nginx/docs/SECURITY_HEADERS.md` for rationale).
| # | Issue | Severity | Resolution |
|---|-------|----------|------------|
| 14 | Missing `rel="noreferrer"` on external links | 🔵 LOW | `rel="noopener noreferrer"` applied in `public/admin/thanks.php`. |
| 15 | Unescaped integer outputs | 🔵 LOW | Explicit `(int)` casts added in `public/index.php` and `public/search.php`. |
| 16 | Redundant `DATABASE_PATH` constant | 🔵 LOW | Removed from `config/bootstrap.php`. |
## Rate limiting
### Admin Panel — Authentication & Sessions
Defined in the nginx config `limit_req_zone` and enforced by
`app/src/RateLimit.php`:
| # | Issue | Severity | Resolution |
|---|-------|----------|------------|
| 2 | No PHP-level authentication in admin | 🔴 CRITICAL | `src/AdminAuth.php` implements session guard with `password_verify` + `session_regenerate_id`. All admin files call `AdminAuth::requireLogin()`. Credentials in gitignored `config/admin_credentials.php`. No-op when constant absent (dev/cli-server). |
| 8 | Session cookies not hardened | 🟡 MEDIUM | Resolved with #2. `AdminAuth::startSession()` sets `HttpOnly=true`, `SameSite=Strict`, `Secure=true` (off on cli-server), `Path=/admin`, `Lifetime=0`. |
- General requests: `30 r/m`
- Search endpoint: `30 r/m`
- Admin panel: `300 r/m` (burst 30)
---
The PHP limiter uses `REMOTE_ADDR` only (not `X-Forwarded-For`) to avoid IP
spoofing.
## In Progress
## Files & storage
| # | Issue | Severity | Status |
|---|-------|----------|--------|
| 7 | LIKE wildcard injection in admin search | 🟡 MEDIUM | Public `Database::searchTheses()` escapes `%` and `_` correctly. Same pattern must be applied to admin search and any other raw LIKE queries. |
- Uploads live **outside the webroot** under `app/storage/` (`tfe/`, `theses/`),
served on demand via `MediaController`/`FileAccessController` through
controlled endpoints (`/media`), not direct static access.
- nginx blocks `/storage`, `/src`, `/templates`, DB/sql/env/md files, and hidden
files. The DocumentRoot is `app/public/` only.
- Restricted-file downloads are gated by a request/approval/token flow
(`file_access_*` tables).
- Logs write to `app/storage/logs/` — outside the webroot, not publicly served.
---
## Injection & output
## Not Yet Implemented
- **SQL:** all queries use PDO prepared statements.
- **LIKE wildcards:** `Database::escapeLikeString()` escapes `%` and `_`
(public search and related queries).
- **XSS:** `htmlspecialchars()` on all user-controlled output; integer/ID
inputs cast.
- **CSRF:** per-session tokens (`bin2hex(random_bytes(32))`), compared with
`hash_equals()`.
- **File uploads:** MIME validated (`finfo`); FilePond prevalidation +
server-side checks in `FilepondHandler`.
- **Markdown/HTML:** user content (pages, help blocks) rendered via
`league/commonmark`; HTML in admin-editable content is expected.
| # | Issue | Severity | Files |
|---|-------|----------|-------|
| 11 | Missing Content-Security-Policy on public pages | 🟡 MEDIUM | `nginx/posterg.conf` → add CSP header to main server block |
| 9 | `error.log` in web-accessible path | 🟡 MEDIUM | `public/admin/actions/formulaire.php` → use absolute path outside webroot |
| 12 | CSV import missing server-side MIME validation | 🟡 MEDIUM | `public/admin/import.php` → add `finfo` MIME check |
## Defence in depth / misc
---
- External links use `rel="noopener noreferrer"`.
- Decryption/`Crypto` failures are logged without leaking secrets; SMTP
password is stored encrypted in `smtp_settings` and rotated via
`just reencrypt-password`.
- Admin operations are recorded in `admin_audit_log` (resource, action,
status, IP, User-Agent).
## Priority Order
## Areas to keep monitored
1. ~~🔴 CRITICAL~~ — All done (items 1–2)
2. 🟡 **MEDIUM** — Items 7, 9, 11, 12 remaining
3. ~~🔵 LOW~~ — All done (items 13–16)
- Tightening the public CSP (`frame-ancestors 'none'` vs `'self'` on embed
routes) is an active topic — see `TODO.md`.
- Sensitive file downloads and their expiry/token handling are worth periodic
review as usage grows.
---
## Good Practices Already in Place
- ✅ SQL injection: all queries use PDO prepared statements
- ✅ XSS output: `htmlspecialchars()` on all user-controlled output
- ✅ CSRF: tokens with `bin2hex(random_bytes(32))`, validated with `hash_equals()`
- ✅ File upload: MIME type validated with `finfo`
- ✅ Input validation: year, IDs, pagination cast to integers
- ✅ LIKE wildcard escaping in public search (`Database::escapeLikeString`)
---
*Last updated: 2026-02-08*
See also: `nginx/docs/SECURITY_HEADERS.md`, `nginx/docs/PHP_AUTH_LAYER.md`,
`nginx/docs/PRODUCTION_DEPLOYMENT.md`.