Security Features
Built-in security protections and how to use them.
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 <script>
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 #
- A unique token is generated per session
- Forms include the token as a hidden field
- POST/PUT/DELETE requests must include a valid token
- 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 #
- Hardening Checklist - production security steps