fix(security): authorize site uploads and harden tokens, media and request identity
This commit is contained in:
1 parent
52f6d1491f
commit
8abfe352ef
70 files changed
+1609
-204
No files matched your search
@@ -16,6 +16,14 @@ Credentials are entered on the host, never in the dashboard. Do not paste `.env`
|
||||
|
||||
After startup, visit `/admin/devops/installation` with the appropriate permission. Verify database, Redis, storage and migration status. Worker heartbeat is a separate runtime signal: a healthy HTTP endpoint does not prove an import worker is processing jobs. Check the worker status and investigate a missing/stale heartbeat before scheduling imports. The dashboard is read-only and cannot start Docker, upgrade the host or grant registry access.
|
||||
|
||||
## Client IP trust at the reverse proxy
|
||||
|
||||
The application validates and normalizes client addresses from `cf-connecting-ip`, the first `x-forwarded-for` entry, then `x-real-ip`. It never accepts `x-real-client-ip`; that legacy derived header is also stripped by the Next.js proxy. Missing or invalid addresses resolve to `0.0.0.0` for rate limits and audit records. API routes use the same resolver even though they do not run through the Next.js proxy.
|
||||
|
||||
These headers are trustworthy only when the ingress sanitizes them. Configure the reverse proxy to discard client-supplied forwarding/derived headers and replace the accepted address from a verified connection or a specifically trusted upstream proxy. Do not append an untrusted incoming `x-forwarded-for` chain and then treat its first entry as authoritative. Forward `cf-connecting-ip` only after verifying that it came through your trusted CDN path; otherwise remove it.
|
||||
|
||||
Restrict direct access to the application port so requests must pass through that ingress. The provided Compose file uses host networking with `HOSTNAME=0.0.0.0`; it does not enforce this restriction or provision nginx/Traefik trust rules. Verify the host firewall and actual reverse-proxy configuration before relying on client IPs for blocking, auditing or abuse limits. Repository tests prove rejection of the derived-header bypass and malformed addresses; they do not certify the deployed forwarding trust chain.
|
||||
|
||||
## Routine and selected-release updates
|
||||
|
||||
```sh
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
# Personal API token scopes
|
||||
|
||||
Public API bearer authentication accepts only tokens owned by the exact `App\Models\User` model. The owner ID must be a positive, safely representable user ID, and the token must satisfy its existing expiration check. Both plaintext tokens and the existing `{id}|{plaintext}` request format remain supported; only the SHA-256 hash is looked up in the database.
|
||||
|
||||
The `abilities` column must contain a non-empty JSON array of non-empty strings. Null, malformed JSON, non-array JSON, empty arrays, non-string entries, and entries with surrounding whitespace are rejected. A valid `"*"` entry grants access to all existing bearer-protected endpoints. Other permissions match exactly: there is no `tickets:*` expansion, implicit read/write inheritance, or fallback to unrestricted access.
|
||||
|
||||
| Ability | Endpoint access |
|
||||
| --- | --- |
|
||||
| `tickets:read` | `GET /api/tickets`, `GET /api/tickets/{id}` |
|
||||
| `tickets:write` | `POST /api/tickets`, `POST /api/tickets/{id}/reply` |
|
||||
| `articles:write` | `POST /api/articles/{slug}/comment` |
|
||||
| `radio:read` | `GET /api/radio/points` |
|
||||
| `radio:write` | `POST /api/radio/shouts` |
|
||||
| `badges:read` | Personal viewer data in `GET /api/badges/leaderboard` |
|
||||
|
||||
For example, `["tickets:read","radio:read"]` allows reading the owner's tickets and radio points. It cannot create tickets, send replies, post article comments, or send radio shouts. Endpoint ownership checks and rate limits still apply after scope authorization.
|
||||
|
||||
Required-token endpoints return the existing generic `401 Unauthorized` response when authorization fails. The badge leaderboard remains public: a denied bearer token receives the anonymous view, without personal viewer data. When an Authorization header is present, this endpoint does not use a session cookie to bypass a denied token. Session-only requests continue to personalize the leaderboard normally.
|
||||
|
||||
## Compatibility and maintenance
|
||||
|
||||
Existing valid wildcard tokens remain compatible. The existing session-authenticated `POST /api/tokens` endpoint continues issuing `["*"]`; this change does not add token-creation options or alter stored tokens. Legacy null, malformed, empty, differently cased model names, and unrelated model tokens are intentionally denied. Review and replace affected tokens with explicit intended scopes, or reissue through the existing token endpoint when full access is appropriate.
|
||||
|
||||
Every new bearer-authenticated endpoint must pass its required abilities to `bearerUserId`. Multiple required abilities use AND semantics. Omitting the requirements, or passing an empty list, requires a wildcard token rather than granting arbitrary scoped tokens access.
|
||||
|
||||
No plaintext token or stored hash is added to error responses or logs by these checks. The existing issuance endpoint returns plaintext once by design.
|
||||
@@ -0,0 +1,31 @@
|
||||
# Security report verification — 2026-09-13
|
||||
|
||||
The supplied review describes commit `baeb54ae` plus a separate port for another hotel. Its “Fixed” labels were not evidence that the changes existed in EpicNext-Cms. This verification inspected canonical `main` at `52f6d149` and the corrective changes prepared here. No exploit or authenticated mutation was performed against production.
|
||||
|
||||
| Supplied finding | Verified state in baseline | Correction / remaining boundary |
|
||||
| --- | --- | --- |
|
||||
| 1. Logo authorization/upload | Confirmed missing action permission and per-file validation | Require settings edit before input or storage access; bounded decoded raster uploads |
|
||||
| 2. Favicon authorization/delete | Confirmed missing action permission; SVG accepted | Same permission boundary for create/delete, bounded raster/ICO validation |
|
||||
| 3. Active uploaded SVG | Confirmed SVG served inline without route CSP | Route CSP sandbox and nosniff on success/errors; existing SVG served as attachment |
|
||||
| 4. Client IP spoofing | Confirmed direct trust in caller-controlled `x-real-client-ip` | Shared validated resolver ignores that header. Forwarded headers still require trusted ingress that overwrites them and prevents direct public origin access |
|
||||
| 5. Email token action exports | Confirmed token helpers in a `use server` module | Move token creation/validation and delivery to a server-only module. Registration and verification call it internally |
|
||||
| 6. Locale cookie | Confirmed missing allowlist | Supported locales only, validate before reading/writing cookies |
|
||||
| 7. Email header injection | Confirmed unsanitized values in sendmail headers | Reject control characters before any mail transport or file fallback; includes configured sender |
|
||||
| 8. Gateway CORS | Supplied gateway path is outside this repository | Read-only GET to our `/api/health` with an unrelated Origin returned a fixed `https://epicnabbo.nl` allow-origin and no allow-credentials. This does not reproduce the report on that route, nor certify every host/route |
|
||||
| 9. Token abilities | Confirmed abilities and owner type not checked by bearer authentication | Enforce User owner type and explicit endpoint abilities; existing wildcard user tokens remain supported |
|
||||
| 10. Broad script CDN | Confirmed unrestricted jsDelivr script source, without a source-code consumer | Remove the broad script source; retain required captcha/analytics sources and nonce |
|
||||
|
||||
The additional `withNitroStaff` code and its tests mentioned in the supplied port do not exist in this checkout; they were not assumed to have been reviewed or imported.
|
||||
|
||||
## Evidence and limits
|
||||
|
||||
- Regression tests exercise authorization before I/O, actual file decoding, SVG/error response headers, token-boundary exports, token abilities, forged derived-IP headers across consumers, locale values, and mail header control characters.
|
||||
- An updated `pnpm audit --json` reported zero known advisories. This is a dependency database result, not proof that application code has no vulnerabilities.
|
||||
- Next.js treats exported Server Actions as public endpoints; unused actions can also be removed by the compiler. The email refactor removes the action boundary entirely instead of relying on whether a specific build exports an unused helper. See [Next.js data security](https://nextjs.org/docs/app/guides/data-security).
|
||||
- The framework also has its own Server Action body limit. The logo defect was absence of application-level file validation, not evidence of literally unlimited bytes through every deployment layer.
|
||||
- No live database, user accounts, uploaded files, or gateway configuration were modified during verification. These changes do not constitute a penetration test or an audit of the emulator, host, or all CMS endpoints.
|
||||
- No nginx/Traefik ingress configuration is versioned here. The deployment guide records the forwarding-header trust requirement. That external boundary remains unverified.
|
||||
|
||||
## Follow-up identified during verification
|
||||
|
||||
The article-comment REST endpoint needs a separate review of publication visibility and moderation parity with the website form. This was discovered while enumerating bearer consumers; it is not silently treated as covered by the supplied review.
|
||||
Reference in new issue
Block a user