How Zana checks the security of every project
Safe defaults, a refusal to start when deployed unsafely, one command that verifies the whole application, a check of the server, and audits whose findings are fixed and published. With real outputs from a project created today.

Most security problems in web applications are not clever attacks. They are ordinary mistakes: debug mode left on in production, a secret copied from an example file, a form without CSRF protection, a test folder uploaded to the server, a proxy setting that trusts the whole internet. Each one is easy to make and easy to miss in a review.
Zana's answer is to check, at every step of a project's life, the things people forget. A Zana application is safe by default when it is written, refuses to start when it is deployed unsafely, is verified by one command before every release, and is inspected again on the server. The framework itself goes through security checks, and through audits whose findings are fixed and published.
Every output in this article is real. It comes from a fresh project created today with Zana 2.19, and from this website.
1. Safe by default, before you write a line
A new project from composer create-project mj-zana/skeleton-web already sends strict security headers on every page:
X-Content-Type-Options: nosniff
Referrer-Policy: strict-origin-when-cross-origin
X-Frame-Options: DENY
Cross-Origin-Opener-Policy: same-origin
Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=()
Content-Security-Policy: default-src 'self'; frame-ancestors 'none'; base-uri 'self'; object-src 'none'; form-action 'self'; script-src 'self' 'nonce-3ej5GK7yHB_iS1i7EXZsHWRO'; img-src 'self' data:
The content security policy only runs scripts from the site itself, or inline scripts carrying a fresh nonce on each request. Even if an attacker manages to inject a script tag, the browser will not run it.
The rest follows the same idea: the safe choice is built in, and costs one line or none.
- Forms are protected against CSRF with one middleware on the route; the back-office screens Zana generates add it for you. A form posted from another site, or without a valid token, is refused. On this website, a project request sent without its token gets
403 Forbidden. A bearer token only lets a request skip the check when the application itself signed it. - Spam and brute force are limited. Two settings add a hidden honeypot field and a minimum fill time, which stop most bots without a CAPTCHA.
#[RateLimit]caps how often a visitor can send a form. Sign-in attempts are limited per account and per address. - Passwords are hashed with Argon2id (or bcrypt, with every byte of a long password still counted). A failed sign-in never reveals whether the account exists, and the session gets a new id at sign-in.
- Output is escaped. Templates escape text by default. Markdown written in the back office escapes raw HTML and removes
javascript:links. - Uploads cannot become code. Files get a random name. Their type is detected from their content, never taken from what the browser claims. Extensions a server or a browser would execute (
php,html,svg,js, …) are replaced by the detected type's. - Secrets never leak through tools.
php bin/console inspectshows configuration keys and their types, never their values. Error logs hold neither stack traces nor passwords.
For applications that need more, the same package offers second factors (TOTP and recovery codes), one-time codes by SMS or WhatsApp, roles per organization or per event, and a tamper-evident security audit log. These are opt-in.
2. An unsafe deployment does not start
Defaults protect the code. The most common real-world failures are in the configuration. So when a Zana application boots, it runs deployment checks, and it refuses to start when something would expose it.
Here is the same project, deployed with debug mode on and the example secret still in place:
APP_ENV=production APP_DEBUG=true APP_SECURITY__SECRET=change-me php bin/console check
✘ The application refuses to start:
- Debug mode is on in production. Set APP_DEBUG=false.
Debug mode is on for the public URL https://www.example.org: errors would show code and paths to visitors. Set APP_ENV=production and APP_DEBUG=false.
security.secret is empty or a placeholder. Generate one, e.g. php -r "echo bin2hex(random_bytes(32));"
[ERROR] The application cannot be built.
In a browser, visitors do not see a stack trace or a file path. They see a neutral page, while the reason goes to the server's error log:
The site is temporarily unavailable. / Le site est momentanément indisponible.
This is a deliberate choice. A site that is briefly down can be fixed in minutes. A site that shows its code to the internet, or signs its sessions with a secret anyone can find on GitHub, may already be compromised by the time someone notices.
3. One command verifies the whole application
php bin/console check runs every verification Zana knows, on every project, in development and in continuous integration:
✔ The container builds
✔ Deployment checks: BuiltInChecks
✔ Deployment checks: LogFilesCheck
✔ All 58 singletons can be created
✔ Architecture: 1 module(s), no violation
✔ MiddlewareCheck
✔ TrustedProxiesCheck
✔ RouteHandlersCheck
✔ RoleAssignmentsCheck
✔ TwoFactorCheck
✔ AgentsCheck
[OK] The application passes its checks.
Several of these lines are about security:
- Middleware: every security middleware (CSRF, authentication, rate limiting, headers) can actually be created. A protection that would crash at runtime is reported now, not discovered in an incident.
- Routes: every route reaches a registered handler and every route middleware exists, so a protection declared on a route really runs.
- Architecture: the modules' boundaries hold. The presentation layer cannot reach the database directly and bypass the rules of the application layer.
- Trusted proxies, second factors and roles: each security feature you turn on comes with its own check of its settings.
Each problem is reported with a clear message and a fix. Here, a typing mistake in the address range of a load balancer:
✘ "10.0.0.0/33" in http.trusted_proxies is not an address or a range, so it matches nothing.
→ Write an address, such as 203.0.113.7, or a range, such as 10.0.0.0/8 or 2001:db8::/32.
[ERROR] The application has errors.
check exits with an error code, so a continuous integration pipeline stops the release. The project skeletons ship that pipeline ready to use: every push and pull request runs the tests and check.
4. The server is checked too
After each deployment, php bin/console doctor checks the server itself: the PHP version and extensions, writable folders, the secret's length, and the deployment checks. Since 2.19, it also notices files that have no business being online:
✔ PHP 8.5.12
✔ Extension json
✔ Extension mbstring
✔ C:/wamp64/www/lab/security-demo/var is writable
✔ security.secret is long enough
✔ Deployment checks: BuiltInChecks
✔ Deployment checks: LogFilesCheck
✔ All 58 singletons can be created
! These files do not belong on a live server: tests, .github, phpunit.xml.dist, .gitignore, AGENTS.md, CLAUDE.md, README.md, .env.example.
→ See what would go: php bin/console release:clean --dry-run
release:clean removes them, and the upload archives built by release:build leave them out from the start. The same rules apply whichever way the code reaches the server: an archive, git pull, or a copied folder.
5. The framework checks itself
The checks above protect each project from its own mistakes. Zana also holds its own code to written rules.
- Every change must pass PHPUnit and PHPStan at level 8, in its package and in every package that depends on it.
- A security check for risky changes. A change that reads outside input, writes files, touches sign-in, permissions, secrets or cryptography, builds SQL or HTML, or adds a dependency gets a security check before it is merged. The contributor names who can reach the code and the worst outcome. They then attack it by running hostile input against the real code, and keep a test for every hole they find, so it can never come back unnoticed.
- The same rule for AI agents. Since 2.19, the
AGENTS.mdfile of every Zana project asks AI coding agents for that same security check on risky changes. They must report what was attacked, what was found, and what was not covered.
6. Audits, and what we do with what they find
Rules are not enough on their own, so Zana's security code is audited. The latest audit covered the security code written from 2.1 to 2.18. It found real weaknesses, and they were fixed and released on the same day as 2.19.1 and 2.19.2:
- a second factor that could not be read, after its key changed, no longer lets the password alone sign in;
- changing a password signs out the account's other sessions;
- only a valid bearer token skips the CSRF checks. Before, any
Authorizationheader was enough, and a back-office preview link could also skip them; - sign-in limits count failed attempts only, and the token endpoint of the API skeleton takes as long for an unknown address as for a known one, so response time no longer reveals which accounts exist;
- behind a proxy, only one forwarding header is read, and an address range that cannot be read now matches nothing instead of everything (the
checkshown above); - uploads drop extensions a browser would run, such as
.htmland.svg; - pages carrying a CSP nonce are no longer cached.
Under the framework's own rule, every hole found is kept as a test, so it cannot come back unnoticed. The release notes say exactly what changed, what to check, and how to upgrade, which for most projects is:
composer update
php bin/console check
We publish this list deliberately. A framework that never announces a security fix is not a framework without weaknesses; it is a framework nobody is looking at. What a project should expect is that weaknesses are looked for, fixed quickly, explained plainly, and guarded against with tests. That is the standard Zana holds itself to.
What remains your job
The checks cover configuration, wiring, deployment, and the security code of the framework. They cannot know your business rules. Who may see a patient's file, refund a ticket or approve a payment is a decision only your team can make, and test. Two habits complete the picture:
- Stay up to date. Run
composer updatewhen a release comes out, and read its notes. All Zana packages share one version number, so there is never a question of which combination is safe. - Make the checks part of every release. Run
checkand the tests in continuous integration, anddoctorafter every deployment. The skeletons already do the first.
A five-minute checklist for your project
- Upgrade to Zana 2.19.2 or later:
composer update. -
php bin/console checkpasses without an error. - Continuous integration runs the tests and
checkon every push. - After each deployment,
php bin/console doctorpasses, andrelease:cleanis part of the deployment. -
php bin/console make:agents --force, so AI agents working on the project get the security check.
If you would like a second pair of eyes on a Zana project, or on an older application you want to bring up to this standard, our team offers security and architecture reviews. Tell us about your project.


