This page is for site owners who see "form expired" pages or warnings, and for developers who handle them.
Exponential protects every form against cross-site request forgery with a form token (the ezformtoken extension in
the kernel). When a POST arrives without its token, or with a wrong one, the visitor used to see a 500 page, and the log
recorded "Unexpected error" for what is really a refusal. Since 27 September 2026 the answer is a clear page with the
right status.
What a visitor sees
A page that says, in plain words:
- the form has expired: the page was open for a long time, or the form was sent from another page;
- nothing was saved;
- Reload the form and send it again, or go to the front page (the dashboard in the administration).
A signed-out visitor is told they may have to sign in again; when the refused form was itself a sign-in form, the page asks to sign in again. The wording is the same for a missing and a wrong token, and does not talk about security tokens and sessions.
The page is design:error/kernel/6.tpl, shipped in design/standard; the admin designs fall back to it. Check:
ls design/*/templates/error/kernel/6.tplWhile a design has no such template, or it renders empty because the template cache was built before it existed, a built-in explanation is shown.
What the browser and scripts get
| Value | |
|---|---|
| HTTP status | 403 Forbidden (kernel error 6, mapped in error.ini) |
| Headers | Cache-Control: no-store, the configured security headers |
| HTML request | Kernel error 6 through the error module, inside the pagelayout of the module that was posted to; no redirect; session and posted data are left as they are |
XHR, Accept: application/json | A JSON body with the same message |
| Error view title and path | "Form expired" instead of "kernel (6)" |
In the log, a refusal writes one line to var/log/warning.log with the check (missing or wrong), the method, the path
and the siteaccess, never a token. The same refusal again (same check, siteaccess, path and client) within
RefusalLogCollapseSeconds is counted instead of written, and the next line gives the count.
Settings
| File | Block | Key | Default | Meaning |
|---|---|---|---|---|
settings/site.ini | HTMLForms | RefusalLogCollapseSeconds | 60 | Collapse window for repeated refusals. 0 writes every one |
settings/site.ini | HTMLForms | Secret | placeholder text | CSRF secret. Set your own per project |
settings/error.ini | ErrorSettings-kernel | HTTPError[6] | 403 | Status of the refusal |
For developers
ezpFormTokenExceptioncarries the failed check and code 403. The old code threw a plainExceptionthat nothing caught until the uncaught handler.ezpFormTokenRefusalbuilds the answer: status, headers, the template parameters ofdesign:error/kernel/6.tpl(reason,reason_code,referrer,retry_url,is_ajax,title,message,action), the JSON body and the built-in page.- The web kernel catches the exception at
request/input. A refusal escaping anywhere else (the REST or tree menu kernel, a module running the check itself, the uncaught exception handler) gets the same 403 and warning line. - The built-in strings are literal
ezpI18ncalls in the contextskernel/error/formtokenanddesign/standard/error/formtoken, translated into German andeng-US. An unmatched refusal no longer writes anhttp_response_code() has no effectwarning. - A page carrying a form token is marked
Cache-Control: private, no-cache, must-revalidate, so no shared cache can store it with a token and hand it to an anonymous visitor.
Related pages
- Security defaults of September 2026
- Datatype and input hardening
- RAD security
- Behaviour changes, 16 to 30 September 2026
- Changelog 6.0.15
- Chronicle, 16 to 30 September 2026
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. Start with Partners: start here and How to apply.