Security
SynaptikCMS ships with a layered security setup out of the box. This page covers what is protected, how, and what you should configure before going to production.
Admin folder
During installation, install.php lets you choose a custom name for the admin folder. Use something non-obvious — avoid admin, cms, backend, or any other common name. This is the single most effective measure against automated scanners.
Protected paths
Sensitive paths are blocked at two levels: directory-level .htaccess files, and file-level rules in the root .htaccess.
Blocked files (root .htaccess)
| Pattern | Why |
|---|---|
settings.json | Site configuration — contains active theme, language, all site settings |
admin-credentials.php | Hashed admin password |
.htaccess files | Prevent rule disclosure |
Blocked directories (own .htaccess)
Each of these directories has its own .htaccess with Require all denied (Apache 2.4) and Deny from all (Apache 2.2 fallback):
| Path | Contains |
|---|---|
/data/ | All content JSON files (articles, pages, projects, categories, tags) |
/bckps/ | Database backups and CSS editor backups |
/private/ | Contact form CSRF secret and rate-limiting data |
Security headers
Set via mod_headers in the root .htaccess:
| Header | Value | Purpose |
|---|---|---|
X-Frame-Options | SAMEORIGIN | Prevents clickjacking — the site can only be framed by itself |
X-Content-Type-Options | nosniff | Browser must honour the declared Content-Type |
Referrer-Policy | strict-origin-when-cross-origin | Limits referer leakage to external domains |
Permissions-Policy | camera, mic, geo, payment all disabled | Disables unused browser APIs |
Content-Security-Policy | See below | Controls which origins can load scripts, styles, fonts, frames |
Default CSP
default-src 'self';
script-src 'self' 'unsafe-inline' https://js.hcaptcha.com https://cdn.jsdelivr.net;
frame-src https://newassets.hcaptcha.com;
style-src 'self' 'unsafe-inline' https://fonts.googleapis.com https://cdn.jsdelivr.net;
font-src 'self' https://fonts.gstatic.com https://cdn.jsdelivr.net;
img-src 'self' data: blob:;
connect-src 'self' https://hcaptcha.com https://api.hcaptcha.com;
frame-ancestors 'self'
If you are not using hCaptcha, remove the js.hcaptcha.com, newassets.hcaptcha.com, hcaptcha.com, and api.hcaptcha.com entries. If you are not using Google Fonts, remove fonts.googleapis.com and fonts.gstatic.com.
'unsafe-inline' is required for scripts because the CMS injects inline JS for the i18n bridge (window.CMS_LANG) and gallery initialisation. Removing it will break the front end.
Contact form
The contact form (contact-process.php) has four independent protection layers:
| Layer | Mechanism |
|---|---|
| CSRF | HMAC-SHA256 token derived from a 32-byte secret stored in /private/contact.secret |
| Honeypot | Hidden field that bots fill in — submission is rejected silently |
| Rate limiting | Max 5 submissions per IP per hour. IPs are stored hashed (SHA-256) in /private/contact_rate.json |
| hCaptcha | Optional server-side verification. Configure via Settings → Contact |
Theme preview token
The live theme preview uses a signed HMAC-SHA256 token with a 2-hour TTL. The secret is derived from the bcrypt hash of the stored admin password — no additional secret file is needed. The token only works when $_SESSION['admin'] === true.
See Live Theme Preview for full details.
Browser caching strategy
Assets are cached aggressively. The .htaccess sets:
| Asset type | Cache-Control | Max-age |
|---|---|---|
| CSS, JS | public, immutable | 1 year |
| Images, fonts | public | 1 year |
| HTML, PHP | no-store | Never cached |
CSS and JS files served by render_header_scripts() include a ?v=<mtime> cache-busting parameter, so browsers automatically pick up changes without needing to clear cache.
Production checklist
[ ] Admin folder renamed to something non-obvious
[ ] /data/, /bckps/, /private/ return 403 (test: curl -I yourdomain.com/data/_index.json)
[ ] settings.json not accessible (curl -I yourdomain.com/settings.json should return 403)
[ ] HTTPS enabled — the CSP and session cookie are ineffective over HTTP
[ ] Directory listing disabled on the server (Options -Indexes or server config)
[ ] PHP error display off in production (display_errors = Off in php.ini)
