Security hardening
This page is for administrators and developers who run an Exponential site that can be reached from the internet. It looks at the site the way an attacker does: which doors exist, what is tried against them every day, and which of them Exponential 6 already keeps shut. For each threat it names the defense that is built in and the settings or commands that close what is left.
Warning
.htaccess slows down every request. When .htaccess files are enabled (AllowOverride anything but None), let alone
used, Apache looks for them and reads and evaluates them on every request, in every directory of the path, and
page loading times go up drastically (measured). The only real fix is to switch the feature off for
the virtual host: AllowOverride None in the HTTPS virtual host, and the rules in the virtual host's own
configuration as mod_rewrite rules. That is far faster in production speed tests and well worth the time it takes
to optimize a production configuration. Switch over right away: copy the rules of the
example HTTPS virtual host into your virtual host (with the static cache, also
staticcache_offload.conf), set AllowOverride None, run apachectl configtest, reload Apache and
test the site. The same rules for other servers: nginx, LiteSpeed,
Caddy, IIS, Velocity (built in); where virtual host rules go in a control
panel: Plesk, cPanel, DirectAdmin, ISPConfig.
On LiteSpeed or OpenLiteSpeed, the server's own page cache can also answer anonymous visitors and crawlers without PHP: Page caching with LiteSpeed and OpenLiteSpeed.
It complements, and does not repeat, chapter 13 of the installation guide, Security hardening for production, which is the setting-by-setting reference and the go-live checklist. Where both cover a topic, this page says why and links to the section of chapter 13 that says how.
Note
This page describes defenses only. To report a vulnerability, write to [email protected], not to the public issue tracker (Reporting a vulnerability).
Contents
- Principles: secure by default
- Who attacks a CMS, and how
- The threats, one by one
- A shield in front: request-shield
- Watching: logs, the request classifier and the audit trail
- Checklist
- Reporting a vulnerability
Principles: secure by default
The OpenBSD project's way of working is a good yardstick for any server software: a fresh installation is safe without extra work, every part has only the rights it needs, and every change is audited. Exponential 6 follows the same principles; your part is to keep them in force.
| Principle | What it means here | Built in | Your part |
|---|---|---|---|
| Secure by default | a new installation is safe before anyone tunes it | strict rewrite rules, security headers, HttpOnly/SameSite cookies, debug off, form tokens, bcrypt hashes, generated administrator passwords, the setup wizard closed on an installed site | do not loosen a default to "make something work"; find the cause |
| Least privilege | every account and process can do only its job | roles and policies, the Anonymous role without user/login to the administration, CLI scripts refuse to run as root without --allow-root-user, Velocity workers never root | editors get editor roles, one personal account per person, admin retired (Account hygiene) |
| Privilege separation | the parts that face the internet cannot change the parts that run code | PHP below the document root is never served, uploads are never executed, ManagedByDeployment keeps the administration from writing code and settings | code owned by another user than the web server, read-only to it; the administration on its own host |
| Minimal attack surface | what is not needed is not reachable | only a fixed list of asset paths is served as files, the admin modules need a sign-in, the shield and the request rules can refuse whole URL families | switch off extensions and siteaccesses you do not use; restrict the administration by network |
| Audit | every change can be traced, and tampering shows | the audit trail with hash chains, security.log, auth.log, the CSP report log, the log viewer | read them, alert on them, forward them off the machine |
| Fix forward, publicly | a published release is never changed; fixes come as the next release | releases are immutable tags; share/filelist.md5 lets you check every file | follow the releases, update, check file consistency |
Who attacks a CMS, and how
Almost all attacks on a site like yours are automated and not personal. The groups behind them reuse the same few techniques on millions of sites:
- Botnets and scanners probe every address for known files and products (
/wp-login.php,/.env,/.git/config,/backup.zip,phpinfo.php), injection strings in every parameter, and old vulnerabilities by version. - Credential attackers try leaked username and password pairs from other breaches (credential stuffing), default
passwords (
admin/publishfor this software), and slow password guessing spread over many addresses. - Access brokers turn one working sign-in or one uploaded script into persistent access and sell it on; ransomware and data theft follow. A forgotten administrator account or an old extension is enough.
- Scrapers and AI crawlers count through every URL,
content/view/full/<number>included, and fill caches. - Supply-chain attackers aim at dependencies, build pipelines and update channels rather than at your site.
Defending well means making all of these cheap to refuse and expensive to succeed, and noticing when one gets through.
The threats, one by one
Brute force and credential stuffing on the sign-in
The risk. /user/login and /admin/user/login are the most attacked URLs of any site. Leaked passwords from other
services work wherever people reuse them.

