Security
Security
Security in Celena is not optional — it's a set of mandatory practices. Here's what to use and what not to do.
Input validation
Any user input goes through Celena\Core\Security\Filter:
use Celena\Core\Security\Filter;
$name = Filter::string((string) $request->input('name'));
$slug = Filter::urlSlug((string) $request->input('slug'));
$email = Filter::email((string) $request->input('email'));
$id = (int) $request->input('id');
Don't trust $_GET/$_POST directly — read via $request->input()/query() and filter.
SQL injection
Prepared statements and QueryBuilder only. Concatenating values into SQL is forbidden.
// CORRECT
$conn->builder('users')->where('email', '=', $email)->first();
$conn->run('SELECT * FROM ' . $conn->table('users') . ' WHERE id = ?', [$id]);
// DANGEROUS — never do this
$conn->run("SELECT * FROM users WHERE email = '$email'");
XSS — HTML output
In templates, variables are escaped automatically ({var}). Raw output ({var|raw}) is only for trusted HTML. In PHP use the e() helper:
echo e($userText); // safe
echo $userText; // DANGEROUS if it's user input
The core's Markdown renderer escapes source HTML — user markdown cannot execute <script>.
CSRF
Forms and AJAX requests are protected with a token. In a template:
<form method="post">{csrf} … </form> <!-- hidden _csrf field -->
{csrf format="meta"} <!-- meta tag for fetch/AJAX -->
On the server:
use Celena\Core\Security\Csrf;
if (!Csrf::validate((string) $request->input('_csrf'))) {
return Response::json(['error' => 'csrf'], 419);
}
Authentication and roles
- Passwords are stored as Argon2id (
Celena\Core\Security\Password). They are never stored in plain text. - Login —
/admin/login. Users live in thecl_celena_userstable. - Roles:
admin(everything),editor,author,user,customer. Permission check:
use Celena\Core\Security\Permission;
if (!Permission::can($user, 'news.update')) {
return Response::redirect('/admin/login');
}
Admin controllers check $this->auth->user() at the start of an action.
Rate limiting
Celena\Core\Security\RateLimiter protects sensitive endpoints (login, lead intake, public API) from brute force:
$limiter = $container->make(RateLimiter::class);
if (!$limiter->attempt('login_' . $request->ip(), 5, 60)) { // 5 attempts per minute
return Response::json(['error' => 'rate_limited'], 429);
}
File and zip uploads
- Check the MIME/extension of uploaded files; don't serve them as executable.
- The plugin installer checks archives for ZIP-Slip (escaping the folder), blocks
.phar, and validatesmanifest.json. Repeat the same checks when unpacking any user archive.
CSP and external resources
The Content Security Policy allows scripts only from 'self' and trusted domains (Google services). External CDNs (jQuery, fonts, widgets) are blocked — host assets locally. There is no jQuery on the site.
.htaccess and the server
Don't use <Directory> or php_flag directives in .htaccess with PHP-FPM — it breaks Apache (a 500 error). Static-file cache headers are configured at the nginx level, not in .htaccess.