Docs LATEST

Security Features

Built-in security protections and how to use them.

Security

VelvetCMS includes several security features enabled by default. This page explains what they do and how to configure them.

Output Escaping #

The view engine escapes output by default to prevent XSS attacks:

// In templates - escaped by default
{{ $userInput }}  // HTML entities are escaped

// Raw output (use carefully)
{!! $trustedHtml !!}

The e() helper escapes strings manually:

$safe = e($userInput);  // Converts <script> to &lt;script&gt;

Best practice: Only use {!! !!} for content you've sanitized yourself or that comes from trusted sources (like your own Markdown renderer).

CSRF Protection #

Cross-Site Request Forgery protection prevents malicious sites from submitting forms on behalf of your users.

How It Works #

  1. A unique token is generated per session
  2. Forms include the token as a hidden field
  3. POST/PUT/DELETE requests must include a valid token
  4. Requests with invalid tokens are rejected

Using CSRF Tokens #

In forms:

<form method="POST" action="/submit">
    <input type="hidden" name="_token" value="{{ csrf_token() }}">
    <!-- form fields -->
</form>

Or use the helper:

<form method="POST" action="/submit">
    {{ csrf_field() }}
    <!-- form fields -->
</form>

Excluding Routes #

Some routes (like webhooks) need to skip CSRF verification. Configure exceptions in config/http.php:

'csrf_except' => [
    '/webhooks/*',
    '/api/external/*',
],

Prepared Statements #

All database queries use PDO prepared statements automatically:

// Safe - parameters are bound separately
$users = $db->query('SELECT * FROM users WHERE email = ?', [$email]);

// Also safe - query builder uses prepared statements internally
$users = $db->table('users')->where('email', $email)->get();

This prevents SQL injection regardless of user input. You literally cannot create an injection vulnerability using the standard database methods.

Session Security #

Sessions are configured with secure defaults:

// config/session.php
return [
    'httponly' => true,     // JavaScript can't access cookie
    'secure' => true,       // Cookie only sent over HTTPS
    'same_site' => 'Lax',   // Prevents CSRF via cookie
];

Session Regeneration #

Regenerate session IDs after authentication changes:

$session = app('session');
$session->regenerate();  // New ID, same data

Path Traversal Protection #

File operations validate paths to prevent directory traversal attacks:

// Requesting "../../../etc/passwd" won't work
$page = $pageService->load($userInput);  // Sanitized internally

The asset server also validates requested paths and restricts to allowed directories.

Rate Limiting #

Protect against brute force and abuse with the throttle middleware:

$router->post('/login', [AuthController::class, 'login'])
    ->middleware('throttle:auth');  // 5 attempts per minute (named limiter)

$router->post('/api/data', [ApiController::class, 'store'])
    ->middleware('throttle:api');   // 120 attempts per minute

$router->post('/upload', [UploadController::class, 'store'])
    ->middleware('throttle:10,1');  // Inline: 10 attempts per minute

Built-in limiters #

Name Attempts Window Use case
standard 60 1 min Default for all routes
api 120 1 min API endpoints
auth 5 1 min Login/password reset
strict 10 1 min Sensitive operations

Rate Limit Headers #

Responses include rate limit headers:

X-RateLimit-Limit: 60
X-RateLimit-Remaining: 45
X-RateLimit-Reset: 1706123456

Dynamic Rate Limits #

Register dynamic limiters that adjust based on user or request:

use VelvetCMS\Http\RateLimiting\Limit;
use VelvetCMS\Http\RateLimiting\RateLimiter;

$rateLimiter = app(RateLimiter::class);

$rateLimiter->for('premium-api', function ($request) {
    $userId = $request->session()->get('user_id');
    return $userId && isPremiumUser($userId)
        ? Limit::perMinute(1000)
        : Limit::perMinute(60);
});

IP Whitelist #

Configure trusted IPs that bypass rate limiting in config/http.php:

'rate_limit' => [
    'whitelist' => ['127.0.0.1', '::1'],
],

Content Security #

The view engine doesn't execute PHP in templates by default. Templates use a limited directive syntax that can't run arbitrary code.

Error Handling #

In production, detailed error messages are hidden from users:

// config/app.php
'debug' => false,  // Shows generic error page

Errors are still logged for debugging, just not displayed publicly.

What's Next #