mirror of
https://codeberg.org/PostERG/xamxam.git
synced 2026-09-25 01:53:03 +02:00
139 lines
5.8 KiB
Markdown
139 lines
5.8 KiB
Markdown
# Security
|
|
|
|
Current security posture for XAMXAM.
|
|
|
|
> This supersedes the earlier `security.md` (2026-02-08 audit). The original
|
|
> 16-item audit is closed; the items below reflect the current state.
|
|
|
|
---
|
|
|
|
## Authentication — admin
|
|
|
|
- **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.
|
|
- **Session timeouts** (server-side, enforced by `AdminAuth::enforceSessionTimeout()`
|
|
on every gated request):
|
|
- **Idle timeout** — 30 min without activity → session destroyed, redirected
|
|
to login.
|
|
- **Absolute timeout** — 12 h since login → forced re-login, regardless of
|
|
activity.
|
|
- Session ID rotated periodically (every 30 min) to limit fixation/replay.
|
|
- The session *cookie* `lifetime` is 7 days, but that is only an upper bound
|
|
on browser retention — the actual session is bounded by the two timeouts above.
|
|
- **PHP-FPM session GC tuning** — see below. The app enforces its own timeouts,
|
|
so `session.gc_maxlifetime` must be ≥ the 12 h absolute timeout or PHP would
|
|
reap active sessions early.
|
|
- 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.md`](ldap.md). It is **not** implemented.)
|
|
|
|
### PHP-FPM session GC configuration
|
|
|
|
The admin session's lifetime is enforced *by the app* (the timeouts above), so
|
|
PHP's own session garbage collector must not reap active sessions before the
|
|
12 h absolute timeout. The following is provisioned by
|
|
`scripts/deploy-server.sh` (run via `just deploy-nginx`):
|
|
|
|
**File:** `/etc/php/8.4/fpm/conf.d/zz-xamxam-session.ini`
|
|
|
|
```ini
|
|
; XAMXAM session tuning.
|
|
; AdminAuth enforces its own idle/absolute timeouts (30 min / 12 h), so
|
|
; gc_maxlifetime must be >= the absolute timeout or PHP would reap active
|
|
; sessions from under the app.
|
|
session.gc_maxlifetime = 43200
|
|
session.gc_probability = 1
|
|
session.gc_divisor = 100
|
|
```
|
|
|
|
- `gc_maxlifetime = 43200` (12 h) — matches `ABSOLUTE_TIMEOUT_SECONDS`;
|
|
**must stay ≥ the app's absolute timeout**.
|
|
- `gc_probability = 1` / `gc_divisor = 100` — re-enables the GC (default was
|
|
`gc_probability = 0`, i.e. disabled, so stale session files were never reaped).
|
|
- Applied on FPM reload (`systemctl reload php8.4-fpm`), done by the deploy script.
|
|
|
|
> ⚠️ Keep these three knobs in sync with the constants in
|
|
> `app/src/AdminAuth.php` (`IDLE_TIMEOUT_SECONDS`, `ABSOLUTE_TIMEOUT_SECONDS`,
|
|
> `COOKIE_LIFETIME_SECONDS`). If you raise the app's absolute timeout beyond
|
|
> 12 h, raise `gc_maxlifetime` to match.
|
|
|
|
## Transport & headers
|
|
|
|
Enforced in `nginx/xamxam.conf` (see `nginx/docs/SECURITY_HEADERS.md`):
|
|
|
|
- **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/`
|
|
|
|
`X-XSS-Protection` is intentionally **omitted** (deprecated; see
|
|
`nginx/docs/SECURITY_HEADERS.md` for rationale).
|
|
|
|
## Rate limiting
|
|
|
|
Defined in the nginx config `limit_req_zone` and enforced by
|
|
`app/src/RateLimit.php`:
|
|
|
|
- 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.
|
|
|
|
## Files & storage
|
|
|
|
- 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 `/var/log/xamxam/` in production (and `app/storage/logs/`
|
|
only in dev/cli-server) — outside the webroot, not publicly served.
|
|
|
|
## Injection & output
|
|
|
|
- **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.
|
|
|
|
## 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).
|
|
|
|
## Areas to keep monitored
|
|
|
|
- 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.
|
|
|
|
See also: `nginx/docs/SECURITY_HEADERS.md`, `nginx/docs/PHP_AUTH_LAYER.md`,
|
|
`nginx/docs/PRODUCTION_DEPLOYMENT.md`.
|