Security
PivotPHP takes security seriously and provides multiple layers of protection out of the box. This guide covers the security features available and best practices for keeping your application secure.
Overview
PivotPHP includes protection against common security vulnerabilities:
- CSRF (Cross-Site Request Forgery)
- XSS (Cross-Site Scripting)
- SQL Injection
- Session Hijacking
- Brute Force Attacks
- Timing Attacks
- Directory Traversal
CSRF Protection
How CSRF Protection Works
PivotPHP generates unique tokens for each session to verify that authenticated requests are coming from your application:
// Enable CSRF protection globally
$app->middleware(new CsrfMiddleware());
// In your forms
<form method="POST" action="/profile">
<?= csrf_field() ?>
<!-- or -->
<input type="hidden" name="_token" value="<?= csrf_token() ?>">
<!-- form fields -->
</form>
// For AJAX requests
fetch('/api/data', {
method: 'POST',
headers: {
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
});
Excluding Routes
Some routes may need to be excluded from CSRF protection:
$app->middleware(new CsrfMiddleware([
'except' => [
'/webhooks/*',
'/api/public/*'
]
]));
Custom Token Verification
class CustomCsrfMiddleware extends CsrfMiddleware
{
protected function tokensMatch($request): bool
{
$token = $this->getTokenFromRequest($request);
$sessionToken = $request->session()->token();
return hash_equals($sessionToken, $token);
}
}
XSS Protection
Automatic Output Escaping
Always escape user input when displaying it:
// In PHP templates
<?= e($userInput) ?>
<?= escape($comment) ?>
// Escape for different contexts
<?= e($data, 'html') ?> // HTML context (default)
<?= e($data, 'js') ?> // JavaScript context
<?= e($data, 'css') ?> // CSS context
<?= e($data, 'attr') ?> // HTML attribute context
Content Security Policy
Set CSP headers to prevent XSS attacks:
$app->middleware(function($request, $handler) {
$response = $handler->handle($request);
return $response->withHeader(
'Content-Security-Policy',
"default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdn.example.com; style-src 'self' 'unsafe-inline';"
);
});
XSS Protection Headers
class SecurityHeadersMiddleware implements MiddlewareInterface
{
public function process($request, $handler): ResponseInterface
{
$response = $handler->handle($request);
return $response
->withHeader('X-XSS-Protection', '1; mode=block')
->withHeader('X-Content-Type-Options', 'nosniff')
->withHeader('X-Frame-Options', 'SAMEORIGIN')
->withHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
}
}
Authentication
Password Hashing
Always hash passwords using secure algorithms:
use PivotPHP\Security\Hash;
// Hash password
$hashedPassword = Hash::make($request->input('password'));
// Verify password
if (Hash::check($plainPassword, $hashedPassword)) {
// Password is correct
}
// Check if rehashing is needed
if (Hash::needsRehash($hashedPassword)) {
$hashedPassword = Hash::make($plainPassword);
// Update stored password
}
Authentication Example
class AuthController
{
public function login(Request $request, Response $response)
{
$credentials = $request->only(['email', 'password']);
$user = User::where('email', $credentials['email'])->first();
if (!$user || !Hash::check($credentials['password'], $user->password)) {
return $response->unauthorized('Invalid credentials');
}
// Generate secure token
$token = bin2hex(random_bytes(32));
// Store token
$user->api_token = hash('sha256', $token);
$user->save();
return $response->json([
'token' => $token,
'user' => $user
]);
}
}
JWT Authentication
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
class JwtAuth
{
private string $secret;
public function __construct(string $secret)
{
$this->secret = $secret;
}
public function generateToken(User $user): string
{
$payload = [
'iss' => config('app.url'),
'sub' => $user->id,
'iat' => time(),
'exp' => time() + (60 * 60 * 24), // 24 hours
'user' => [
'id' => $user->id,
'email' => $user->email
]
];
return JWT::encode($payload, $this->secret, 'HS256');
}
public function validateToken(string $token): ?object
{
try {
return JWT::decode($token, new Key($this->secret, 'HS256'));
} catch (\Exception $e) {
return null;
}
}
}
Rate Limiting
Protect against brute force attacks and API abuse:
// Global rate limiting
$app->middleware(new RateLimitMiddleware([
'max_attempts' => 60,
'decay_minutes' => 1
]));
// Route-specific limits
$app->post('/login', function($req, $res) {
// Login logic
})->middleware('throttle:5,1'); // 5 attempts per minute
// Custom rate limiting
class CustomRateLimiter extends RateLimitMiddleware
{
protected function resolveRequestSignature($request): string
{
// Use user ID if authenticated, otherwise IP
if ($user = $request->getAttribute('user')) {
return 'user:' . $user->id;
}
return 'ip:' . $request->getServerParams()['REMOTE_ADDR'];
}
}
SQL Injection Prevention
Use Parameter Binding
Always use parameter binding for database queries:
// Using query builder (safe)
$users = DB::table('users')
->where('email', $email)
->where('active', true)
->get();
// Using raw queries with binding (safe)
$users = DB::select(
'SELECT * FROM users WHERE email = ? AND active = ?',
[$email, true]
);
// NEVER do this (vulnerable)
$users = DB::select("SELECT * FROM users WHERE email = '$email'");
Input Validation
$validator = validate($request->all(), [
'email' => 'required|email|max:255',
'age' => 'required|integer|between:18,100',
'role' => 'required|in:admin,user,guest'
]);
if ($validator->fails()) {
return $response->unprocessable($validator->errors());
}
Session Security
Configure Secure Sessions
// In your session configuration
return [
'driver' => 'file',
'lifetime' => 120,
'expire_on_close' => false,
'encrypt' => true,
'secure' => true, // HTTPS only
'httponly' => true, // No JavaScript access
'same_site' => 'lax',
'partitioned' => true
];
Session Fixation Prevention
class SessionSecurityMiddleware implements MiddlewareInterface
{
public function process($request, $handler): ResponseInterface
{
// Regenerate session ID on login
if ($request->getUri()->getPath() === '/login' && $request->isPost()) {
session_regenerate_id(true);
}
// Check for session hijacking
$sessionIp = $_SESSION['ip_address'] ?? null;
$currentIp = $request->getServerParams()['REMOTE_ADDR'];
if ($sessionIp && $sessionIp !== $currentIp) {
session_destroy();
return response()->unauthorized('Session invalid');
}
$_SESSION['ip_address'] = $currentIp;
return $handler->handle($request);
}
}
File Upload Security
Validate File Uploads
class FileUploadController
{
public function upload(Request $request, Response $response)
{
$validator = validate($request->all(), [
'file' => 'required|file|mimes:jpg,png,pdf|max:2048'
]);
if ($validator->fails()) {
return $response->unprocessable($validator->errors());
}
$file = $request->file('file');
// Additional validation
if (!$this->isValidFile($file)) {
return $response->badRequest('Invalid file');
}
// Generate safe filename
$filename = $this->generateSafeFilename($file);
// Store outside web root
$path = storage_path('uploads/' . $filename);
$file->moveTo($path);
return $response->json(['path' => $filename]);
}
private function isValidFile($file): bool
{
// Check actual MIME type
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mimeType = finfo_file($finfo, $file->getStream()->getMetadata('uri'));
finfo_close($finfo);
$allowedTypes = ['image/jpeg', 'image/png', 'application/pdf'];
return in_array($mimeType, $allowedTypes);
}
private function generateSafeFilename($file): string
{
$extension = pathinfo($file->getClientFilename(), PATHINFO_EXTENSION);
return uniqid() . '_' . time() . '.' . $extension;
}
}
Encryption
Data Encryption
use PivotPHP\Security\Encryption;
// Encrypt data
$encrypted = Encryption::encrypt($sensitiveData);
// Decrypt data
$decrypted = Encryption::decrypt($encrypted);
// Encrypt with custom key
$encrypted = Encryption::encrypt($data, $customKey);
Configuration
// In your .env file
APP_KEY=base64:your-32-character-random-string-here
CIPHER=AES-256-CBC
Security Headers
Comprehensive Security Headers
class SecurityHeadersMiddleware implements MiddlewareInterface
{
public function process($request, $handler): ResponseInterface
{
$response = $handler->handle($request);
return $response
// Prevent XSS
->withHeader('X-XSS-Protection', '1; mode=block')
// Prevent MIME sniffing
->withHeader('X-Content-Type-Options', 'nosniff')
// Prevent clickjacking
->withHeader('X-Frame-Options', 'DENY')
// Force HTTPS
->withHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains')
// Control referrer information
->withHeader('Referrer-Policy', 'strict-origin-when-cross-origin')
// Feature policy
->withHeader('Permissions-Policy', 'geolocation=(), microphone=(), camera=()')
// Content Security Policy
->withHeader('Content-Security-Policy', $this->getCspPolicy());
}
private function getCspPolicy(): string
{
return implode('; ', [
"default-src 'self'",
"script-src 'self' 'unsafe-inline' 'unsafe-eval'",
"style-src 'self' 'unsafe-inline'",
"img-src 'self' data: https:",
"font-src 'self'",
"connect-src 'self'",
"frame-ancestors 'none'",
"base-uri 'self'",
"form-action 'self'"
]);
}
}
API Security
API Authentication
class ApiAuthMiddleware implements MiddlewareInterface
{
public function process($request, $handler): ResponseInterface
{
$apiKey = $request->getHeaderLine('X-API-Key');
if (!$apiKey) {
return response()->unauthorized('API key required');
}
// Constant-time comparison to prevent timing attacks
$validKey = hash('sha256', $_ENV['API_KEY']);
$providedKey = hash('sha256', $apiKey);
if (!hash_equals($validKey, $providedKey)) {
return response()->unauthorized('Invalid API key');
}
return $handler->handle($request);
}
}
CORS Configuration
$app->middleware(new CorsMiddleware([
'allowed_origins' => ['https://app.example.com'],
'allowed_methods' => ['GET', 'POST', 'PUT', 'DELETE'],
'allowed_headers' => ['Content-Type', 'Authorization', 'X-API-Key'],
'exposed_headers' => ['X-RateLimit-Remaining'],
'max_age' => 86400,
'credentials' => true
]));
Security Best Practices
- Keep PivotPHP Updated: Regularly update to get security patches
- Use HTTPS: Always use SSL/TLS in production
- Validate All Input: Never trust user input
- Principle of Least Privilege: Give minimum necessary permissions
- Security Audits: Regularly audit your code and dependencies
- Error Handling: Never expose sensitive information in error messages
- Logging: Log security events for monitoring
- Environment Variables: Store sensitive configuration in environment variables
- Dependencies: Keep all dependencies updated
- Input Sanitization: Sanitize data before storage and escape before output
Security Checklist
- CSRF protection enabled
- XSS protection headers set
- All user input validated
- Passwords hashed with bcrypt
- HTTPS enforced in production
- Security headers configured
- Rate limiting implemented
- File uploads validated
- SQL injection prevented
- Sessions configured securely
- API authentication implemented
- Error messages don’t leak information
- Dependencies regularly updated
- Security logging enabled