-
Notifications
You must be signed in to change notification settings - Fork 0
Security
| Risk | Mitigation |
|---|---|
| XSS | Use e() in every view |
| SQL injection | All Model methods use prepared statements; column/table names are validated against a strict identifier pattern |
| Credential exposure | Credentials only live in .env (excluded from git) |
| Error exposure |
APP_DEBUG=false in production |
| Source code exposure | Only public/ is reachable by the web server; core/, controllers/, models/, config/ sit outside the document root |
| Directory listing |
Options -Indexes in public/.htaccess
|
| Sensitive files (Nginx) |
deny all for .env, .git, .htaccess
|
| Spoofed client IP |
Request::ip() trusts X-Forwarded-For, which any client can set — only rely on it behind a trusted reverse proxy that overwrites that header |
| Password storage |
password_hash()/password_verify() (bcrypt), never plain text — see Authentication
|
| Password reset tokens | Stored as SHA-256 hashes, single-use (deleted after redemption), 1-hour expiry |
| Session fixation |
session_regenerate_id(true) on login and logout |
| Email enumeration |
/forgot-password shows the same message whether or not the email is registered |
Tanuki doesn't enforce CSRF verification by default — the base skeleton avoids anything that would run silently on every request without you choosing to add it.
csrf_token() // returns the current session's token (creates it on first call)
csrf_field() // renders a ready-to-use hidden <input>
csrf_verify($tok) // timing-safe comparison against the session's token1. Add the field to any form that mutates data:
<form method="POST" action="/todo">
<?= csrf_field() ?>
<!-- ... -->
</form>2. Verify it at the top of the controller action:
public function store(): void
{
if (!csrf_verify($this->request->post('_token'))) {
$this->flash('error', 'Your session expired. Please try again.');
$this->redirect('/todo/create');
}
// ...
}Why this isn't wired into the router by default: enforcing it globally for every POST/PUT/DELETE route would require every form in every project — including webhooks and API-style endpoints that don't use sessions — to carry a token, whether they need it or not. Keeping it explicit per form keeps the base skeleton unopinionated. The example TODO CRUD and the tanuki_login/tanuki_admin extensions already use it on every mutating form — copy that pattern for your own resources.
Everything except public/ sits outside the web server's document root. core/, controllers/, models/, and config/ can never be requested directly by URL, no matter how the server is configured. Same pattern used by Laravel, Symfony, and most modern PHP frameworks — see Project Structure.