This guide covers security best practices for building secure applications with Marwa Framework.
use Marwa\Framework\Validation\RequestValidator;
$validator = new RequestValidator();
$rules = [
'email' => 'required|email',
'age' => 'required|integer|min:18',
'name' => 'required|string|max:100',
];
$errors = $validator->validate($request->all(), $rules);
if ($errors->any()) {
return redirect('/form')->withErrors($errors);
}
// Only allow specific values
$allowedTypes = ['image', 'document', 'video'];
$type = $request->input('type');
if (!in_array($type, $allowedTypes, true)) {
throw new InvalidArgumentException('Invalid type');
}
// WRONG - SQL injection vulnerable
$query = "SELECT * FROM users WHERE id = " . $id;
// CORRECT - Parameterized query
$users = db()
->connection()
->select("SELECT * FROM users WHERE id = ?", [$id]);
// Safe query building
$users = User::where('email', $email)
->where('active', true)
->get();
// Hash when creating/updating
$hashedPassword = password_hash($password, PASSWORD_DEFAULT);
// Verify when authenticating
if (password_verify($input, $storedHash)) {
// Success
}
// Check if needs rehashing
if (password_needs_rehash($storedHash, PASSWORD_DEFAULT)) {
// Update hash
}
// In Twig templates - auto-escaped
// In Liquid templates
// Raw output (only when trusted)
{!! $htmlContent !!}
// config/security.php
return [
'csp' => [
'enabled' => true,
'default-src' => "'self'",
'script-src' => "'self' 'unsafe-inline'",
'style-src' => "'self' 'unsafe-inline'",
],
];
// config/session.php
return [
'secure' => true, // HTTPS only
'httpOnly' => true, // No JS access
'sameSite' => 'Strict', // CSRF protection
'encrypt' => true, // Encrypted data
];
// On login
session()->regenerate();
session()->set('user_id', $user->id);
// On logout
session()->invalidate();
$file = $request->file('upload');
if ($file === null) {
return response()->json(['error' => 'No file uploaded'], 400);
}
// Validate file type
$allowedTypes = ['image/jpeg', 'image/png', 'application/pdf'];
if (!in_array($file->getMimeType(), $allowedTypes, true)) {
throw new InvalidArgumentException('Invalid file type');
}
// Validate file size (max 5MB)
if ($file->getSize() > 5 * 1024 * 1024) {
throw new InvalidArgumentException('File too large');
}
// Store files outside public/
$path = storage_path('uploads/' . $filename);
// Generate public URL separately
$url = '/storage/uploads/' . $filename;
// In SecurityMiddleware or .htaccess
if (empty($_SERVER['HTTPS']) || $_SERVER['HTTPS'] === 'off') {
header('Location: https://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI']);
exit;
}
The framework automatically adds security headers:
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
X-XSS-Protection: 1; mode=block
Referrer-Policy: strict-origin-when-cross-origin
// config/security.php
'throttle' => [
'enabled' => true,
'limit' => 60, // requests
'window' => 60, // per seconds
],
// config/cors.php
return [
'allowedOrigins' => ['https://example.com'],
'allowedMethods' => ['GET', 'POST', 'PUT', 'DELETE'],
'allowedHeaders' => ['Content-Type', 'Authorization'],
'maxAge' => 3600,
];
APP_DEBUG=false
APP_ENV=production
logger()->error('Error occurred', [
'user_id' => auth()->id(),
'path' => request()->getUri()->getPath(),
// Don't log passwords or sensitive data
]);
composer audit
composer update --dry-run
Only request necessary permissions:
{
"name": "myapp/package",
"permissions": ["filesystem", "network"]
}
password_hash()