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 the cl_celena_users table.
  • 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 validates manifest.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.