diff --git a/app/includes/security_headers_middleware.php b/app/includes/security_headers_middleware.php
index 329921c..f4ab237 100644
--- a/app/includes/security_headers_middleware.php
+++ b/app/includes/security_headers_middleware.php
@@ -12,88 +12,106 @@
* - Permissions-Policy: Control browser features
*/
-// Get current page
-$current_page = $_GET['page'] ?? 'dashboard';
+function applySecurityHeaders($testMode = false) {
+ $headers = [];
+
+ // Get current page
+ $current_page = $_GET['page'] ?? 'dashboard';
-// Define pages that need media access
-$media_enabled_pages = [
- // 'conference' => ['camera', 'microphone'],
- // 'call' => ['microphone'],
- // Add more pages and their required permissions as needed
-];
+ // Define pages that need media access
+ $media_enabled_pages = [
+ // 'conference' => ['camera', 'microphone'],
+ // 'call' => ['microphone'],
+ // Add more pages and their required permissions as needed
+ ];
-// Strict Transport Security (HSTS)
-// Only enable if HTTPS is properly configured
-if (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on') {
- header('Strict-Transport-Security: max-age=31536000; includeSubDomains; preload');
-}
-
-// Content Security Policy (CSP)
-$csp = [
- "default-src 'self'",
- "script-src 'self' 'unsafe-inline' 'unsafe-eval'", // Required for Bootstrap and jQuery
- "style-src 'self' 'unsafe-inline' https://use.fontawesome.com", // Allow FontAwesome CSS
- "img-src 'self' data:", // Allow data: URLs for images
- "font-src 'self' https://use.fontawesome.com", // Allow FontAwesome fonts
- "connect-src 'self'",
- "frame-ancestors 'none'", // Equivalent to X-Frame-Options: DENY
- "form-action 'self'",
- "base-uri 'self'",
- "upgrade-insecure-requests" // Force HTTPS for all requests
-];
-header("Content-Security-Policy: " . implode('; ', $csp));
-
-// X-Frame-Options (legacy support)
-header('X-Frame-Options: DENY');
-
-// X-Content-Type-Options
-header('X-Content-Type-Options: nosniff');
-
-// Referrer-Policy
-header('Referrer-Policy: strict-origin-when-cross-origin');
-
-// Permissions-Policy
-$permissions = [
- 'geolocation=()',
- 'payment=()',
- 'usb=()',
- 'accelerometer=()',
- 'autoplay=()',
- 'document-domain=()',
- 'encrypted-media=()',
- 'fullscreen=(self)',
- 'magnetometer=()',
- 'midi=()',
- 'sync-xhr=(self)',
- 'usb=()'
-];
-
-// Add camera/microphone permissions based on current page
-$camera_allowed = false;
-$microphone_allowed = false;
-
-if (isset($media_enabled_pages[$current_page])) {
- $allowed_media = $media_enabled_pages[$current_page];
- if (in_array('camera', $allowed_media)) {
- $camera_allowed = true;
+ // Strict Transport Security (HSTS)
+ // Only enable if HTTPS is properly configured
+ if (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on') {
+ $headers[] = 'Strict-Transport-Security: max-age=31536000; includeSubDomains; preload';
}
- if (in_array('microphone', $allowed_media)) {
- $microphone_allowed = true;
+
+ // Content Security Policy (CSP)
+ $csp = [
+ "default-src 'self'",
+ "script-src 'self' 'unsafe-inline' 'unsafe-eval'", // Required for Bootstrap and jQuery
+ "style-src 'self' 'unsafe-inline' https://use.fontawesome.com", // Allow FontAwesome CSS
+ "img-src 'self' data:", // Allow data: URLs for images
+ "font-src 'self' https://use.fontawesome.com", // Allow FontAwesome fonts
+ "connect-src 'self'",
+ "frame-ancestors 'none'", // Equivalent to X-Frame-Options: DENY
+ "form-action 'self'",
+ "base-uri 'self'",
+ "upgrade-insecure-requests" // Force HTTPS for all requests
+ ];
+ $headers[] = "Content-Security-Policy: " . implode('; ', $csp);
+
+ // X-Frame-Options (legacy support)
+ $headers[] = 'X-Frame-Options: DENY';
+
+ // X-Content-Type-Options
+ $headers[] = 'X-Content-Type-Options: nosniff';
+
+ // X-XSS-Protection
+ $headers[] = 'X-XSS-Protection: 1; mode=block';
+
+ // Referrer-Policy
+ $headers[] = 'Referrer-Policy: strict-origin-when-cross-origin';
+
+ // Permissions-Policy
+ $permissions = [
+ 'geolocation=()',
+ 'payment=()',
+ 'usb=()',
+ 'accelerometer=()',
+ 'autoplay=()',
+ 'document-domain=()',
+ 'encrypted-media=()',
+ 'fullscreen=(self)',
+ 'magnetometer=()',
+ 'midi=()',
+ 'sync-xhr=(self)',
+ 'usb=()'
+ ];
+
+ // Add camera/microphone permissions based on current page
+ $camera_allowed = false;
+ $microphone_allowed = false;
+
+ if (isset($media_enabled_pages[$current_page])) {
+ $allowed_media = $media_enabled_pages[$current_page];
+ if (in_array('camera', $allowed_media)) {
+ $camera_allowed = true;
+ }
+ if (in_array('microphone', $allowed_media)) {
+ $microphone_allowed = true;
+ }
+ }
+
+ // Add media permissions
+ $permissions[] = $camera_allowed ? 'camera=(self)' : 'camera=()';
+ $permissions[] = $microphone_allowed ? 'microphone=(self)' : 'microphone=()';
+
+ $headers[] = 'Permissions-Policy: ' . implode(', ', $permissions);
+
+ // Clear PHP version
+ if (!$testMode) {
+ header_remove('X-Powered-By');
+ }
+
+ // Prevent caching of sensitive pages
+ if (in_array($current_page, ['login', 'register', 'profile', 'security'])) {
+ $headers[] = 'Cache-Control: no-store, no-cache, must-revalidate, max-age=0';
+ $headers[] = 'Pragma: no-cache';
+ $headers[] = 'Expires: ' . gmdate('D, d M Y H:i:s', time() - 3600) . ' GMT';
+ }
+
+ if ($testMode) {
+ return $headers;
+ }
+
+ // Apply headers in production
+ foreach ($headers as $header) {
+ header($header);
}
}
-
-// Add media permissions
-$permissions[] = $camera_allowed ? 'camera=(self)' : 'camera=()';
-$permissions[] = $microphone_allowed ? 'microphone=(self)' : 'microphone=()';
-
-header('Permissions-Policy: ' . implode(', ', $permissions));
-
-// Clear PHP version
-header_remove('X-Powered-By');
-
-// Prevent caching of sensitive pages
-if (in_array($current_page, ['login', 'register', 'profile', 'security'])) {
- header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
- header('Pragma: no-cache');
- header('Expires: ' . gmdate('D, d M Y H:i:s', time() - 3600) . ' GMT');
-}
diff --git a/tests/framework/Integration/Security/CsrfProtectionTest.php b/tests/framework/Integration/Security/CsrfProtectionTest.php
new file mode 100644
index 0000000..3e3d6a5
--- /dev/null
+++ b/tests/framework/Integration/Security/CsrfProtectionTest.php
@@ -0,0 +1,153 @@
+ 200, 'message' => ''];
+ }
+
+ // Skip CSRF check for initial login attempt
+ if ($_SERVER['REQUEST_METHOD'] === 'POST' &&
+ isset($_GET['page']) && $_GET['page'] === 'login' &&
+ !isset($_SESSION['username'])) {
+ return ['status' => 200, 'message' => ''];
+ }
+
+ // Check CSRF token for all other POST requests
+ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
+ $token = $_POST['csrf_token'] ?? '';
+ if (!$security->verifyCsrfToken($token)) {
+ // Log CSRF attempt
+ $message = "CSRF attempt detected from IP: " . $_SERVER['REMOTE_ADDR'];
+ TestLogger::insertLog(0, $message, 'system');
+
+ // Return error message
+ return ['status' => 403, 'message' => $message];
+ }
+ }
+
+ return ['status' => 200, 'message' => ''];
+}
+
+class CsrfProtectionTest extends TestCase
+{
+ private $security;
+
+ protected function setUp(): void
+ {
+ parent::setUp();
+ $this->security = \SecurityHelper::getInstance();
+ $_SERVER['REMOTE_ADDR'] = '127.0.0.1';
+ }
+
+ protected function tearDown(): void
+ {
+ parent::tearDown();
+ unset($_SESSION['csrf_token']);
+ unset($_POST['csrf_token']);
+ unset($_GET['page']);
+ unset($_SESSION['username']);
+ }
+
+ public function testCsrfProtectionValidToken()
+ {
+ // Generate CSRF token
+ $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
+ $token = $_SESSION['csrf_token'];
+
+ // Simulate POST request with valid token
+ $_SERVER['REQUEST_METHOD'] = 'POST';
+ $_POST['csrf_token'] = $token;
+
+ // Call CSRF middleware
+ $response = applyCsrfMiddleware();
+
+ // Assert that the response is success
+ $this->assertEquals(200, $response['status']);
+ $this->assertEmpty($response['message']);
+ }
+
+ public function testCsrfProtectionInvalidToken()
+ {
+ // Simulate POST request with invalid token
+ $_SERVER['REQUEST_METHOD'] = 'POST';
+ $_POST['csrf_token'] = 'invalid_token';
+
+ // Call CSRF middleware
+ $response = applyCsrfMiddleware();
+
+ // Assert that the response is forbidden with error message
+ $this->assertEquals(403, $response['status']);
+ $this->assertStringContainsString("CSRF attempt detected from IP", $response['message']);
+ }
+
+ public function testCsrfProtectionGetRequest()
+ {
+ // Simulate GET request without token
+ $_SERVER['REQUEST_METHOD'] = 'GET';
+
+ // Call CSRF middleware
+ $response = applyCsrfMiddleware();
+
+ // Assert that GET requests are allowed without token
+ $this->assertEquals(200, $response['status']);
+ $this->assertEmpty($response['message']);
+ }
+
+ public function testCsrfProtectionInitialLogin()
+ {
+ // Simulate initial login POST request
+ $_SERVER['REQUEST_METHOD'] = 'POST';
+ $_GET['page'] = 'login';
+
+ // Call CSRF middleware
+ $response = applyCsrfMiddleware();
+
+ // Assert that initial login is allowed without token
+ $this->assertEquals(200, $response['status']);
+ $this->assertEmpty($response['message']);
+ }
+
+ public function testCsrfProtectionMissingToken()
+ {
+ // Simulate POST request without token
+ $_SERVER['REQUEST_METHOD'] = 'POST';
+
+ // Call CSRF middleware
+ $response = applyCsrfMiddleware();
+
+ // Assert that missing token is rejected
+ $this->assertEquals(403, $response['status']);
+ $this->assertStringContainsString("CSRF attempt detected from IP", $response['message']);
+ }
+
+ public function testCsrfProtectionEmptyToken()
+ {
+ // Simulate POST request with empty token
+ $_SERVER['REQUEST_METHOD'] = 'POST';
+ $_POST['csrf_token'] = '';
+
+ // Call CSRF middleware
+ $response = applyCsrfMiddleware();
+
+ // Assert that empty token is rejected
+ $this->assertEquals(403, $response['status']);
+ $this->assertStringContainsString("CSRF attempt detected from IP", $response['message']);
+ }
+}
diff --git a/tests/framework/Integration/Security/SecurityHeadersTest.php b/tests/framework/Integration/Security/SecurityHeadersTest.php
new file mode 100644
index 0000000..2b80561
--- /dev/null
+++ b/tests/framework/Integration/Security/SecurityHeadersTest.php
@@ -0,0 +1,160 @@
+assertContains('X-Frame-Options: DENY', $headers);
+ $this->assertContains('X-XSS-Protection: 1; mode=block', $headers);
+ $this->assertContains('X-Content-Type-Options: nosniff', $headers);
+ $this->assertContains('Referrer-Policy: strict-origin-when-cross-origin', $headers);
+ }
+
+ public function testContentSecurityPolicy()
+ {
+ // Apply security headers in test mode
+ $headers = \applySecurityHeaders(true);
+
+ // Get CSP header
+ $cspHeader = '';
+ foreach ($headers as $header) {
+ if (strpos($header, 'Content-Security-Policy:') === 0) {
+ $cspHeader = $header;
+ break;
+ }
+ }
+
+ // Check CSP directives
+ $this->assertStringContainsString("default-src 'self'", $cspHeader);
+ $this->assertStringContainsString("script-src 'self' 'unsafe-inline' 'unsafe-eval'", $cspHeader);
+ $this->assertStringContainsString("style-src 'self' 'unsafe-inline'", $cspHeader);
+ $this->assertStringContainsString("frame-ancestors 'none'", $cspHeader);
+ $this->assertStringContainsString("form-action 'self'", $cspHeader);
+ $this->assertStringContainsString("base-uri 'self'", $cspHeader);
+ }
+
+ public function testHstsHeader()
+ {
+ // Simulate HTTPS
+ $_SERVER['HTTPS'] = 'on';
+
+ // Apply security headers in test mode
+ $headers = \applySecurityHeaders(true);
+
+ // Check HSTS header
+ $this->assertContains(
+ 'Strict-Transport-Security: max-age=31536000; includeSubDomains; preload',
+ $headers
+ );
+ }
+
+ public function testNoHstsHeaderOnHttp()
+ {
+ // Apply security headers in test mode
+ $headers = \applySecurityHeaders(true);
+
+ // Check HSTS header is not present
+ $hasHsts = false;
+ foreach ($headers as $header) {
+ if (strpos($header, 'Strict-Transport-Security:') === 0) {
+ $hasHsts = true;
+ break;
+ }
+ }
+ $this->assertFalse($hasHsts, 'HSTS header should not be present on HTTP');
+ }
+
+ public function testCacheControlForSensitivePages()
+ {
+ $sensitivePages = ['login', 'register', 'profile', 'security'];
+
+ foreach ($sensitivePages as $page) {
+ // Set current page
+ $_GET['page'] = $page;
+
+ // Apply security headers in test mode
+ $headers = \applySecurityHeaders(true);
+
+ // Check cache control headers
+ $this->assertContains('Cache-Control: no-store, no-cache, must-revalidate, max-age=0', $headers);
+ $this->assertContains('Pragma: no-cache', $headers);
+ $this->assertStringContainsString('Expires:', implode(' ', $headers));
+ }
+ }
+
+ public function testNoCacheControlForNonSensitivePages()
+ {
+ $_GET['page'] = 'dashboard';
+
+ // Apply security headers in test mode
+ $headers = \applySecurityHeaders(true);
+
+ // Check cache control headers are not present
+ $this->assertNotContains('Cache-Control: no-store, no-cache, must-revalidate, max-age=0', $headers);
+ $this->assertNotContains('Pragma: no-cache', $headers);
+ }
+
+ public function testPermissionsPolicy()
+ {
+ // Apply security headers in test mode
+ $headers = \applySecurityHeaders(true);
+
+ // Get Permissions-Policy header
+ $permissionsHeader = '';
+ foreach ($headers as $header) {
+ if (strpos($header, 'Permissions-Policy:') === 0) {
+ $permissionsHeader = $header;
+ break;
+ }
+ }
+
+ // Check basic permissions
+ $this->assertStringContainsString('geolocation=()', $permissionsHeader);
+ $this->assertStringContainsString('payment=()', $permissionsHeader);
+ $this->assertStringContainsString('camera=()', $permissionsHeader);
+ $this->assertStringContainsString('microphone=()', $permissionsHeader);
+ $this->assertStringContainsString('fullscreen=(self)', $permissionsHeader);
+ $this->assertStringContainsString('sync-xhr=(self)', $permissionsHeader);
+ }
+
+ public function testPermissionsPolicyForMediaEnabledPages()
+ {
+ $_SERVER['REQUEST_URI'] = '/media/upload';
+
+ // Apply security headers in test mode
+ $headers = \applySecurityHeaders(true);
+
+ // Get Permissions-Policy header
+ $permissionsHeader = '';
+ foreach ($headers as $header) {
+ if (strpos($header, 'Permissions-Policy:') === 0) {
+ $permissionsHeader = $header;
+ break;
+ }
+ }
+
+ // Check permissions policy header
+ $this->assertStringContainsString('camera=()', $permissionsHeader);
+ $this->assertStringContainsString('microphone=()', $permissionsHeader);
+ $this->assertStringContainsString('geolocation=()', $permissionsHeader);
+ $this->assertStringContainsString('payment=()', $permissionsHeader);
+ }
+}
diff --git a/tests/framework/phpunit.xml b/tests/framework/phpunit.xml
index 706b87e..26cbf9d 100644
--- a/tests/framework/phpunit.xml
+++ b/tests/framework/phpunit.xml
@@ -8,6 +8,9 @@
./Unit
+
+ ./Integration
+