Specification: security defaults of September 2026
Between 22 and 30 September 2026 a set of defaults changed so that a default installation is safe without
extra work. This page lists each default, the setting that controls it, what changed for an existing
installation and how to check it. Read it before you upgrade an installation with its own .htaccess, a caching
proxy, or scripts that read the session cookie; the upgrade checklist at the end is the
short version. Input handling of individual datatypes is in
Datatype and input hardening.
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.
Settings at a glance
| File | Block | Key | Default | Scope |
|---|---|---|---|---|
settings/site.ini | HTTPHeaderSettings | SecurityHeaders[<header>] | see section 1 | installation or siteaccess |
settings/site.ini | Session | CookieSecure | auto | installation or siteaccess |
settings/site.ini | Session | CookieHttponly | true | installation or siteaccess |
settings/site.ini | Session | CookieSameSite | Lax | installation or siteaccess |
settings/httpcache.ini | HttpCacheSettings | TagHeader | disabled (replaces ProxyHeaders) | installation |
settings/velocity.ini | ServerSettings | ResponseHeaders[], ResponseHeadersOnScripts | X-Content-Type-Options, X-Frame-Options, Referrer-Policy, Permissions-Policy with the values of section 1; enabled | Velocity |
settings/velocity.ini | HTTPSSettings | HSTSMaxAge, HSTSIncludeSubDomains, HSTSPreload | 300 (0 writes nothing), disabled, disabled | Velocity |
settings/velocity.ini | CacheSettings | SkipCookies[] | empty (derive the list) | Velocity |
settings/velocity.ini | ServerSettings | User, Group, AllowRootWorkers | AllowRootWorkers=disabled | Velocity |
The three [Session] keys are commented out in the shipped settings/site.ini; the defaults above apply while
they are unset.
1. Security headers on every page
Before 27 September a default installation answered every page, the
administration included, with no X-Content-Type-Options, no framing
restriction, no Referrer-Policy and no Permissions-Policy, on Apache and on
Velocity alike. The administration could be framed by any site (clickjacking on
the publish and delete buttons) and uploaded files could be sniffed into another
type by the browser.
settings/site.ini [HTTPHeaderSettings]:
| Key | Default |
|---|---|
SecurityHeaders[X-Content-Type-Options] | nosniff |
SecurityHeaders[X-Frame-Options] | SAMEORIGIN |
SecurityHeaders[Content-Security-Policy] | frame-ancestors 'self' |
SecurityHeaders[Referrer-Policy] | strict-origin-when-cross-origin |
SecurityHeaders[Permissions-Policy] | camera=(), microphone=(), payment=(), usb=() |
SecurityHeaders[Strict-Transport-Security] | empty (off; only ever sent over HTTPS) |
- An empty value drops a header.
HeaderListoverrides per path still win. Names and values that could split the response are ignored. - To allow a trusted editor host to frame the admin, override the policy:
SecurityHeaders[Content-Security-Policy]=frame-ancestors 'self' https://edit.example.com. - Turn HSTS on only when the whole site is HTTPS-only:
SecurityHeaders[Strict-Transport-Security]=max-age=31536000. - Pages answered from the HTTP cache carry the same headers. The cache stores the configured headers with each page; the key carries the scheme so a header meant for HTTPS never reaches an HTTP answer.
- Velocity sends the same headers on static files, its own error pages,
redirects and 304 answers (
[ServerSettings] ResponseHeaders[]) and on the answers of the three front controllers (ResponseHeadersOnScripts=enabled, only where the script sent none), plusStrict-Transport-Securityon every HTTPS answer ([HTTPSSettings] HSTSMaxAge=300,HSTSIncludeSubDomains,HSTSPreload;HSTSMaxAge=0writes nothing). Needs the engine release that readsQ.webserver.headers,headersOnScriptsandhsts; an older one ignores them.
Check:
curl -sI https://example.com/ | grep -i -E 'x-content-type|x-frame|referrer|permissions|content-security'2. The session cookie
The session cookie is the login, yet it went out without HttpOnly, SameSite or
Secure unless the site had set them, which a default installation never did. A
script injected into any page could read the admin session; a forged cross-site
form carried it along; over HTTPS it was still offered to plain HTTP.
settings/site.ini [Session]:
| Key | Default | Meaning |
|---|---|---|
CookieSecure | auto | auto marks the cookie Secure whenever the request itself came over HTTPS; true and false still force it. |
CookieHttponly | true | Scripts cannot read the cookie. |
CookieSameSite | Lax | Lax, Strict, None (honoured only together with Secure) or empty to send no attribute. |
setCookieParams() uses the array form of session_set_cookie_params();
Velocity's session layer already honours it, so both servers send the same
cookie. Explicit settings still win. If an integration needs cross-site cookies,
set CookieSameSite=None and CookieSecure=true.
3. Login and account recovery
| Behaviour | Before | Now |
|---|---|---|
| Failed login timing | Unknown user name returned at once; a known one ran the password hash (about a quarter second with the default hash), so three requests per name told which accounts exist. | When no account matches, eZUser computes a hash of the submitted password with the configured hash type, costing the same as the check it stands in for. |
| Forgot password | "There is no registered user with that email address" (also on the admin login form) allowed testing addresses one by one. | A valid but unknown address gets the same page as a known one ("a mail has been sent if an account is registered"); only input that is not an address at all is refused. Templates wash every address they print. |
Forgot-password link key (ezmbpaex 6.0.3) | Carries a key that cannot be guessed. | |
| Non-string password or hash | Cast to string; true could match a stored true. | Never authenticates. |
| First administrator password of a kickstart | Guessable default, or the site data's own password when no e-mail was given. | Random 20 characters, printed once and written to var/log/initial-admin-password. See Installing in one command. |
4. Files the web server serves
.htaccess_root and .htaccess_root_static (and Velocity, which uses the same
list for every engine) changed so that Apache and Velocity serve rule for rule the
same files:
- A
.php,.phtmlor.pharfile below the document root answers 404, so only the front controllers at the root run. Before, any.phpin the package store (56 in the demo packages), in an extension or undervar/was executed when its path was asked for. - Only the preview images (png, jpg, gif, webp) of imported packages are served
from
var/storage/packages. Before, package XML, SQL, templates and settings were readable. SVG is left out on purpose because it can carry script. - A dot file or dot directory (
.git,.env,.htaccess) answers 404 wherever it lies;.well-knownstays reachable. - The site's own references are served: the service worker
/index.js(and/sw.js, its former name, kept for browsers registered before the rename of 30 September), thevendorandmediafolders of an extension's design (the layout editor's scripts and fonts) and the image files among the stored originals (so SVG images are shown). Other originals stay behindcontent/download. - Fonts of an extension's design are no longer refused by the shipped rules.
- Velocity serves from
var/only image files belowstorage/original/image(png, jpeg, gif, webp, svg) and only the previews from the package store. Before, every file belowvar/*/storage/original/imageand the whole package store were handed out.
Upgrade note: if you maintain your own copy of .htaccess, merge these rules.
The shipped example is doc/eng-us/examples/ and the root files .htaccess_root and
.htaccess_root_static.
5. Caching and personal pages
- A page that carries a form token (
csrf-tokenmeta tag, hidden field in every POST form) is sentCache-Control: private, no-cache, must-revalidate. Before,no-cacheonly asked for revalidation, and a shared cache that did not know the session cookie could store the signed-in page with its token and hand it to every anonymous visitor. - Velocity tells its response cache which cookies mean "personal": the session
cookie as the handler names it (
session.namefor thedefaulthandler,[Session] SessionNamePrefixas a prefix forcustom) plusis_logged_in. An explicit list replaces the derived one:SkipCookies[]invelocity.ini[CacheSettings](the former[ServerSettings] CacheSkipCookiesstill works; a value in[CacheSettings]wins; default empty = derive the list). Before, an installation on the default handler (PHPSESSID) got a skip list ofeZSESSIDand never matched its own cookie, so with the response cache on, a signed-in request could be answered from the cache. - Purge tags are no longer public. Every response of a cached siteaccess used
to carry the page's purge tags twice (
xkeyandSurrogate-Key), naming its nodes, objects and layouts.httpcache.ini [HttpCacheSettings] TagHeaderreplacesProxyHeaders; the defaultdisabledsends none. Set it to the one header your purging proxy reads (xkeyfor Varnish,Surrogate-Keyfor Fastly,Cache-Tagfor Cloudflare).ProxyHeaders=enabledin an existing override still meansxkey. A name that is not an HTTP token or is a framing header is refused. See HTTP cache.
6. Other fixes with a security effect
| Fix | Detail |
|---|---|
| Phar stream wrapper | The bootstrap unregisters the phar stream wrapper, guarded by PHP_SAPI !== 'cli'. A persistent-worker server runs public traffic under the CLI SAPI, so the protection was missing exactly where it is needed (measured: still registered on the worker server, unregistered under FPM). The guard now asks whether the request is a web request. |
| Package view and viewfile | A package description is escaped; a package file opened directly gets a sandboxing Content-Security-Policy. |
| Package datatype | Cannot read a package outside the repository or write an INI file outside a configured siteaccess. |
| INI writer | Values and names cannot inject lines or write outside the settings directory. |
| Mail headers, download file names | See Datatype and input hardening. |
| REST | An unauthenticated request answers 401 with WWW-Authenticate on every server (the missing-credentials case used to state HTTP/1.1 500); the OAuth Authorization header is read case-insensitively with a HTTP_AUTHORIZATION fallback; an unknown token answers 401 on SQLite sites and on persistent workers. |
| Node sorting | content/action requires edit permission and a known sort field and order. |
| Form token refusals | A 403 page with no token in the log. See form expired page. |
| Velocity workers | [ServerSettings] User, Group, AllowRootWorkers=disabled: workers drop root after they start, so files they write belong to the site user. |
| Anonymous forms | No longer subscribe the shared anonymous account. |
| Outgoing identity | The static cache generator, link checker, RSS import, exchange rate and package downloads, outgoing mail and the REST realm identify themselves as Exponential. A web server rule that matches the former static cache user agent must be changed. |
Upgrade checklist
- Compare your
.htaccesswith.htaccess_rootand merge the new rules. - Look at your pages' response headers (command above) and decide whether you
need to relax
Content-Security-Policyfor a trusted editor host. - If a proxy in front of the site purged by
xkeyorSurrogate-Key, set[HttpCacheSettings] TagHeaderinsettings/override/httpcache.ini.append.php. - If a script of yours depended on a non-HttpOnly or cross-site session cookie,
set
[Session] CookieHttponlyorCookieSameSiteexplicitly. - Change the static cache user agent in any web server rule that matched the old name.
- Clear caches after the changes:
php bin/php/ezcache.php --clear-all --allow-root-user.
Related pages
- Specifications: Datatype and input hardening, Security hardening of August 2026 (the earlier patch set), Security hardening 6.0.13, Velocity HTTP/2 and security
- Upgrade notes: Hardening guide, HTTP cache, Behaviour changes, 16 to 30 September 2026, RAD tools — security
- Features: Velocity, Form expired page, Installing in one command
- Changelog 6.0.15, chronicles 16 to 30 September 2026 and February 2026