fix: harden CMS security controls
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
# NeoCMS Architecture
|
||||
|
||||
## Design Goals
|
||||
|
||||
NeoCMS adds controlled visual editing to static HTML websites while retaining three properties:
|
||||
|
||||
* Public pages remain ordinary HTML files served directly by the web server.
|
||||
* Supporting state uses files rather than a database service.
|
||||
* Existing page structure and styling remain under the site's control.
|
||||
|
||||
## Request Flow
|
||||
|
||||
`cms/index.php` renders the authenticated administration shell. It passes the CSRF token and configured editable class to
|
||||
`cms/js/cms.js` through metadata tags. The JavaScript client loads public pages into a same-origin iframe, binds editing
|
||||
behaviour to matching regions, and submits complete serialised documents to `cms/controller/index.php`.
|
||||
|
||||
The preview iframe uses `sandbox="allow-same-origin"` without `allow-scripts`. Parent-side JavaScript may still inspect and
|
||||
edit the DOM, while scripts belonging to public pages cannot reach the administration window, session, or CSRF token.
|
||||
|
||||
`CMSController` is the single API dispatcher. It authenticates every request and delegates mutating actions through
|
||||
`requirePost()`, which enforces POST, CSRF validation, and the requested role capability.
|
||||
|
||||
Image uploads use the separate `cms/image_upload.php` endpoint because TinyMCE sends multipart file data. It applies the
|
||||
same authentication, role, and CSRF requirements before validating image bytes.
|
||||
|
||||
## Storage Model
|
||||
|
||||
Published content stays in the web document root. NeoCMS stores supporting data beneath `cms/data/`:
|
||||
|
||||
| Path or document | Contents |
|
||||
| --- | --- |
|
||||
| `drafts/` | Complete unpublished HTML documents, named by a hash of their URI. |
|
||||
| `revisions/` | Immutable snapshots of earlier published documents. |
|
||||
| `scheduled/` | Complete HTML staged for future publication. |
|
||||
| `drafts.json` | Human-readable draft ownership and timestamp metadata. |
|
||||
| `revisions.json` | Revision URI, author, reason, and timestamp index. |
|
||||
| `schedules.json` | Publication times and staged-document identifiers. |
|
||||
| `shared.json` | Named global HTML fragments. |
|
||||
| `menus.json` | Named flat menu structures with optional parent labels. |
|
||||
| `media.json` | Reusable image alternative text. |
|
||||
| `activity.json` | The latest 250 dashboard activity entries. |
|
||||
| `login-attempts.json` | Hashed address and identity buckets used for login throttling. |
|
||||
|
||||
`FileStore` replaces JSON documents atomically by writing a temporary neighbour and renaming it. Public page publication
|
||||
uses the same approach. A revision is created before replacing, deleting, or globally updating a public page. Private
|
||||
directories use mode `0700` and private files use `0600` where supported. Configurable count and byte quotas prevent drafts,
|
||||
schedules, revisions, and uploads from growing without bound.
|
||||
|
||||
## Editing Model
|
||||
|
||||
The configured `editableClass` identifies editable regions. The client edits a region's inner HTML in TinyMCE, then writes
|
||||
it back to the iframe only. Drafting, scheduling, and publishing serialise a clone of the complete iframe document after
|
||||
removing temporary CMS controls.
|
||||
|
||||
`neo-dupe` marks a repeatable block. `data-neo-shared="key"` marks a globally managed fragment. `data-neo-menu="name"`
|
||||
marks generated navigation that should be replaced whenever its named menu changes.
|
||||
|
||||
The bundled templates use the default `editable` class. Sites that override `editableClass` must update their templates
|
||||
to match; otherwise the resulting page is valid HTML but, quite reasonably, is not treated as editable by NeoCMS.
|
||||
|
||||
## Roles
|
||||
|
||||
Capabilities are intentionally small and cumulative:
|
||||
|
||||
| Role | Capabilities |
|
||||
| --- | --- |
|
||||
| `editor` | Save drafts and upload images. |
|
||||
| `publisher` | Editor capabilities plus publish, schedule, and restore revisions. |
|
||||
| `administrator` | Publisher capabilities plus page, media, shared-content, and menu management. |
|
||||
|
||||
The interface hides controls the user cannot invoke, but server-side checks remain authoritative. Hidden buttons are a
|
||||
convenience, not a security boundary wearing a clever disguise.
|
||||
|
||||
Active sessions are bound to the current configured password hash. Removing an account or changing its hash invalidates
|
||||
the session on the next request, while roles are re-read from configuration so demotion is immediate.
|
||||
|
||||
## Scheduled Publishing
|
||||
|
||||
Every API request opportunistically processes due jobs. Reliable unattended operation comes from invoking
|
||||
`cms/publish_scheduled.php` through cron. The worker is CLI-only and uses the same publication and revision path as the UI.
|
||||
|
||||
## Security Boundaries
|
||||
|
||||
NeoCMS relies on PHP sessions with strict mode, HTTP-only cookies, SameSite=Lax, and Secure cookies under HTTPS. Mutating
|
||||
requests require a constant-time-checked CSRF token. Page paths are canonicalised and constrained to HTML files beneath the
|
||||
document root, excluding the CMS directory and symbolic links. Uploads are constrained by byte size, detected MIME type,
|
||||
image decoding, dimensions, pixel count, file count, and total storage. Login attempts are throttled by hashed address and
|
||||
address/username buckets.
|
||||
|
||||
HTML and JSON responses receive Content Security Policy, anti-framing, no-sniffing, referrer, permissions, cross-origin,
|
||||
cache-control, and conditional HSTS headers. CDN scripts and styles are fixed to explicit versions and protected by SRI.
|
||||
|
||||
The supplied Apache rules protect configuration, data, logs, CLI entry points, and uploaded executable extensions. Other
|
||||
servers must provide equivalent denial rules, and production use should always use HTTPS.
|
||||
|
||||
Audit entries are locked while appending, normalised to prevent forged lines, rotated at a configurable size, and pruned
|
||||
after a configurable retention period. They remain operational records rather than a tamper-proof ledger.
|
||||
Reference in New Issue
Block a user