WavePHP provides built-in protection against common web vulnerabilities through the WavePHP\Security\Security class and WavePHP\HTTP\Middleware\SecurityMiddleware.
- CSRF Protection
- XSS Prevention
- SQL Injection Prevention
- Security Headers
- Rate Limiting
- File Upload Security
- Password Hashing
- Encryption
- SecurityException Handling
- Best Practices
Cross-Site Request Forgery (CSRF) tokens prevent attackers from submitting requests on behalf of your users.
use WavePHP\Security\Security;
// Generate or retrieve a token
$token = Wave::csrfToken();
// Output a hidden form field
echo Wave::csrfField();
// <input type="hidden" name="_token" value="...">
// Output a meta tag (useful for AJAX)
echo Security::csrfMeta();
// <meta name="csrf-token" content="...">
// Verify a submitted token (consumed after verification by default)
if (!Security::verifyCsrfToken($request->input('_token'))) {
return $response->status(419)->json(['error' => 'CSRF token mismatch']);
}Apply CSRF checking globally. GET, HEAD, and OPTIONS requests are excluded by default since they should not change state.
Wave::middleware(Security::csrfMiddleware([
'exclude_methods' => ['GET', 'HEAD', 'OPTIONS'],
'exclude_routes' => ['/api/webhooks/*', '/api/public/*'],
'error_handler' => function ($request, $response) {
return $response->json(['error' => 'CSRF token mismatch'], 419);
},
]));Include the token in a request header:
fetch('/api/endpoint', {
method: 'POST',
headers: {
'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content,
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
});use WavePHP\Security\Security;
// Remove dangerous HTML and script tags
$clean = Security::sanitizeInput($userInput);
// Targeted XSS cleaning
$clean = Security::xssClean($userInput);if (Security::detectXss($userInput)) {
throw new \WavePHP\Exceptions\SecurityException('Potential XSS detected');
}WavePHP templates escape output by default:
{{ $variable }}-- HTML-escaped (safe){!! $variable !!}-- raw output (use only with trusted content)
use WavePHP\HTTP\Middleware\SecurityMiddleware;
Wave::middleware(new SecurityMiddleware([
'xss_protection' => true,
'excluded_routes' => ['/api/raw-content'],
]));Always use parameterized queries. WavePHP's Database class binds parameters automatically:
use WavePHP\Database\Database;
$db = Wave::resolve(Database::class);
// Parameterized query -- values are never interpolated into the SQL string
$users = $db->query(
'SELECT * FROM users WHERE email = ? AND status = ?',
[$email, 'active']
);
// Insert, update, and delete methods bind parameters internally
$db->insert('users', ['name' => $name, 'email' => $email]);
$db->update('users', ['name' => $newName], ['id' => $userId]);The SecurityMiddleware can also detect common SQL injection patterns in request parameters and block them before they reach your application:
Wave::middleware(new SecurityMiddleware([
'xss_protection' => true, // also enables SQL injection scanning
]));Wave::middleware(new SecurityMiddleware([
'xss_protection' => true,
'content_type_sniffing' => true,
'frame_protection' => true,
'frame_option' => 'DENY',
'content_security_policy' => [
'default-src' => "'self'",
'script-src' => "'self'",
'style-src' => "'self' 'unsafe-inline'",
'img-src' => "'self' data: https:",
'frame-ancestors' => "'none'",
],
]));For more control, apply headers in an after-middleware:
Wave::after(function ($request, $response) {
return Security::applySecurityHeaders($response, [
'enable_default_headers' => true,
'content_security_policy' => "default-src 'self'",
'strict_transport_security' => 'max-age=31536000; includeSubDomains; preload',
'custom_headers' => [
'Permissions-Policy' => 'geolocation=(), microphone=(), camera=()',
],
]);
});These headers are applied automatically when enable_default_headers is true:
| Header | Value |
|---|---|
| X-Content-Type-Options | nosniff |
| X-Frame-Options | DENY |
| X-XSS-Protection | 1; mode=block |
| Referrer-Policy | strict-origin-when-cross-origin |
| X-Permitted-Cross-Domain-Policies | none |
Rate limiting protects against brute-force attacks and API abuse.
use WavePHP\Security\Security;
$ip = $request->ip();
$key = $ip . ':login';
// Allow 5 attempts in a 300-second window
if (!Security::checkRateLimit($key, 5, 300)) {
throw \WavePHP\Exceptions\SecurityException::rateLimitExceeded(300);
}$rateLimitMiddleware = function ($request, $response, $next) {
$ip = $request->ip();
$path = $request->path();
$limits = [
'/api/login' => ['max' => 5, 'window' => 300],
'/api/register' => ['max' => 3, 'window' => 3600],
'/api/upload' => ['max' => 10, 'window' => 60],
];
if (isset($limits[$path])) {
$key = $ip . ':' . $path;
$limit = $limits[$path];
if (!Security::checkRateLimit($key, $limit['max'], $limit['window'])) {
throw \WavePHP\Exceptions\SecurityException::rateLimitExceeded($limit['window']);
}
}
return $next($request, $response);
};
Wave::middleware($rateLimitMiddleware);Wave::get('/api/resource', function ($request, $response) {
$key = $request->ip() . ':api';
$max = 100;
$response->header('X-RateLimit-Limit', $max);
$response->header('X-RateLimit-Remaining', Security::getRemainingAttempts($key, $max));
if (!Security::checkRateLimit($key, $max, 60)) {
return $response->status(429)->json(['error' => 'Rate limit exceeded']);
}
return $response->json(['data' => 'resource']);
});use WavePHP\Security\Security;
Wave::post('/upload', function ($request, $response) {
$file = $request->files('avatar');
if (!$file || $file['error'] !== UPLOAD_ERR_OK) {
return $response->status(400)->json(['error' => 'No file uploaded']);
}
// Allow only images, max 5 MB
if (!Security::validateFileUpload($file, ['jpg', 'jpeg', 'png', 'webp'], 5 * 1024 * 1024)) {
return $response->status(400)->json(['error' => 'Invalid file type or size']);
}
// Scan for malicious content
if (Security::scanForMalware($file['tmp_name'])) {
return $response->status(400)->json(['error' => 'File failed security scan']);
}
// Generate a safe random filename and store outside the web root
$ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
$filename = Security::randomString(32) . '.' . $ext;
move_uploaded_file($file['tmp_name'], '../storage/uploads/' . $filename);
return $response->json(['success' => true, 'filename' => $filename]);
});$safeFilename = Security::sanitizeFilename($file['name']);WavePHP blocks executable extensions by default (php, phtml, phar, asp, etc.). You can extend the list:
Security::addDangerousExtension('jar');
$blocked = Security::getDangerousExtensions();WavePHP uses bcrypt by default.
use WavePHP\Security\Security;
// Hash a password
$hash = Security::hashPassword($password);
// Hash with a custom cost factor
$hash = Security::hashPassword($password, ['cost' => 12]);
// Verify a password against a stored hash
if (Security::verifyPassword($password, $hash)) {
// Password is correct
}// Registration
$db->insert('users', [
'email' => $email,
'password' => Security::hashPassword($password),
]);
// Login
$user = $db->findOne('users', ['email' => $email]);
if (!$user || !Security::verifyPassword($password, $user['password'])) {
return $response->status(401)->json(['error' => 'Invalid credentials']);
}WavePHP encrypts data with AES-256-GCM, which provides both confidentiality and integrity.
use WavePHP\Security\Security;
$key = getenv('ENCRYPTION_KEY');
// Encrypt -- returns a base64 string containing IV + ciphertext + auth tag
$encrypted = Security::encrypt($sensitiveData, $key);
// Decrypt -- returns null if the key is wrong or data is corrupted
$decrypted = Security::decrypt($encrypted, $key);
if ($decrypted === null) {
throw new \RuntimeException('Decryption failed');
}// Cryptographically secure hex string
$token = Security::randomString(64);
$apiKey = 'sk_' . Security::randomString(32);Use this when comparing tokens to prevent timing attacks:
if (Security::constantTimeCompare($expected, $provided)) {
// Tokens match
}All security violations throw WavePHP\Exceptions\SecurityException with a specific status code and error code.
| Factory Method | Description | HTTP Status |
|---|---|---|
csrfTokenMismatch() |
CSRF validation failed | 403 |
rateLimitExceeded($retryAfter) |
Too many requests | 429 |
suspiciousInput($field) |
Suspicious input detected | 400 |
invalidToken($type) |
Invalid token | 401 |
tokenExpired($type) |
Token expired | 401 |
ipBlocked($ip, $reason) |
IP address blocked | 403 |
xssDetected($field) |
XSS attack detected | 400 |
sqlInjectionDetected($field) |
SQL injection detected | 400 |
unauthorizedAccess($resource) |
Unauthorized access attempt | 403 |
use WavePHP\Exceptions\SecurityException;
Wave::error(function (\Throwable $e, $request, $response) {
if ($e instanceof SecurityException) {
Wave::error('Security violation', [
'code' => $e->getErrorCode(),
'message' => $e->getMessage(),
'context' => $e->getContext(),
'ip' => $request->ip(),
'path' => $request->path(),
]);
return $response->json([
'error' => $e->getMessage(),
'code' => $e->getErrorCode(),
], $e->getStatusCode());
}
return null; // Let other handlers process the exception
});- Enable CSRF protection on all POST, PUT, and DELETE routes.
- Sanitize user input with
Security::sanitizeInput()before processing or storing it. - Use parameterized queries for every database operation -- never concatenate user input into SQL.
- Hash passwords with bcrypt using
Security::hashPassword()-- never store plaintext. - Encrypt sensitive data at rest with
Security::encrypt()and a strong key stored outside version control. - Rate-limit authentication endpoints to slow down brute-force attacks.
- Validate file uploads for type, size, and content before saving them.
- Set security headers on every response via
SecurityMiddleware. - Log security events so you can detect and respond to incidents.
- Keep dependencies updated to patch known vulnerabilities.
- Authentication -- Login, guards, OAuth2, and password resets
- Getting Started -- Framework overview
- API Reference -- Complete API documentation