- Add comprehensive health check system with multiple endpoints - Add Prometheus metrics endpoint - Add production logging configurations (5 strategies) - Add complete deployment documentation suite: * QUICKSTART.md - 30-minute deployment guide * DEPLOYMENT_CHECKLIST.md - Printable verification checklist * DEPLOYMENT_WORKFLOW.md - Complete deployment lifecycle * PRODUCTION_DEPLOYMENT.md - Comprehensive technical reference * production-logging.md - Logging configuration guide * ANSIBLE_DEPLOYMENT.md - Infrastructure as Code automation * README.md - Navigation hub * DEPLOYMENT_SUMMARY.md - Executive summary - Add deployment scripts and automation - Add DEPLOYMENT_PLAN.md - Concrete plan for immediate deployment - Update README with production-ready features All production infrastructure is now complete and ready for deployment.
326 lines
9.4 KiB
PHP
326 lines
9.4 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Tests\Security\AuthenticationTests;
|
|
|
|
use Tests\Security\SecurityTestCase;
|
|
use App\Framework\Http\HttpRequest;
|
|
use App\Framework\Http\Method;
|
|
|
|
/**
|
|
* Session Security tests
|
|
*
|
|
* Tests session hijacking, session fixation, and session timeout mechanisms
|
|
*/
|
|
final readonly class SessionSecurityTest extends SecurityTestCase
|
|
{
|
|
/**
|
|
* Test session hijacking prevention
|
|
*/
|
|
public function testPreventsSessionHijacking(): void
|
|
{
|
|
// Simulate session creation
|
|
session_start();
|
|
$originalSessionId = session_id();
|
|
$_SESSION['user_id'] = 123;
|
|
$_SESSION['ip_address'] = '192.168.1.100';
|
|
$_SESSION['user_agent'] = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)';
|
|
|
|
// Simulate hijack attempt from different IP
|
|
$hijackAttempt = $this->detectSessionHijack(
|
|
sessionIp: '192.168.1.100',
|
|
requestIp: '203.0.113.42', // Different IP
|
|
sessionUserAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)',
|
|
requestUserAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'
|
|
);
|
|
|
|
if (!$hijackAttempt) {
|
|
throw new \RuntimeException('Session hijacking from different IP not detected');
|
|
}
|
|
|
|
echo "✅ Session hijacking from different IP detected\n";
|
|
|
|
// Simulate hijack attempt from different User-Agent
|
|
$hijackAttempt = $this->detectSessionHijack(
|
|
sessionIp: '192.168.1.100',
|
|
requestIp: '192.168.1.100',
|
|
sessionUserAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)',
|
|
requestUserAgent: 'curl/7.68.0' // Different User-Agent
|
|
);
|
|
|
|
if (!$hijackAttempt) {
|
|
throw new \RuntimeException('Session hijacking from different User-Agent not detected');
|
|
}
|
|
|
|
echo "✅ Session hijacking from different User-Agent detected\n";
|
|
}
|
|
|
|
/**
|
|
* Test session fixation prevention
|
|
*/
|
|
public function testPreventsSessionFixation(): void
|
|
{
|
|
// Attacker provides session ID
|
|
session_id('attacker_controlled_session_id');
|
|
session_start();
|
|
|
|
$oldSessionId = session_id();
|
|
|
|
// User logs in - session should be regenerated
|
|
$this->regenerateSessionOnLogin();
|
|
|
|
$newSessionId = session_id();
|
|
|
|
if ($oldSessionId === $newSessionId) {
|
|
throw new \RuntimeException('Session ID not regenerated after login - vulnerable to session fixation');
|
|
}
|
|
|
|
echo "✅ Session ID regenerated after login (fixation prevention)\n";
|
|
}
|
|
|
|
/**
|
|
* Test session timeout
|
|
*/
|
|
public function testEnforcesSessionTimeout(): void
|
|
{
|
|
session_start();
|
|
$_SESSION['last_activity'] = time() - 3600; // 1 hour ago
|
|
|
|
$isExpired = $this->checkSessionTimeout(
|
|
lastActivity: $_SESSION['last_activity'],
|
|
timeoutSeconds: 1800 // 30 minute timeout
|
|
);
|
|
|
|
if (!$isExpired) {
|
|
throw new \RuntimeException('Expired session not detected');
|
|
}
|
|
|
|
echo "✅ Session timeout enforced (30 minute inactivity)\n";
|
|
}
|
|
|
|
/**
|
|
* Test session data integrity
|
|
*/
|
|
public function testValidatesSessionDataIntegrity(): void
|
|
{
|
|
session_start();
|
|
$_SESSION['user_id'] = 123;
|
|
$_SESSION['role'] = 'user';
|
|
|
|
// Simulate tampering
|
|
$_SESSION['role'] = 'admin'; // Privilege escalation attempt
|
|
|
|
$isTampered = $this->detectSessionTampering(
|
|
originalData: ['user_id' => 123, 'role' => 'user'],
|
|
currentData: ['user_id' => 123, 'role' => 'admin']
|
|
);
|
|
|
|
if (!$isTampered) {
|
|
throw new \RuntimeException('Session tampering not detected');
|
|
}
|
|
|
|
echo "✅ Session data tampering detected\n";
|
|
}
|
|
|
|
/**
|
|
* Test session cookie security attributes
|
|
*/
|
|
public function testSessionCookieSecurityAttributes(): void
|
|
{
|
|
$cookieParams = session_get_cookie_params();
|
|
|
|
$issues = [];
|
|
|
|
// Check HttpOnly flag
|
|
if (!$cookieParams['httponly']) {
|
|
$issues[] = 'HttpOnly flag not set (vulnerable to XSS)';
|
|
}
|
|
|
|
// Check Secure flag
|
|
if (!$cookieParams['secure']) {
|
|
$issues[] = 'Secure flag not set (session can be transmitted over HTTP)';
|
|
}
|
|
|
|
// Check SameSite attribute
|
|
if (empty($cookieParams['samesite']) || !in_array($cookieParams['samesite'], ['Strict', 'Lax'])) {
|
|
$issues[] = 'SameSite attribute not properly set (vulnerable to CSRF)';
|
|
}
|
|
|
|
if (!empty($issues)) {
|
|
throw new \RuntimeException(
|
|
"Session cookie security issues:\n" . implode("\n", $issues)
|
|
);
|
|
}
|
|
|
|
echo "✅ Session cookie has proper security attributes (HttpOnly, Secure, SameSite)\n";
|
|
}
|
|
|
|
/**
|
|
* Test concurrent session limit
|
|
*/
|
|
public function testEnforcesConcurrentSessionLimit(): void
|
|
{
|
|
$userId = 123;
|
|
$maxSessions = 3;
|
|
|
|
// Simulate active sessions
|
|
$activeSessions = [
|
|
'session1' => ['created' => time() - 100],
|
|
'session2' => ['created' => time() - 200],
|
|
'session3' => ['created' => time() - 300],
|
|
];
|
|
|
|
// Try to create 4th session
|
|
$canCreateSession = $this->checkConcurrentSessionLimit(
|
|
userId: $userId,
|
|
activeSessions: $activeSessions,
|
|
maxSessions: $maxSessions
|
|
);
|
|
|
|
if ($canCreateSession) {
|
|
throw new \RuntimeException('Concurrent session limit not enforced');
|
|
}
|
|
|
|
echo "✅ Concurrent session limit enforced (max {$maxSessions} sessions)\n";
|
|
}
|
|
|
|
/**
|
|
* Test session destruction on logout
|
|
*/
|
|
public function testProperSessionDestruction(): void
|
|
{
|
|
session_start();
|
|
$_SESSION['user_id'] = 123;
|
|
$_SESSION['authenticated'] = true;
|
|
|
|
// Logout
|
|
$this->destroySession();
|
|
|
|
if (isset($_SESSION['user_id']) || isset($_SESSION['authenticated'])) {
|
|
throw new \RuntimeException('Session data not properly destroyed on logout');
|
|
}
|
|
|
|
if (session_status() === PHP_SESSION_ACTIVE) {
|
|
throw new \RuntimeException('Session still active after logout');
|
|
}
|
|
|
|
echo "✅ Session properly destroyed on logout\n";
|
|
}
|
|
|
|
/**
|
|
* Run all session security tests
|
|
*/
|
|
public function runAllTests(): array
|
|
{
|
|
$results = [];
|
|
|
|
try {
|
|
$this->testPreventsSessionHijacking();
|
|
$results['session_hijacking'] = 'PASS';
|
|
} catch (\Exception $e) {
|
|
$results['session_hijacking'] = 'FAIL: ' . $e->getMessage();
|
|
}
|
|
|
|
try {
|
|
$this->testPreventsSessionFixation();
|
|
$results['session_fixation'] = 'PASS';
|
|
} catch (\Exception $e) {
|
|
$results['session_fixation'] = 'FAIL: ' . $e->getMessage();
|
|
}
|
|
|
|
try {
|
|
$this->testEnforcesSessionTimeout();
|
|
$results['session_timeout'] = 'PASS';
|
|
} catch (\Exception $e) {
|
|
$results['session_timeout'] = 'FAIL: ' . $e->getMessage();
|
|
}
|
|
|
|
try {
|
|
$this->testValidatesSessionDataIntegrity();
|
|
$results['data_integrity'] = 'PASS';
|
|
} catch (\Exception $e) {
|
|
$results['data_integrity'] = 'FAIL: ' . $e->getMessage();
|
|
}
|
|
|
|
try {
|
|
$this->testSessionCookieSecurityAttributes();
|
|
$results['cookie_security'] = 'PASS';
|
|
} catch (\Exception $e) {
|
|
$results['cookie_security'] = 'FAIL: ' . $e->getMessage();
|
|
}
|
|
|
|
try {
|
|
$this->testEnforcesConcurrentSessionLimit();
|
|
$results['concurrent_sessions'] = 'PASS';
|
|
} catch (\Exception $e) {
|
|
$results['concurrent_sessions'] = 'FAIL: ' . $e->getMessage();
|
|
}
|
|
|
|
try {
|
|
$this->testProperSessionDestruction();
|
|
$results['session_destruction'] = 'PASS';
|
|
} catch (\Exception $e) {
|
|
$results['session_destruction'] = 'FAIL: ' . $e->getMessage();
|
|
}
|
|
|
|
return $results;
|
|
}
|
|
|
|
private function detectSessionHijack(
|
|
string $sessionIp,
|
|
string $requestIp,
|
|
string $sessionUserAgent,
|
|
string $requestUserAgent
|
|
): bool {
|
|
// IP mismatch detection
|
|
if ($sessionIp !== $requestIp) {
|
|
return true;
|
|
}
|
|
|
|
// User-Agent mismatch detection
|
|
if ($sessionUserAgent !== $requestUserAgent) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private function regenerateSessionOnLogin(): void
|
|
{
|
|
session_regenerate_id(true);
|
|
}
|
|
|
|
private function checkSessionTimeout(int $lastActivity, int $timeoutSeconds): bool
|
|
{
|
|
$inactiveTime = time() - $lastActivity;
|
|
return $inactiveTime > $timeoutSeconds;
|
|
}
|
|
|
|
private function detectSessionTampering(array $originalData, array $currentData): bool
|
|
{
|
|
// Simplified tampering detection
|
|
foreach ($originalData as $key => $value) {
|
|
if (!isset($currentData[$key]) || $currentData[$key] !== $value) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private function checkConcurrentSessionLimit(
|
|
int $userId,
|
|
array $activeSessions,
|
|
int $maxSessions
|
|
): bool {
|
|
return count($activeSessions) < $maxSessions;
|
|
}
|
|
|
|
private function destroySession(): void
|
|
{
|
|
$_SESSION = [];
|
|
session_destroy();
|
|
}
|
|
}
|