Skip to the content
Documentation

Security

No ratings yet

This page is the starting point for the security of an Exponential site: how to report a vulnerability, which versions receive security fixes, and what to check and set before a site goes live and while it runs. It describes Exponential 6.0, the maintained line: the newest release is 6.0.14 (7 June 2026), and 6.0.15 is in development on the main branch. Where a feature is new in 6.0.15, the text says so.

A default Exponential 6.0 installation is built to be safe without extra work: the rewrite rules hand out only a fixed list of files, security headers go out on every page, the session cookie is HttpOnly and SameSite, debug output is off, forms carry a token against cross-site request forgery, and an audit trail records who did what. What remains for you is a set of decisions about your own site, listed below. The complete, maintained guides are in the help center of every installation; this page links to them.

Reporting a vulnerability

Please report security issues privately, never in the public issue tracker, the forums or the discussions:

You can expect a first answer within one business day. Please include:

  • the version (composer show se7enxweb/exponential, or the tag or commit you run) and the PHP version;
  • the engine that serves the site (Exponential Velocity, FrankenPHP, Apache or nginx with PHP-FPM) and the database;
  • what an attacker can do, and the steps to reproduce it.

Leave out passwords, secrets and personal data. You will be told whether the report is accepted or declined, and kept informed while a fix is prepared. An accepted issue is fixed on the main branch and released with the next 6.0.x version. The policy itself is the file SECURITY.md at the root of every installation and of the repository; When something goes wrong explains where other kinds of problems are reported.

Supported versions and security releases

Version Security fixes
Exponential 6.0.x, the current line (branch main, Composer version dev-main, and the next tag cut from it) Yes
Exponential 6.0.0 to 6.0.14 (released tags) Fixed by upgrading to the newest 6.0.x
eZ Publish 5.x, 4.x, 3.x and older No

A published tag is never changed: a fix for a problem in 6.0.14 or an earlier release reaches you as the next release, not as a corrected copy of the old one. Releases are listed on the GitHub releases page and on Packagist; published advisories appear on the repository's security advisories page, and the changelog of each release names the security fixes it contains (for example the seven findings fixed in 6.0.13, described in the 6.0.13 hardening specification, and the hardening of the 6.0.15 line).

Keeping up to date

  • Exponential and its extensions are Composer packages: composer outdated lists newer versions, and composer audit lists known advisories of the installed packages.
  • Read the changelog and the upgrade notes before updating, and update a copy of the site first. The procedures are in Upgrading and Upgrading to 6.0, and in the help center's Updating Exponential 6.0.
  • After an update, Setup > Upgrade check compares the files with the release (Check file consistency) and the database with its schema (Check database consistency); a modified kernel file you did not change deserves a look (The upgrade check).
  • PHP, the database server, the operating system, Velocity's engine and the web server need their own updates as well.
  • Review your own extensions for the patterns fixed in the kernel: SQL built from request data without escapeString() or integer casts, shell commands without escapeshellarg(), output without escaping (the 6.0.13 hardening notes).

Hardening checklist

