Pramnos Security Guide¶
Security is a core concern in Pramnos Framework v1.2. This guide covers built-in security features and best practices.
CSRF Protection¶
Session Token Hardening (v1.2)¶
| Change | Before | After |
|---|---|---|
| Session token entropy | random_bytes(5) → 40-bit |
random_bytes(32) → 256-bit |
| Fingerprint algorithm | md5($ua . $ip . $token) |
hash_hmac('sha256', $ua . $ip, $token) |
| Existing sessions | — | Silently upgraded on first request |
New methods on Session:
| Method | Description |
|---|---|
getCsrfToken(): string |
Returns/generates the synchronizer CSRF token (256-bit) |
verifyCsrfToken(string $submitted): bool |
Timing-safe comparison via hash_equals() |
regenerateCsrfToken(): void |
Regenerate the CSRF token (call after login/logout) |
CsrfMiddleware¶
Validates the token on POST, PUT, PATCH, DELETE. Passes GET, HEAD, OPTIONS, TRACE through unchecked.
// Global protection for all state-changing routes
$router->addGlobalMiddleware(new CsrfMiddleware());
// Per-route
$router->post('/transfer', fn() => ...)
->middleware(new CsrfMiddleware());
Token lookup order per request:
1. $_POST[$fieldName] (default field: _csrf_token)
2. X-CSRF-Token request header
In HTML Forms¶
<!-- Synchronizer token field -->
<?php echo \Pramnos\Http\Middleware\CsrfMiddleware::tokenField(); ?>
<!-- → <input type="hidden" name="_csrf_token" value="…" /> -->
AJAX / Fetch¶
fetch('/api/data', {
method: 'POST',
headers: { 'X-CSRF-Token': document.querySelector('meta[name=csrf-token]').content }
});
Legacy API (unchanged)¶
CsrfMiddleware API¶
| Method | Description |
|---|---|
new CsrfMiddleware(string $fieldName = '_csrf_token') |
Constructor; custom field name for legacy forms |
CsrfMiddleware::token(): string |
Returns the session CSRF token |
CsrfMiddleware::tokenField(string $fieldName = '_csrf_token'): string |
Returns <input type="hidden"> with HTML-escaped token |
Session Security¶
Session Cookie Hardening (v1.2)¶
Three security improvements added transparently:
- Session fixation prevention —
Session::reset()now callssession_regenerate_id(true)on login/logout. An attacker who planted a session ID is immediately locked out. - Strict session ID mode —
session.use_strict_mode = 1is set beforesession_start(). PHP rejects any session ID not generated by itself. - HTTPS detection fix —
Session::isHttps()now accepts both'on'(Apache/nginx) and'1'(IIS/CGI).
Usage¶
The hardening is transparent — existing login/logout code gets protection automatically:
// Session::start() sets strict mode automatically
$session = Session::getInstance();
$session->start();
// Session::reset() regenerates session ID + CSRF token
// Call AFTER setting session data on login/logout
$session->set('userid', $user->userid);
$session->reset();
Session::isHttps()¶
// Before (fragile — missed IIS/CGI '1' value)
if ($_SERVER['HTTPS'] === 'on') { ... }
// After (handles 'on', '1' consistently)
if (Session::isHttps()) {
// set Secure cookie, redirect HTTP→HTTPS, etc.
}
Session Cookie Settings (already set in v1.1, unchanged)¶
// HttpOnly, SameSite=Lax, and Secure (when HTTPS) are set automatically
'session' => [
'secure' => true, // HTTPS only
'http_only' => true, // No JavaScript access
'same_site' => 'Lax', // CSRF protection
],
Password Security¶
Hashing¶
Always hash passwords before storing:
// DO NOT store plain passwords
$plainPassword = $_POST['password'];
// Hash using secure algorithm (bcrypt/scrypt)
$hashedPassword = password_hash($plainPassword, PASSWORD_BCRYPT);
// OR use PHP 8.1+ modern syntax
$hashedPassword = password_hash($plainPassword, PASSWORD_DEFAULT);
// Store $hashedPassword in database
Password Verification¶
// Verify against stored hash
if (password_verify($plainPassword, $storedHash)) {
// Password correct
} else {
// Password incorrect
}
// Check if hash needs rehashing (algorithm updated)
if (password_needs_rehash($storedHash, PASSWORD_DEFAULT)) {
$newHash = password_hash($plainPassword, PASSWORD_DEFAULT);
// Update database with new hash
}
XSS Prevention¶
View Escaping Helpers (v1.2)¶
A global e() function and View::escape() / View::e() instance methods wrap htmlspecialchars() with the safest flags.
Flags used: ENT_QUOTES | ENT_SUBSTITUTE — escapes both single and double quotes; replaces invalid UTF-8 with U+FFFD.
<!-- Short form — most common -->
<h1><?php echo e($model->title); ?></h1>
<p><?php echo e($model->description); ?></p>
<input name="q" value="<?php echo e($request->get('q')); ?>">
<!-- Via $this in a View template -->
<h1><?php echo $this->e($model->title); ?></h1>
<a href="<?php echo $this->escape($model->url); ?>"><?php echo e($model->label); ?></a>
<!-- Trusted HTML — no escaping needed -->
<?php echo $doc->getContent(); ?>
Escaping Table¶
| Input | Output |
|---|---|
<script>alert(1)</script> |
<script>alert(1)</script> |
" onclick="alert(1) |
" onclick="alert(1) |
it's fine |
it's fine |
AT&T |
AT&T |
null / false |
'' (empty string) |
42 / 3.14 |
'42' / '3.14' |
API Reference¶
| Symbol | Description |
|---|---|
e(mixed $value, string $encoding = 'UTF-8'): string |
Global function — HTML-escape a value |
View::escape(mixed $value, string $encoding = 'UTF-8'): string |
Instance method — delegates to e() |
View::e(mixed $value, string $encoding = 'UTF-8'): string |
Short alias for escape() |
Context-Aware Escaping¶
e() is for HTML character escaping only. For other contexts:
// JavaScript context
$escaped = json_encode($text);
// URL context
$escaped = urlencode($text);
// CSS context — whitelist approach
$escaped = preg_replace('![^a-z0-9-]!i', '', $text);
Note:
e()does not filterjavascript:URIs. Validate and whitelist URLs at the application level, or use CSP.
SQL Injection Prevention¶
Use Parameterized Queries¶
// UNSAFE — never do this
$sql = "SELECT * FROM users WHERE email = '" . $_POST['email'] . "'";
$result = $db->query($sql);
// SAFE — use QueryBuilder
$user = $db->queryBuilder()
->from('users')
->where('email', $_POST['email']) // Automatically parameterized
->first();
// SAFE — use prepareQuery with printf-style
$sql = $db->prepareQuery("SELECT * FROM users WHERE email = %s", $_POST['email']);
$result = $db->query($sql);
QueryBuilder Escaping¶
The QueryBuilder automatically handles escaping:
$users = $db->queryBuilder()
->from('users')
->where('username', 'LIKE', '%' . $search . '%') // Auto-escaped
->get();
Authentication¶
Login/Logout¶
// Login
$user = \Pramnos\User\User::authenticate($username, $password);
if ($user) {
$session->set('userid', $user->userid);
// Success
} else {
// Authentication failed
}
// Logout
$session->destroy();
Login Lockout¶
Prevent brute-force attacks:
$lockout = new \Pramnos\Auth\Loginlockout($user);
// Check if user is locked out
if ($lockout->isLocked()) {
return "Too many login attempts. Try again in " . $lockout->getRemainingTime() . " seconds";
}
// Record failed attempt
$lockout->recordFailure();
// Clear failures on success
$lockout->clearFailures();
Two-Factor Authentication¶
Protect accounts with 2FA:
$totp = new \Pramnos\Auth\TOTPHelper($user);
// Generate secret
$secret = $totp->generateSecret();
// Verify code
if ($totp->verify($code)) {
// Code valid
} else {
// Code invalid
}
Content Security Policy¶
CSP Headers¶
Protect against XSS by restricting script sources:
// In controller or middleware
$response->setHeader('Content-Security-Policy',
"default-src 'self'; script-src 'nonce-" . $nonce . "'; style-src 'unsafe-inline'");
// In template
<script nonce="<?php echo $nonce; ?>">
// Only this script executes
</script>
Nonce Generation¶
File Upload Security¶
Validate Uploads¶
if ($_FILES['avatar']['size'] > 5 * 1024 * 1024) {
throw new \RuntimeException('File too large');
}
$allowed = ['jpg', 'png', 'gif'];
$ext = pathinfo($_FILES['avatar']['name'], PATHINFO_EXTENSION);
if (!in_array(strtolower($ext), $allowed)) {
throw new \RuntimeException('File type not allowed');
}
// Verify MIME type
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mime = finfo_file($finfo, $_FILES['avatar']['tmp_name']);
if (!in_array($mime, ['image/jpeg', 'image/png', 'image/gif'])) {
throw new \RuntimeException('Invalid file type');
}
// Move to secure location
move_uploaded_file($_FILES['avatar']['tmp_name'], '/secure/uploads/avatar_' . uniqid() . '.jpg');
Dependency Security¶
Keep Dependencies Updated¶
# Check for security vulnerabilities
composer audit
# Update dependencies
composer update
# Require security patches
composer require symfony/security --security-advisories
Security Headers¶
Recommended Headers¶
// In base controller or middleware
$response->setHeader('X-Content-Type-Options', 'nosniff');
$response->setHeader('X-Frame-Options', 'SAMEORIGIN');
$response->setHeader('X-XSS-Protection', '1; mode=block');
$response->setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
$response->setHeader('Permissions-Policy', 'geolocation=(), microphone=()');
Reference¶
For complete technical details, see the inline documentation in the framework source.
Related Guides: - Pramnos_Authentication_Guide.md — Login lockout, 2FA/TOTP, OAuth2 - Pramnos_Framework_Guide.md — Middleware pipeline, CORS, exception handler - Pramnos_Authorization_Guide.md — Policy engine, gates, access control