Security
CPalius does not leave security to plugins; it ships as part of the core. This page gives each layer, the related setting key and its default value. All settings are managed from the AACP → Security screen.
A new installation deliberately starts in observation mode: WAF detect, CSP report. Both record events but do not block. This way the legitimate traffic of a newly installed site is not cut off by false positives. When going live, reviewing the logs and moving the modes to block / enforce is the operator's decision. cp:security:audit reports both states as findings.
Layers
request → IP ban / allowlist → WAF (signature scan) → rate limit → authentication
→ permission (capability) → controller → output escaping → security headers (CSP, HSTS…)
WAF
RequestGuardSubscriber scans every request with ThreatAnalyzer. The scan looks for four signature families across the URL, query parameters, headers and body:
- SQL injection signatures
- XSS signatures
- Path traversal / LFI signatures (such as
../,/etc/passwd) - Known attack tools (scanner user agents)
Every match produces a score.
| Setting | Default | Meaning |
|---|---|---|
security.waf_mode |
detect |
off disabled, detect only records, block rejects the request at the score threshold |
security.waf_block_score |
80 | This score and above is rejected (in block mode) |
security.waf_autoban_score |
95 | This score and above bans the IP |
security.waf_autoban_minutes |
1440 | Ban duration (minutes) |
security.ip_allowlist |
empty | Comes before every ban: the IPs here are not blocked |
security.trusted_hosts |
empty | Allowed Host headers |
The WAF covers the /aacp path too; if the admin panel were left reachable, a banned address could keep hammering the login form. Only the database-independent recovery console is exempt, because it is the way back for an operator who has banned themselves.
Writing fields and false positives
Human-written areas such as blog, forum and docs may legitimately contain phrases like ../, .env and wp-login.php (for example a technical article). There is a narrow exemption for this:
- only in POST requests,
- only on known write paths (Studio/AACP paths and forum write paths),
- the body is not scanned only in writing fields such as
body,content,excerpt,faqanddescription.
Title, slug, URL and all other fields and all other paths are scanned as before. Security here is provided by output escaping, parameterized SQL and HTML cleaning at save time; the WAF is not turned off.
If your writing field gets caught by the WAF, do not loosen the WAF; have your path and field name added to the exemption list in ThreatAnalyzer (as a separate, narrow rule) and verify escaping on output.
Login defense
Login attempts are counted with FloodService (a sliding-window limiter). Identifiers are hashed; the email address does not appear in cache keys. Blocking happens before the password hash is computed, so a locked account cannot let an attacker spend our CPU. Since the defense is bound to authenticator events, all of today's and tomorrow's authenticators are covered.
| Setting | Default |
|---|---|
security.login_ip_limit |
20 attempts / window (per IP) |
security.login_user_limit |
5 attempts / window (per account) |
security.login_window_minutes |
15 |
security.lockout_minutes |
15 |
security.autoban_after_lockouts |
IP ban after 3 lockouts |
security.register_limit_hourly |
5 registrations / hour |
security.reset_limit_hourly |
5 password resets / hour |
CAPTCHA
Providers: reCAPTCHA (v2 / v3, score threshold 0.5), Cloudflare Turnstile, hCaptcha. The default provider is none; it comes on for registration and off for login (security.captcha_on_register / security.captcha_on_login).
Password policy
The decision is made in a single place (PasswordPolicy); registration, in-account change and administrator reset use the same rule.
| Setting | Default |
|---|---|
security.password_min_length |
10 |
security.password_required_classes |
3 (from the classes uppercase, lowercase, digit, symbol) |
security.password_breach_check |
on |
security.password_breach_fail_closed |
off |
security.password_history_depth |
the last 5 passwords cannot be reused |
security.password_max_age_days |
0 (no expiry) |
security.password_denylist |
empty (your own forbidden words) |
The breach check looks at the Pwned Passwords dataset with k-anonymity: only the first five characters of the password's SHA-1 digest leave the server; the remote service does not see enough information to recognize the password. If the service is unreachable, the default behaviour is to accept the password; if fail_closed is turned on, an unreachable service rejects the password.
Two-factor authentication
| Setting | Default |
|---|---|
security.twofactor_enabled |
on |
security.twofactor_methods |
both (time-based code and email code) |
security.twofactor_enforce_privileged |
off (mandatory for privileged accounts when turned on) |
security.twofactor_drift |
1 (clock drift tolerance, in steps) |
security.twofactor_email_ttl_minutes |
10 |
security.twofactor_email_resend_seconds |
60 |
Session
| Setting | Default |
|---|---|
security.session_idle_minutes |
60 |
security.session_admin_idle_minutes |
20 (panel session is shorter) |
security.session_absolute_hours |
0 (no absolute limit) |
security.session_bind_user_agent |
on |
security.session_bind_ip |
off |
security.session_purge_days |
30 |
No session is opened for anonymous visitors; this is a deliberate decision for both privacy and caching.
AACP gate
Optional (security.aacp_gate_enabled, off by default); it is a shared-secret question asked once per session. It is not a second factor; a question to which everyone gives the same answer cannot be a second factor. What it provides: a stolen session cookie or a browser left open gives the attacker the site but not the admin console. An enabled but unconfigured gate is ineffective, so an operator who ticks the box and forgets the answer does not lock themselves out. The recovery console is exempt from this gate too.
CSP and security headers
| Setting | Default |
|---|---|
security.headers_enabled |
on |
security.csp_mode |
report (blocks when enforce is chosen) |
security.csp_script_src, csp_style_src, csp_img_src, csp_connect_src |
empty (extra allowed sources) |
security.csp_frame_src |
YouTube (nocookie) and Vimeo players |
security.csp_frame_ancestors |
'self' |
security.hsts_max_age |
0 (off) |
security.referrer_policy |
strict-origin-when-cross-origin |
security.permissions_policy |
camera etc. off |
Inline scripts and styles carry a nonce; csp_nonce_attr() is used in templates. Do not load scripts/styles from external domains; take libraries locally into the module's Resources/assets/vendor folder. In report mode, violations are collected by CspReportController, so you can see which sources would be blocked before enforce.
Permission system
Every operation is protected by a capability, and an undefined capability is denied. Capability names are checked, not role names. Details: Permission system.
Uploads and content
- File type is validated by MIME content, not by extension; there is an allowed-type list.
- Uploaded files are written under a hash name to a non-executable folder;
public/uploads/.htaccessdisables PHP execution. - Rich text passes through the HTML sanitizer at save time; raw output in Twig is done only for content that has been explicitly sanitized.
Malware scanner
Off by default. It is turned on from the AACP → Tools → Malware scanner screen. Rules are classified as high / medium / low severity; findings can be quarantined, and there is a hash-bound allowlist. Uploads and module/theme packages are scanned; weak (low) hints are skipped in core code.
Audit log and telemetry
- Audit log: important admin actions are stored with who/when/what.
audit.enabledis on,audit.retention_daysis 365. - Security telemetry: WAF and login events are recorded with severity levels (
telemetry.security_enabled, off by default). Retention: info 3, warning 7, critical 30, threat 90 days.
Webhooks and outgoing requests
Outgoing webhooks are signed with HMAC and go out behind SSRF protection (SsrfGuard): requests to internal network addresses are rejected. Details: API and webhooks.
Security audit: cp:security:audit
php cp-core/bin/console cp:security:audit
php cp-core/bin/console cp:security:audit --json # machine-readable
php cp-core/bin/console cp:security:audit --fail-on=high # for CI
The auditor looks not only at stored settings but at how requests are actually handled, and flags setting combinations that cancel each other out. The default installation deliberately does not score 100/100: the audit is not a certificate but a checklist for the operator.
Reporting a vulnerability
Do not post security vulnerabilities anywhere public. The SECURITY.md file in the repository gives the reporting channels and target times: a report is acknowledged within 72 hours, the first assessment takes 7 days, and a fix or mitigation is targeted within 30 days for critical vulnerabilities. There is no paid bug bounty program at the moment; reporters are credited in the release notes unless they prefer otherwise.
Rules for module authors
- Use
#[IsGranted('module.action')]on the controller orcapability:in the menu; do not check role names. - Print user input escaped in Twig;
|rawonly for sanitized content. - Query parameters must always be bound (parameterized).
- Do not start a session on anonymous requests.
- Use a nonce on inline scripts; do not call external CDNs.
- If a writing field gets caught by the WAF, have the exemption added as a narrow, separate rule.