The help center has two guides for this: Security hardening for production (chapter 13 of the installation book, setting by setting, with a go-live checklist) and Security hardening (the attacker's view, threat by threat). Settings are changed in overrides, never in the shipped files: settings/override/<file>.ini.append.php for the whole installation, settings/siteaccess/<siteaccess>/<file>.ini.append.php for one siteaccess. Clear the INI cache after a change (php bin/php/ezcache.php --clear-tag=ini), and under Velocity run exp:velocity deploy or restart the server.

What the web server must never hand out

The document root is the installation root, so settings/, var/, vendor/, the kernel sources and an SQLite database lie inside it. Safety comes from the rewrite rules: only a fixed list of asset paths is served as files; everything else is passed to index.php. Dot files and directories (.git, .env) answer 404, every .php, .phtml and .phar file below the root answers 404, and uploaded files other than images are delivered only through content/download, which checks the reader's permissions.

  • Apache: use the shipped rules, .htaccess_root. ./console exp:setup:htaccess shows the difference between your .htaccess and the template and exits 1 while they differ; --apply copies it. Compare and merge after every upgrade. Better still, put the rules into the virtual host with AllowOverride None (example HTTPS virtual host, Setting up .htaccess and config.php from the console).
  • nginx: use the derived server block (nginx example), which keeps the rules for dot files and scripts ahead of every static location.
  • Velocity: the same list is built in; symbolic links that lead out of the document root are refused (FollowSymlinks=disabled, the default).
  • Never serve the installation with a generic "serve the file if it exists, else index.php" configuration: it hands out settings/*.ini and the database.

Check from outside; every line must show 404, or 200 with an Exponential page, never the file's content:

for p in /.git/config /.env /settings/site.ini /composer.json /kernel/classes/ezini.php /var/log/error.log; do
  printf '%-32s %s\n' "$p" "$(curl -s -o /dev/null -w '%{http_code}' https://example.com$p)"
done

Secrets

  • settings/override/ holds the database password, the audit signing and pseudonym keys and the site secret of the e-mail preferences. Never commit it to version control, keep it at mode 0640 or stricter, back it up as a secret together with the database, and copy it with every move of the installation. The same goes for config.php when it holds a password.
  • Since 6.0.15 the passwords, tokens and API keys that tools need (data table connections, import sources, remote servers) are kept in the secret store: encrypted in the database, with the key outside it (the environment variable EXP_SECRET_KEY or a key file). Manage them under Setup > Secrets (policy secrets/manage); a value is written there and never shown again (The secret store).
  • After installing, change the generated administrator password and delete var/log/initial-admin-password and kickstart.ini (Change the administrator password).

HTTPS and response headers

  • Serve every host name over HTTPS and redirect plain HTTP. Set [Session] CookieSecure=true on an HTTPS-only site, and turn on HSTS once everything works, starting with a short max-age.
  • Every page carries X-Content-Type-Options, X-Frame-Options, a Content-Security-Policy with frame-ancestors 'self', Referrer-Policy and Permissions-Policy (site.ini [HTTPHeaderSettings] SecurityHeaders[]); Strict-Transport-Security is empty until you set it, and is sent only over HTTPS.
  • A full Content Security Policy is sent from site.ini [ContentSecurityPolicy]: report-only on the administration and off on the sites as shipped (Mode=auto). Read the reports (CSP reports in the administration), then enforce it (Content Security Policy).
  • Behind a proxy, load balancer or CDN, list exactly your proxies in [HTTPHeaderSettings] TrustedProxies[]: since 6.0.15 forwarded headers are believed only from those addresses (Forwarded headers are trusted only from configured proxies).
curl -sI https://example.com/ | grep -i -E 'strict-transport|x-content-type|x-frame|referrer|permissions|content-security'
curl -sI https://example.com/user/login | grep -i set-cookie

Debug output, the setup wizard and unused parts

  • Keep [DebugSettings] DebugOutput=disabled (the default) and PHP's display_errors=Off. To look at a live problem, limit debug output to your own address with DebugByIP and DebugIPList[], and remove it afterwards.
  • The installer writes [SiteAccessSettings] CheckValidity=false into settings/override/site.ini.append.php; while it is true, every request goes to the setup wizard. Never set it back on a live site.
  • Switch off the extensions and siteaccesses you do not use. Modules a siteaccess does not need can be switched off for it with [SiteAccessRules] Rules[] in site.ini; a switched-off module answers 404 as shipped (site.ini [SiteAccessRules]).
  • Keep the form token extension ezformtoken active: it adds a per-session token to every form and checks it on every POST, against cross-site request forgery (Form expired page).

The administration

  • The administration siteaccess requires a sign-in (RequireUserLogin=true). Give it a host name of its own, and restrict it by network, VPN or HTTP authentication in front of it.
  • Request rules (settings/requestrules.ini) decide before a view runs: keep anonymous visitors off system URLs, require a sign-in for a section, refuse a network (Request rules).
  • Make sure demo mode is off: the environment variable EXP_DEMO_MODE must not be set for any process of a production site (Demo mode).

File permissions and ownership

  • One user writes: PHP-FPM, the Velocity workers, cronjobs and console scripts run as the site's user or share one group. Never let them run as root.
  • Only var/ and settings/ need to be writable; the code (kernel/, lib/, vendor/, index.php) can be read-only for the site's user, so a compromised PHP process cannot change it.
  • No world-writable files. The shipped [FileSettings] values are 0777 and 0666; set the hardened values in an override:
# settings/override/site.ini.append.php
[FileSettings]
StorageDirPermissions=0770
StorageFilePermissions=0660
TemporaryPermissions=0770
LogFilePermissions=0660

The database user

Give Exponential a database user of its own, with privileges on its own database only: no global privilege, no SUPER, no right to list databases. For MySQL and MariaDB the installation book lists the exact grants (SELECT, INSERT, UPDATE, DELETE, CREATE, DROP, INDEX, ALTER, LOCK TABLES, CREATE TEMPORARY TABLES); see Databases. An SQLite database is protected by the file system: keep it below var/, writable by the site's user only.

Velocity

When Exponential Velocity serves the site: bind it to 127.0.0.1 unless it faces visitors directly, run its workers as the site's user (never root), keep the dashboard, /Q/phpinfo and the control panel local or protected, and list only your own proxies in its trusted proxy list. Section 13.12 of Security hardening for production has the settings; the engine's own notes are in its repository.

Accounts and sign-in

  • Passwords. At least 10 characters as shipped ([UserSettings] MinPasswordLength), hashed with bcrypt, and since 6.0.15 well-known and common passwords are refused (WeakPasswordCheck). Character rules in [PasswordSettings] are off by default; current guidance favors length (for example 15) over composition rules. After a password change the user's other sessions are signed out (EndOtherSessions=enabled) and the account receives a notification mail (Changing your password).
  • Failed sign-ins. MaxNumberOfFailedLogin is 0 (no lockout) as shipped; weigh a lockout against the risk that anyone can lock a known account. The audit's alert rules report brute-force attempts either way.
  • Two-step sign-in. After the password, a code from an authenticator app or one sent by e-mail, with recovery codes. It is provided by the extension sevenx_authentication_2fa and asked by the web sign-in form; the REST interface, SOAP, WebDAV and the expservices login do not ask for it unless a site opts in (Two-step sign-in, The second factor on other sign-in paths). Require it for administrators and editors.
  • Passkeys (6.0.15): users sign in with a passkey of their device and manage them under user/passkeys (Passkeys).
  • Verify it's you (6.0.15): before a sensitive action, such as changing a password, assigning a role, clearing caches, exporting data or emptying the trash, a signed-in user confirms who they are again; a confirmation counts for ten minutes. The groups of actions are set under Setup > Verify it's you (/setup/reauth). Only the web pages ask (Verify it's you).
  • Sessions. The session cookie is HttpOnly and SameSite=Lax, and Secure on HTTPS requests (CookieSecure=auto). "Remember me" is checked on the sign-in form by default and keeps a user signed in for 360 days ([Session] RememberMeTimeout, with SessionTimeout as long). Since 6.0.15 every user sees and ends their signed-in sessions on Your devices (user/sessions) and their sign-ins and account changes on Security activity (user/security); a sign-in from a new device sends a mail (Your devices, Your devices and sessions).
  • Roles and policies. Give people the roles they need, not Administrator, and one personal account per person. Policies that matter for security include setup/setup, setup/managecache, audit/read, audit/manage, secrets/manage and user/login per siteaccess (Roles and policies).

Auditing and monitoring

  • The audit trail is on in every installation. It records sign-ins and failed sign-ins, role and setting changes, content moved, hidden or removed, commands and cronjobs, as hash-chained JSON lines, so a changed or removed line shows. Read it under Audit in the administration or on the console; run the frequent cronjob group so its alert rules work, check who receives the alert mail, forward it off the machine with a sink, and back up settings/override/audit.ini.append.php (Audit, The audit trail).
  • Logs. Since 6.0.15 the request classifier writes probes, path traversal, injection attempts and attack tools to var/log/security.log, and not-found answers to var/log/404.log; Content Security Policy reports go to var/log/csp.log. Read them under Setup > Logs (Request logs, Log viewer). The classifier only classifies; blocking is the job of the web server or a firewall in front.
php bin/php/console exp:audit status
php bin/php/console exp:audit verify
php bin/php/console exp:audit tail --channel=access --lines=20
php bin/php/console exp:audit alerts recipients

Content and uploads

  • Nothing below the document root is executed, and stored files other than images are delivered only through content/download, which checks the reader's permissions.
  • Images are accepted by what the file contains, not by its name (checked against image.ini [ImageUploadSettings] AllowedMIMETypes[]), and uploaded SVG files are sanitized before they are stored (SVG images).
  • The file, media and image datatypes refuse every extension a web server may run as code (file.ini [FileSettings] FileExtensionBlackList[]).
  • Templates escape output with the wash operator; give upload rights only to the roles that need them, and keep TemporaryDir private to the site.

Editors find the upload pages explained in Images, media and uploads.

Earlier pages and advisories

This page has no child pages: the security advisories of the eZ Publish years were published by eZ Systems on its own websites and were not part of this documentation. The technical manual pages below are kept as they were written for the classic releases:

History of this page

The previous text of this page came from the eZ Publish years: it described how to report a security issue to eZ Systems, through its customer support portal, its JIRA issue tracker or its e-mail address. eZ Publish, the predecessor of Exponential, was discontinued by its creator, and those channels are no longer the place to report a problem in Exponential. Today's reporting channel is the one at the top of this page. The earlier text remains in the version history of this page.

Contributors

Comments

Add a comment

Comments are read by a moderator before they appear.