Security
ZephyrPHP takes security seriously, providing built-in protection against common web vulnerabilities.
Security Features
- Authentication - Session and JWT-based auth with remember me
- Authorization - Gates, policies, and role-based access control
- Password Hashing - Argon2id/bcrypt with automatic rehashing
- Rate Limiting - Multiple drivers (File, Redis, APCu, Database)
- Encryption - AES-256-GCM authenticated encryption
- JWT Tokens - Secure API authentication with refresh tokens
- CSRF Protection - Automatic token verification for forms
- XSS Prevention - Automatic output escaping in Twig
- SQL Injection Prevention - Parameterized queries with Doctrine
- Security Headers - CSP with nonces, HSTS, modern isolation headers
- Input Sanitization - Built-in sanitization helpers
- Secure File Uploads - Type validation and safe storage
CSRF Protection
Cross-Site Request Forgery (CSRF) attacks trick users into performing unwanted actions. ZephyrPHP automatically protects against these attacks.
Using CSRF Tokens in Forms
Include the CSRF token in every form that submits data:
<form method="POST" action="/users">
{{ csrf_field()|raw }}
<input type="text" name="name">
<button type="submit">Create User</button>
</form>
This generates a hidden input field:
<input type="hidden" name="_token" value="abc123...">
AJAX Requests
For AJAX requests, include the token in a header:
// Get the token from a meta tag
<meta name="csrf-token" content="{{ csrf_token() }}">
// Include in AJAX requests
fetch('/api/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content
},
body: JSON.stringify(data)
});
Verifying CSRF Tokens
The framework automatically verifies CSRF tokens for POST, PUT, PATCH, and DELETE requests. You can also verify manually:
<?php
use ZephyrPHP\Security\Security;
$security = new Security();
if (!$security->verifyCsrf($this->request->input('_token'))) {
abort(403, 'Invalid CSRF token');
}
XSS Prevention
Cross-Site Scripting (XSS) attacks inject malicious scripts into web pages. Twig automatically escapes all output to prevent this:
{# This is safe - automatically escaped #}
{{ user_input }}
{# Output: <script>alert('xss')</script> #}
{# Rendered as text, not executed #}
Only use |raw when you're absolutely certain the content is safe:
{# Only for trusted content like admin-generated HTML #}
{{ trusted_html|raw }}
Sanitizing Input
Sanitize user input before storing:
<?php
use ZephyrPHP\Security\Security;
$security = new Security();
// Sanitize input (removes dangerous characters)
$name = $security->sanitizeInput($_POST['name']);
// HTML purify (allows safe HTML, removes dangerous tags)
$content = $security->purifyHtml($_POST['content']);
// Strip all HTML tags
$text = strip_tags($_POST['bio']);
SQL Injection Prevention
ZephyrPHP uses Doctrine ORM with prepared statements, preventing SQL injection automatically:
// Safe - uses parameterized queries
$users = User::query()
->where('email', '=', $userInput)
->all();
// Also safe - Doctrine handles escaping
$user = User::findOneBy(['email' => $userInput]);
When using raw queries, always use parameters:
// SAFE - parameterized
$users = $connection->fetchAllAssociative(
'SELECT * FROM users WHERE email = ?',
[$userInput]
);
// UNSAFE - never do this!
$users = $connection->fetchAllAssociative(
"SELECT * FROM users WHERE email = '$userInput'"
);
Security Headers
ZephyrPHP automatically adds security headers to responses:
| Header | Purpose |
|---|---|
X-Content-Type-Options |
Prevents MIME-type sniffing |
X-Frame-Options |
Prevents clickjacking attacks |
X-XSS-Protection |
Enables browser XSS filtering |
Referrer-Policy |
Controls referrer information |
Content-Security-Policy |
Controls resource loading with nonce support |
Permissions-Policy |
Controls browser features and APIs |
Strict-Transport-Security |
Enforces HTTPS (production only) |
Content Security Policy (CSP)
ZephyrPHP provides comprehensive CSP support with nonce-based inline scripts and styles, protecting your application from XSS attacks while still allowing necessary inline code.
CSP Levels
Configure CSP enforcement level via environment variables or the security config:
| Level | Description |
|---|---|
strict |
No unsafe-inline styles, strict-dynamic for scripts |
moderate |
Nonce-based scripts/styles, allows style-src-attr inline (default) |
relaxed |
More permissive, allows unsafe-inline styles |
# .env configuration
CSP_ENABLED=true
CSP_LEVEL=moderate
CSP_REPORT_ONLY=false
CSP_REPORT_URI=/api/csp-report
Using CSP Nonces in Templates
ZephyrPHP automatically generates unique nonces per request. Use them in your Twig templates:
{# Method 1: Get nonce for manual use #}
<script nonce="{{ csp_nonce() }}">
console.log('This script has a nonce!');
</script>
{# Method 2: Use the csp_script helper (recommended) #}
{{ csp_script("console.log('Hello, secure world!');") }}
{# Method 3: For styles #}
<style nonce="{{ csp_style_nonce() }}">
.highlight { color: blue; }
</style>
{# Or use the csp_style helper #}
{{ csp_style('.highlight { color: blue; }') }}
{# Get nonce as HTML attribute #}
<script {{ nonce_attr()|raw }}>...</script>
CSP Meta Tags for Dynamic Scripts
For JavaScript that needs to create inline scripts dynamically, include the CSP meta tag:
{# In your layout head #}
{{ csp_meta() }}
{# This outputs: #}
{# <meta name="csp-nonce" content="abc123..."> #}
{# <meta name="csp-style-nonce" content="def456..."> #}
Then in your JavaScript:
// Get nonce from meta tag
const nonce = document.querySelector('meta[name="csp-nonce"]').content;
// Create a script with the nonce
const script = document.createElement('script');
script.nonce = nonce;
script.textContent = 'console.log("Dynamic script with nonce");';
document.body.appendChild(script);
Adding Custom CSP Sources
Add trusted sources programmatically or via environment variables:
<?php
use ZephyrPHP\Security\Headers;
// Add a CDN for scripts
Headers::addCspSource('script-src', 'https://cdn.example.com');
// Add multiple sources
Headers::addCspSource('img-src', [
'https://images.example.com',
'https://cdn.example.com'
]);
// Via environment variables (comma-separated)
// .env: CSP_SCRIPT_SRC=https://cdn.example.com,https://api.example.com
CSP Report-Only Mode
Test your CSP policy without blocking violations:
<?php
use ZephyrPHP\Security\Headers;
// Enable report-only mode
Headers::reportOnly(true);
// Set violation report endpoint
Headers::setReportUri('/api/csp-report');
// Via .env
// CSP_REPORT_ONLY=true
// CSP_REPORT_URI=/api/csp-report
Violations are logged to storage/logs/csp-violations.log.
Security Configuration File
Configure all security settings in config/security.php:
<?php
return [
'csp' => [
'enabled' => true,
'level' => 'moderate', // strict, moderate, relaxed
'use_nonces' => true,
'report_only' => false,
'report_uri' => '/api/csp-report',
'custom_directives' => [
'script-src' => ['https://cdn.example.com'],
'style-src' => ['https://fonts.googleapis.com'],
],
],
'hsts' => [
'enabled' => true,
'max_age' => 31536000,
'include_subdomains' => true,
'preload' => false,
],
'isolation' => [
'enabled' => false,
'coep' => 'credentialless',
'coop' => 'same-origin',
'corp' => 'same-origin',
],
];
Modern Isolation Headers
Enable cross-origin isolation for powerful features like SharedArrayBuffer:
<?php
use ZephyrPHP\Security\Headers;
// Enable isolation headers
Headers::useIsolationHeaders(true);
// Via .env
// USE_ISOLATION_HEADERS=true
// COEP=credentialless
// COOP=same-origin
// CORP=same-origin
Isolation headers may break third-party resources (iframes, images, scripts). Test thoroughly before enabling in production.
Authentication
ZephyrPHP provides a complete authentication system with session and JWT support.
Basic Authentication
<?php
use ZephyrPHP\Auth\Auth;
// Attempt login
if (Auth::attempt(['email' => $email, 'password' => $password])) {
// Authentication successful
return redirect('/dashboard');
}
// With remember me
Auth::attempt($credentials, remember: true);
// Check if authenticated
if (Auth::check()) {
$user = Auth::user();
$userId = Auth::id();
}
// Logout
Auth::logout();
User Model Setup
Your User model must implement AuthenticatableInterface:
<?php
namespace App\Models;
use ZephyrPHP\Auth\AuthenticatableInterface;
use ZephyrPHP\Auth\Authenticatable;
class User extends Model implements AuthenticatableInterface
{
use Authenticatable;
// Your model code...
}
Auth Middleware
<?php
use ZephyrPHP\Middleware\AuthMiddleware;
use ZephyrPHP\Middleware\GuestMiddleware;
// Protect routes (authenticated users only)
Route::get('/dashboard', [DashboardController::class, 'index'])
->middleware([AuthMiddleware::class]);
// Guest only routes (login, register)
Route::get('/login', [AuthController::class, 'showLogin'])
->middleware([GuestMiddleware::class]);
Password Hashing
Use the Hash class for secure password hashing with Argon2id (recommended) or bcrypt:
<?php
use ZephyrPHP\Security\Hash;
// Hash a password (uses Argon2id by default)
$hash = Hash::make('secret123');
// Verify a password
if (Hash::check('secret123', $hash)) {
// Password matches
}
// Check if rehashing is needed (algorithm/options changed)
if (Hash::needsRehash($hash)) {
$user->password = Hash::make($password);
$user->save();
}
// Use bcrypt instead
Hash::useBcrypt();
Hash::setBcryptCost(12);
// Generate secure random tokens
$token = Hash::randomToken(32); // Hex encoded
$token = Hash::randomUrlSafeToken(); // URL-safe base64
Never store passwords in plain text or use MD5/SHA1 for password hashing.
Authorization (Gates & Policies)
Control what users can do with abilities and policies:
Defining Abilities
<?php
use ZephyrPHP\Auth\Gate;
// Define abilities in a service provider
Gate::define('edit-post', function($user, $post) {
return $user->id === $post->user_id;
});
Gate::define('admin', function($user) {
return $user->role === 'admin';
});
Checking Permissions
<?php
// Check if allowed
if (Gate::allows('edit-post', $post)) {
// Can edit
}
if (Gate::denies('edit-post', $post)) {
abort(403);
}
// Authorize (throws exception if denied)
Gate::authorize('edit-post', $post);
// Check multiple abilities
if (Gate::all(['edit-post' => $post, 'publish-post' => $post])) { ... }
if (Gate::any(['edit-post', 'delete-post'], $post)) { ... }
Policies
<?php
// Register policy
Gate::policy(Post::class, PostPolicy::class);
// PostPolicy.php
class PostPolicy
{
public function update($user, Post $post): bool
{
return $user->id === $post->user_id;
}
public function delete($user, Post $post): bool
{
return $user->id === $post->user_id || $user->isAdmin();
}
}
Authorization Middleware
<?php
use ZephyrPHP\Middleware\CanMiddleware;
Route::put('/posts/{id}', [PostController::class, 'update'])
->middleware([new CanMiddleware('update', Post::class)]);
Route::get('/admin', [AdminController::class, 'index'])
->middleware([CanMiddleware::role('admin')]);
JWT Authentication (API)
Secure your APIs with JSON Web Tokens:
Generating Tokens
<?php
use ZephyrPHP\Auth\JwtToken;
// Configure (in service provider)
JwtToken::configure([
'secret' => env('JWT_SECRET'),
'lifetime' => 3600, // 1 hour
'issuer' => 'your-app',
]);
// Generate token for user
$token = JwtToken::forUser($user);
// Generate with custom payload
$token = JwtToken::generate([
'user_id' => $user->id,
'role' => $user->role,
]);
// Generate refresh token (long-lived)
$refreshToken = JwtToken::generateRefreshToken(['user_id' => $user->id]);
Validating Tokens
<?php
// Validate and decode
$payload = JwtToken::validate($token);
if ($payload === null) {
// Invalid or expired
}
// Check if valid
if (JwtToken::isValid($token)) { ... }
// Get time until expiration
$seconds = JwtToken::expiresIn($token);
// Refresh a token
$newToken = JwtToken::refresh($oldToken);
API Auth Middleware
<?php
use ZephyrPHP\Middleware\ApiAuthMiddleware;
Route::prefix('/api')->middleware([ApiAuthMiddleware::class])->group(function() {
Route::get('/user', [ApiController::class, 'user']);
Route::get('/posts', [ApiController::class, 'posts']);
});
// Client usage:
// Authorization: Bearer <token>
Encryption
Encrypt sensitive data with AES-256-GCM authenticated encryption:
<?php
use ZephyrPHP\Security\Encryption;
// Set key (usually from .env APP_KEY)
Encryption::setKey(env('APP_KEY'));
// Generate a new key
$key = Encryption::generateKey(); // base64:...
// Encrypt string
$encrypted = Encryption::encrypt('sensitive data');
// Decrypt string
$decrypted = Encryption::decrypt($encrypted);
// Encrypt arrays/objects
$encrypted = Encryption::encryptArray(['ssn' => '123-45-6789']);
$data = Encryption::decryptArray($encrypted);
// Create signed tokens (for URLs, etc.)
$token = Encryption::signedToken(['user_id' => 123], time() + 3600);
$data = Encryption::verifySignedToken($token); // null if invalid/expired
Secure File Uploads
Validate and safely handle file uploads:
<?php
use ZephyrPHP\Security\Security;
$security = new Security();
if ($this->request->hasFile('avatar')) {
$file = $this->request->file('avatar');
// Validate file type
$allowedTypes = ['image/jpeg', 'image/png', 'image/gif'];
if (!in_array($file['type'], $allowedTypes)) {
abort(400, 'Invalid file type');
}
// Validate file size (e.g., 2MB max)
if ($file['size'] > 2 * 1024 * 1024) {
abort(400, 'File too large');
}
// Generate safe filename
$extension = pathinfo($file['name'], PATHINFO_EXTENSION);
$safeFilename = bin2hex(random_bytes(16)) . '.' . $extension;
// Move to safe location
$uploadPath = storage_path('uploads/' . $safeFilename);
move_uploaded_file($file['tmp_name'], $uploadPath);
}
Environment Security
- Never commit
.envfiles to version control - Keep
APP_DEBUG=falsein production - Regenerate
APP_KEYif compromised - Use HTTPS in production
# Production .env
APP_ENV=production
APP_DEBUG=false
APP_KEY=your-secret-key-here
# Set secure session cookies
SESSION_SECURE=true
Rate Limiting
Protect against brute force attacks and API abuse with the built-in rate limiter:
Basic Usage
<?php
use ZephyrPHP\Security\RateLimiter;
$key = 'login:' . $request->ip();
// Check if rate limited
if (RateLimiter::tooManyAttempts($key, maxAttempts: 5)) {
$retryAfter = RateLimiter::availableIn($key);
abort(429, "Too many attempts. Try again in {$retryAfter} seconds.");
}
// Increment counter (60 second decay)
RateLimiter::hit($key, decaySeconds: 60);
// Get remaining attempts
$remaining = RateLimiter::remaining($key, 5);
// Clear rate limit
RateLimiter::clear($key);
Attempt Helper
<?php
// Execute callback if not rate limited
$result = RateLimiter::attempt(
key: 'api:' . $userId,
maxAttempts: 100,
callback: fn() => $this->handleRequest(),
decaySeconds: 60
);
if ($result === false) {
// Rate limited
}
Rate Limit Middleware
<?php
use ZephyrPHP\Middleware\RateLimitMiddleware;
// 60 requests per minute
Route::prefix('/api')->middleware([RateLimitMiddleware::perMinute(60)])
->group(function() { ... });
// 5 login attempts per 15 minutes
Route::post('/login', [AuthController::class, 'login'])
->middleware([RateLimitMiddleware::loginAttempts(5)]);
// Custom rate limit
Route::middleware([new RateLimitMiddleware(100, 3600)]) // 100/hour
->group(function() { ... });
Storage Drivers
<?php
// File storage (default)
RateLimiter::driver('file');
// Redis (recommended for production)
RateLimiter::driver('redis');
// Or set Redis connection directly
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
RateLimiter::setRedis($redis);
// APCu (in-memory, single server)
RateLimiter::driver('apcu');
// Database
RateLimiter::setDatabase($pdo);
RateLimiter::createTable($pdo); // Create table if needed
Rate Limit Headers
<?php
// Get headers for response
$headers = RateLimiter::headers($key, maxAttempts: 60);
// Returns:
// X-RateLimit-Limit: 60
// X-RateLimit-Remaining: 55
// X-RateLimit-Reset: 1234567890
// Retry-After: 45
- Set
APP_DEBUG=falseandENV=production - Generate strong
APP_KEYandJWT_SECRET - Enable HTTPS and set
SESSION_SECURE=true - Configure CSP with appropriate sources
- Enable HSTS with preload for production domains
- Implement rate limiting on authentication endpoints
- Use Argon2id for password hashing
- Validate and sanitize all user input
- Use parameterized database queries
- Validate file uploads (type, size, content)
- Keep all dependencies updated
- Monitor CSP violation reports
- Never commit
.envfiles to version control - Regularly rotate encryption keys and tokens
OWASP Top 10 Protection
ZephyrPHP provides built-in protection against the OWASP Top 10 vulnerabilities:
| Vulnerability | Protection |
|---|---|
| A01: Broken Access Control | Gates, Policies, Auth Middleware |
| A02: Cryptographic Failures | AES-256-GCM encryption, Argon2id hashing |
| A03: Injection | Parameterized queries, Input sanitization |
| A04: Insecure Design | Security-first architecture, RBAC |
| A05: Security Misconfiguration | Secure defaults, Security headers |
| A06: Vulnerable Components | Minimal dependencies, Composer audit |
| A07: Auth Failures | Rate limiting, Session security, JWT |
| A08: Data Integrity Failures | CSRF protection, Signed tokens |
| A09: Logging Failures | CSP violation logging, Error logging |
| A10: SSRF | URL validation, Input sanitization |