Built in. Passwords are stored as bcrypt hashes (HashType=php_default). The sign-in costs the same for an unknown
login as for a wrong password, and Forgot your password? answers the same for every address, so neither reveals
which accounts exist. The audit trail's alert rules brute_force (20 failures from one network in 5 minutes) and
brute_force_user (10 for one account in 15 minutes) report attacks. Two-step sign-in (sevenx_authentication_2fa)
makes a stolen password useless on its own. Every form that sets a password refuses well-known and common ones
(WeakPasswordCheck, the list attackers try first), and a Forgot your password? link works once and for one hour
(ForgotPasswordKeyLifetime).
Harden.
- Turn on two-step sign-in and require it at least for administrators
(Two-step sign-in,
[General] Enforce2FA=enabled). - A password policy:
MinPasswordLength=15,ForbidLogin,ForbidCurrentPassword(13.6). - Optionally lock accounts after failed attempts (
[UserSettings] MaxNumberOfFailedLogin=10) with your own network inTrustedIPList[]; weigh that a lockout also lets anyone lock a known account. - Restrict the administration to your network or a VPN, or put a browser check in front of its sign-in with
request-shield. Behind a proxy, configure
TrustedProxies[]first, or every visitor has the proxy's address (13.10).
Default and shared credentials
The risk. The login admin exists on every installation, and publish is the default password printed in every
old guide. Shared accounts hide who did what.
Built in. No installer of Exponential 6 leaves publish in place: the kickstarter and exp:install replace an
empty, short or well-known password with a random one, and the setup wizard refuses a short or well-known one
(The default administrator password).
Harden. Change the administrator password after installing, delete var/log/initial-admin-password, create a
personal administrator account per person, and retire admin as a sign-in
(Account hygiene). Check for an old seed password on migrated sites:
./console exp:resetuserpassword --allow-root-user -u admin -g -l 24 sets a new one.
Demo mode. A public demo that publishes its administrator password runs with the environment variable
EXP_DEMO_MODE=1: the installers accept the published password and nobody can change or reset an administrator
password. On a real site that would be a known password nobody can change. It cannot come from a setting or a
request, the administration shows a banner while it is on, and every process logs it; check that no process of a
production site has the variable (Demo mode).
An exposed setup wizard or installer files
The risk. A setup wizard reachable on a live site lets anyone reinstall it with their own administrator. Installer
files (kickstart.ini, a generated password file) contain passwords.
Built in. On an installed site setup/init only shows "This site is already installed" and writes nothing
(expSetupWizardGuard; installed means var/site_installed.json exists, the site is served normally, or the database
holds content). [SetupSettings] AllowWizardOnInstalledSite (default disabled, development only) is the one way
around it, and even then a confirmation with a one-time token is required. The installer writes
[SiteAccessSettings] CheckValidity=false. With [ConfigurationSettings] ManagedByDeployment=enabled the
administration cannot run the wizard, edit settings or templates, activate extensions or install packages at all
(Configuration managed by the deployment).
Harden. Keep CheckValidity=false and AllowWizardOnInstalledSite=disabled. Remove kickstart.ini and
var/log/initial-admin-password after installing. Turn on ManagedByDeployment on production administration
siteaccesses when settings and templates come from version control.
File upload abuse
The risk. An uploaded file that the server later runs (a PHP script disguised as an image), a file that runs script in the browser (SVG, HTML), or a huge or malformed file that exhausts the server.
Built in.
- Nothing below the document root is executed: the rewrite rules return 404 for every
.php,.phtmland.pharbelow the root, and serve only image files from the storage directories; other uploads are delivered only throughcontent/download, which checks the reader's permissions (13.2). - Images are accepted by what the file contains, not by its name:
expImageUploadValidatorreads the type with PHP's fileinfo and compares it withimage.ini [ImageUploadSettings] AllowedMIMETypes[]. - SVG files are never stored as uploaded, whichever datatype takes them (image, binary file, media, and their file
and package imports):
expSvgSanitizerkeeps only what is known to be safe, and the stored file is served with headers that stop it from running anything (SVG images). - Package uploads need the
packagepolicies, andManagedByDeploymentblocks them. - The binary file, media and image datatypes refuse every extension a web server may run as code
(
file.ini [FileSettings] FileExtensionBlackList[]:phptophp8,phtml,pht,phps,phar,phpt,pgif,shtml,cgi,pl,asp,aspx,jsp,htaccess) and HTML pages (html,htm,xhtml,xht), compared in lower case (Fixes from the Opencontent and Mugo Web forks).
Harden. Keep the rewrite rules current after every upgrade (.htaccess_root, or the derived nginx block;
./console exp:setup:htaccess shows the diff between your .htaccess and the shipped rules and exits 1 while they
differ, see Setting up .htaccess and config.php from the console). Give
upload rights only to the roles that need them. Never serve var/ as static files beyond the shipped allow-list
(storage/images, storage/pdf/*.pdf, storage/original/image/*.{png,jpg,gif,webp,svg} and the public caches):
a server that hands out storage/original/ as files, or PHP's built-in server started without the router of the
built-in server engine, serves uploads in the site's origin. Keep TemporaryDir private to the site
(13.11).
Path traversal and access to internal files
The risk. ../ sequences or encoded variants that make the application read a file outside its directory, and
direct requests for .git, settings/, var/, vendor/, composer.json or the SQLite database.
Built in. Only a fixed list of asset paths is served as files; everything else goes to index.php, so
/settings/site.ini is an Exponential page or a 404, never the file. Dot files and directories return 404.
Velocity refuses symbolic links that lead out of the document root (FollowSymlinks=disabled). The request
classifier writes every traversal attempt to security.log (rule set traversal).
Harden. Never serve the installation with a generic "serve the file if it exists" configuration. Run the check of 13.2 from outside after every change of the web server's configuration.
config.php and its backups. config.php can hold the cluster database password. Never commit it (add
/config.php to your project's .gitignore; the Exponential repository tracks only config.php-RECOMMENDED) and
narrow its mode: chmod 0640 config.php, with the web server's group. Edit it with ./console exp:config:set: the
value of every name with PASS, SECRET, KEY, TOKEN, CREDENTIAL or PWD is shown as ******** unless you ask
for --show-secrets, and a secret written into a config.php every user can read prints a WARN: that says so.
Every edit backs the old file up to var/backup/config/<time>/ (directories of mode 0700, not served by the shipped
rules): include var/backup/ in what you protect and delete old backups
(Setting up .htaccess and config.php from the console, Security).
Cross-site scripting (XSS) and the Content Security Policy
The risk. A value that reaches a page unescaped runs script in an editor's or administrator's browser, with their session.
Built in. Templates escape with |wash; the rich text is stored as XML and rendered through templates; uploaded
SVG is sanitized. A Content Security Policy is sent from site.ini [ContentSecurityPolicy]: script-src 'self' with
a per-request nonce, no inline scripts. The Admin 4, Admin 4 L and AdminUI administrations are written to run under an
enforced policy. Violations are reported to var/log/csp.log and shown under CSP reports (csp/report).
Harden. As shipped, Mode=auto is report-only on administration siteaccesses and off on the sites. Read the
reports, then set Mode=enforce on the administration siteaccess; the classic design stays report-only through
ReportOnlyDesigns[]. Do the same for the public siteaccesses once your designs and extensions report nothing
(Content Security Policy). Developers: escape every output, never build
HTML from request data in PHP, and put scripts in files.
Cross-site request forgery (CSRF)
The risk. Another site makes a signed-in editor's browser send a form: publish, delete, change a role.
Built in. The ezformtoken extension adds a per-session token to every form and checks it on every POST; a
refused token gets the "form expired" page. Session cookies are SameSite=Lax.
Harden. Keep ezformtoken in ActiveExtensions[]; custom AJAX code sends the token too
(13.8).
SQL injection through custom code
The risk. The kernel's queries are parameterized or escaped; the risk is in extensions and site code that build SQL from request data.
Built in. The kernel's own patterns were reviewed and fixed in the 6.0.13 hardening; extensions generated by the
RAD wizards produce safe code since their fix; the request classifier logs injection patterns in URLs (rule set
injection), and request-shield's @attacks rules can refuse them before PHP runs.
Harden (developers). Use eZPersistentObject and the content API; when SQL is unavoidable, $db->escapeString()
for every string and (int) for every number; never pass request data to a shell without escapeshellarg().
Regenerate extensions made by the RAD wizards before their fix
(6.0.13 hardening notes, RAD tools: security,
Datatype and input hardening).
Session hijacking
The risk. A stolen session cookie is as good as the password, and survives a password change on many systems.
Built in. Cookies are HttpOnly and SameSite=Lax, and Secure over HTTPS. The session id changes at sign-in.
A password change signs out every other session of the user, through the password stamp
([PasswordSettings] EndOtherSessions=enabled). A disabled account's sessions end at their next request.
Harden. CookieSecure=true on an HTTPS-only site, a shorter SessionTimeout and RememberMeTimeout (both
ship as 31104000 seconds, 360 days), a separate session cookie per
siteaccess (SessionNameHandler=custom), PHP's session.use_strict_mode=1
(13.7). After a suspected compromise: change the
password, and remove all sessions (Setup > Sessions, with the database session handler).
Open redirects and phishing through your domain
Built in. Every redirect target that comes from outside the code (a form field, the session, a URL parameter),
including those of the sign-in and of two-step sign-in, passes the safe redirect rules
(eZRedirectManager): a target on another host than the site's own, or [SiteSettings] AllowedRedirectHosts, is
refused, as are javascript: and data: targets and the usual encoding tricks.
Harden. Keep AllowedRedirectHosts to hosts you control.
Outdated dependencies and the supply chain
The risk. A known vulnerability in an old release of the kernel, an extension, a Composer dependency, PHP or the database; a tampered file on the server.

Built in. Releases are immutable tags on Packagist and GitHub, with changelogs. Setup > Upgrade check
compares every file with the release's share/filelist.md5 ("Check file consistency") and the schema with the
release's ("Check database consistency").
Harden. Follow the releases of Exponential and of every extension; update a copy first; run composer audit to list
known vulnerabilities in the locked dependencies; keep composer.lock in version control; check file consistency after
every update; update PHP, the database server, the operating system and Velocity's engine as well
(13.15). Review a third-party extension's code before
installing it.
Debug output in production
The risk. Debug reports show SQL, file paths, template names and timings: a map for an attacker.
Built in. [DebugSettings] DebugOutput=disabled, template debug off, errors logged, not shown.
Harden. Keep it off; if you must debug live, limit it with DebugByIP to your own address and remove it again.
PHP itself: display_errors=Off, expose_php=Off
(13.4).
Cron and command-line scripts as root
The risk. A script run as root writes files the web server cannot change later, or that run with root's rights.
Built in. Exponential's command-line scripts refuse to run as the operating system's root user unless
--allow-root-user is given, and resetuserpassword --allow-root-user checks that you really are root.
Harden. Run runcronjobs.php and the console as the site's user from that user's crontab. Use root only for the
few documented tasks, and check afterwards that files in var/ are still owned by the site's user and group.
Velocity, PHP-FPM and user separation
Built in. Velocity's workers never run as root ([ServerSettings] AllowRootWorkers=disabled), its dashboard,
figures and /Q/phpinfo are local only unless you set a token, its control panel is local and password protected, and
its per-site configuration files are 0600. PHP-FPM pools run as the site's user.
Harden. Bind Velocity to 127.0.0.1 when a proxy is in front; set User and Group to the site's user; keep the
code read-only for that user; list exactly your proxies in Q.webserver.proxy.trusted
(13.12).
TLS and security headers
Built in. Five security headers on every page (X-Content-Type-Options, X-Frame-Options,
Content-Security-Policy: frame-ancestors 'self', Referrer-Policy, Permissions-Policy); Velocity serves HTTPS with
automatic certificates and HSTS.
Harden. HTTPS on every name, HTTP redirected, HSTS raised once everything works, certificates watched (13.9).
Backups and recovery
The risk. Ransomware, a failed update or a mistaken deletion; backups that hold secrets in clear text.
Harden. Back up the database, var/storage and settings/override (as secrets, encrypted), keep at least one copy
off the machine and offline, and restore one on a test machine regularly
(10.9 Backups and restore). Keep the break-glass procedure
for the administrator next to the restore procedure.
A shield in front: request-shield
request-shield (source: cjw-network/request-shield, MIT license) is a small web application firewall for PHP by the CJW Network. It looks at every request before the Exponential kernel starts: real visitors pass in microseconds, scanners and broken requests are refused, floods are slowed down, suspicious clients prove they are a browser with an invisible check, and the administration can be limited to a network or put behind that check. Every decision names the rule that made it, and its rules are plain text, one per line. It is recommended for every Exponential site that can be reached from the internet.
How it complements the request classifier
| Kernel request classifier | request-shield | |
|---|---|---|
| What it does | classifies only: probes, traversal, injection, attack tools, bursts of 404s | decides: refuses, slows down, challenges, bans for a while, or only logs (monitor mode) |
| Where it runs | inside the kernel, after the request has started | before the kernel, so a refused request costs about 30 µs instead of a rendered page |
| Its log | var/log/security.log | its own log, and a live view under /rs/ |
| Settings | settings/requestclassifier.ini | its rule files |
They use the same names where they overlap (the classifier's probe and injection rules follow request-shield's
WP-*, SCAN-* and ATK-* names in lower case), so a line in one can be found in the other. A request the shield
refuses never reaches the kernel, so it is not in security.log; in monitor mode the same probe can be in both logs.
Exponential's HTTP cache works with it: a request the shield marks allow-uncached (a made-up path or parameter) is
answered but never stored, so random addresses cannot fill the cache
(HTTP cache: With request-shield in front).
Details: Request logs: Request shield and the classifier.
Installing it
request-shield is not part of Exponential's composer.json; you add it to your installation. Two ways:
- With Composer:
composer require cjw-network/request-shield. - Without Composer: a checkout or release of the library in a directory outside the document root.
Then copy the Exponential example rules from the library's examples/exponential/ directory to a directory the web
server never hands out (the library suggests settings/request-shield/), and choose the main file for your
administration:
| File | When |
|---|---|
exponential.rules | the frontend rules; included by one of the two below, not loaded on its own |
exponential-admin-uri.rules | the administration is the siteaccess /admin on the same host |
exponential-admin-host.rules | the administration has a host of its own (admin.example.org) |
Switching it on
The shield must run at the start of every request.
- Apache or nginx with PHP-FPM:
config.phpruns on every request, so the library's example, aShield::protectFile()call at the top ofconfig.php, works; so does PHP'sauto_prepend_filepointing to the library'sbootstrap.php. - Exponential Velocity:
config.phpruns once per worker, not per request. Since 6.0.15,index.phpruns the file thatconfig.phpnames in the constantEXP_REQUEST_PREPEND_FILEat the start of every request; that file loads the library and callsShield::protectFile().config.php-RECOMMENDEDshows the pattern (a guard that is true only inside a Velocity worker, and a file name that onlyconfig.phpcan set, never one the web server can write). Velocity answers pages from its own response cache before a worker starts, so the shield sees only what reaches a worker.
Then:
- Start in monitor mode. The examples ship with
set mode monitor: every decision is logged as it would be taken, and nobody is refused. Read the log (or the live view) for a few days while editors work. - Adapt the few site-specific lines: the URI siteaccesses that may come first (
EXP-MODULES), the modules of your extensions that take forms (EXP-POST), the site's host names and load balancer (host,trust). - Check and test the rules after every change:
bin/request-shield check <main file>names mistakes by file and line,bin/request-shield test <main file>runs the examples written next to every rule, andbin/request-shield trace <main file> "GET https://www.example.org/..."shows how one request would be decided. - Switch to
set mode enforce. KeepEXP-BANandEXP-STRICTwatched (monitorin front) until their log lines are clean. - To switch it off, use
set mode offrather than removing the include; under Velocity a persistent worker would otherwise keep the last request's marks.
What the Exponential example rules do
- System URLs in the frontend (
EXP-SYSVIEW):/content/view/full/<node>and similar numbered URLs answer 404 on the public site (the administration needs them); the sitemap and tag cloud stay reachable. - Admin modules in the frontend (
EXP-MODULES):setup,class,role,workflow,packageand the other administration modules answer 404 on the public siteaccesses. - Internal files (
EXP-INTERNAL):settings/,var/and the like are refused, as a second line behind the web server's rules. - Forms, sign-in and search (
EXP-POST,EXP-FORMS,EXP-LOGIN): POST only where forms belong, the browser check before the sign-in, registration and password forms, a budget for searches. - The administration (
EXP-ADMIN-*): the browser check before the sign-in form, every parameter scanned for attacks, optionally only the office's network (EXP-ADMIN-NET). - The administration's background requests (
EXP-ADMIN-BG, on an admin hostEXP-AH-BG): Admin 4 loads parts of its pages with a script, today the mega menu's panels (/admin/user/megamenu/<tab>). A background request cannot show the check page, so the shield answers it in a machine-readable way: with a valid pass it goes through; without one it gets the check as JSON (status 429,Content-Type: application/json), never an HTML page, and the page's script reloads the page to pass the check. The shield recognizes such requests bySec-Fetch-Dest: emptyorX-Requested-With: XMLHttpRequest, and the examples name the mega menu's path withapi-pathfor clients that send neither. Add the paths of your own background calls to the same line. - Pace and bans (
EXP-PACE,EXP-BAN): past 120 pages a minute the browser check; ten refusals in ten minutes keep an address out for an hour. Verified search engines are never banned.
The shield does not see static files that the web server serves itself, and it cannot tell a right password from a
wrong one: locking accounts stays Exponential's job (MaxNumberOfFailedLogin, two-step sign-in). The full reasoning
for every rule is in the library's docs/use-cases/exponential.md.
Watching: logs, the request classifier and the audit trail
Hardening without watching only delays the moment you learn of an attack.

| Source | What it records | Where to read it |
|---|---|---|
var/log/security.log | the request classifier's verdicts: probes, traversal, injection, attack tools, 404 bursts | Setup > Logs, ./console exp:logs |
| the audit trail | sign-ins and failures, password changes and resets, role and setting changes, content removed, commands run; hash-chained | Audit, ./console exp:audit tail, exp:audit verify |
var/log/auth.log | every step of two-step sign-in (never a code or a secret) | Setup > Logs |
var/log/csp.log | Content Security Policy violations | CSP reports (csp/report) |
error.log, the web server's logs | errors with a reference id | Setup > Logs (Log viewer) |
| request-shield's log | its decisions | its live view under /rs/ |
Do at least this:
- Run the
frequentcronjob group so the audit's alert rules run, and check who receives alert mail (./console exp:audit alerts recipients) (13.14). - Forward the audit trail to a central log or SIEM with a sink (
[AuditSink_syslog],[AuditSink_webhook]), so a compromised machine cannot quietly rewrite its own history; runexp:audit verifyregularly. - Look at the log viewer's error digest and
security.logweekly; a sudden rise of one rule is worth a look. - Give
audit/readand the log viewer only to administrators.
The request classifier only classifies: nothing is blocked because of a verdict. Blocking is the job of the shield in
front, of the web server, or of a listener you register in requestclassifier.ini [ClassifierSettings] Listeners[]
(Request logs).
Checklist
- To do: Administrator password changed,
var/log/initial-admin-passwordandkickstart.iniremoved. - To do: A personal administrator account per person;
adminretired as a sign-in; a tested break-glass procedure. - To do: Two-step sign-in required for administrators; password policy decided.
- To do: Demo mode off:
EXP_DEMO_MODEset for no process; no Demo mode banner in the administration. - To do: The administration on its own host, restricted by network, VPN or the shield's browser check.
- To do:
CheckValidity=false,AllowWizardOnInstalledSite=disabled;ManagedByDeploymentwhere the configuration comes from version control. - To do: Rewrite rules current (
./console exp:setup:htaccessexits 0); the outside check of 13.2 returns no file. - To do:
config.phpnot in version control, mode0640; old backups invar/backup/removed. - To do:
ezformtokenactive; Content Security Policy enforced on the administration, then on the sites. - To do: Debug output off; PHP
display_errors=Off. - To do: HTTPS everywhere, HSTS,
CookieSecure=true. - To do: Code read-only for the site's user; workers and cronjobs never as root.
- To do: request-shield in front, first in monitor mode, then enforced.
- To do: Audit alerts going to a person, the trail forwarded off the machine; logs read weekly.
- To do: Backups off the machine, restore tested; updates followed and tested;
composer auditclean.
The setting-by-setting go-live checklist is 13.16.
Reporting a vulnerability
Report security issues privately to [email protected], or as a private advisory on GitHub (https://github.com/se7enxweb/exponential/security/advisories/new). Do not open a public issue. Include the version, the PHP version, the engine that serves the site and the steps to reproduce; leave out passwords, secrets and personal data. The policy and the supported versions are in SECURITY.md. The help's report page explains the other trackers.
Related pages
- Chapter 13: Security hardening for production
- Security and audit: lock it down, see who did what, the 30-minute walk-through
- Setting up .htaccess and config.php from the console: the rules file,
config.php, masked secrets and backups - Change the administrator password, The default administrator password
- Security defaults of September 2026, the August 2026 security patches
- Content Security Policy, Request logs, Log viewer, Audit, Maintenance mode
- OWASP Top Ten and Authentication Cheat Sheet; NIST SP 800-63B-4
Become an Exponential Partner
Partners share their information, provide funding, contribute back and develop the products with us, and that is how Exponential grows. A partner website and dashboard are planned for mid-2027: please become a partner today to speed up our growth rate and land new features faster.