Skip to content

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.

Default strictness

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, faq and description.

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 you are writing a new module

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/.htaccess disables 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.enabled is on, audit.retention_days is 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

  1. Use #[IsGranted('module.action')] on the controller or capability: in the menu; do not check role names.
  2. Print user input escaped in Twig; |raw only for sanitized content.
  3. Query parameters must always be bound (parameterized).
  4. Do not start a session on anonymous requests.
  5. Use a nonce on inline scripts; do not call external CDNs.
  6. If a writing field gets caught by the WAF, have the exemption added as a narrow, separate rule.
Last updated: 13.10.2026