/** * Security Framework Test Suite * * Tests the new security framework implementation: * - Role-based access control (trainer, master_trainer, admin) * - CSRF protection via nonce verification * - Input sanitization validation * - Authentication boundary testing * - Permission escalation prevention * - Session security * * @package HVAC_Community_Events * @version 3.0.0 * @created 2025-08-20 */ const { test, expect, authHelpers, authScenarios } = require('../helpers/auth-fixtures'); const path = require('path'); // Test configuration const BASE_URL = process.env.UPSKILL_STAGING_URL || 'https://upskill-staging.measurequick.com'; const TEST_TIMEOUT = 90000; // Test users with different privilege levels (now handled by auth system) const USER_ACCESS_MATRIX = { trainer: { role: 'hvac_trainer', expectedPages: ['/trainer/dashboard/', '/trainer/profile/', '/trainer/events/'] }, master_trainer: { role: 'hvac_master_trainer', expectedPages: ['/master-trainer/master-dashboard/', '/trainer/dashboard/', '/trainer/events/'] }, admin: { role: 'administrator', expectedPages: ['/wp-admin/', '/trainer/dashboard/', '/master-trainer/master-dashboard/'] } }; // Security test payloads const SECURITY_PAYLOADS = { xss: [ '', '">', 'javascript:alert("xss")', '', '\">' ], sqlInjection: [ "'; DROP TABLE wp_users; --", "' OR '1'='1", "1' UNION SELECT * FROM wp_users --", "'; INSERT INTO wp_users VALUES('test'); --" ], pathTraversal: [ '../../../etc/passwd', '..\\..\\..\\windows\\system32\\drivers\\etc\\hosts', '....//....//....//etc/passwd', '/var/www/wp-config.php' ], commandInjection: [ '; cat /etc/passwd', '| whoami', '&& ls -la', '`rm -rf /`' ] }; // Helper functions - updated to use new auth system async function loginAsUser(page, userType = 'trainer') { await authHelpers.loginAs(page, userType); } async function attemptUnauthorizedAccess(page, url, expectedResult = 'denied') { const response = await page.goto(url); await page.waitForTimeout(2000); const statusCode = response?.status() || 0; const content = await page.content(); const currentUrl = page.url(); return { statusCode, content, currentUrl, isAccessDenied: content.includes('Access denied') || content.includes('Insufficient permissions') || content.includes('403 Forbidden') || statusCode === 403 || currentUrl.includes('wp-login.php'), isRedirected: currentUrl !== url }; } async function testFormWithPayloads(page, formSelector, fieldMappings, payloads) { const results = []; for (const payload of payloads) { try { // Fill form with malicious payload for (const [fieldSelector, payloadValue] of Object.entries(fieldMappings)) { const field = await page.$(fieldSelector); if (field) { await field.fill(typeof payloadValue === 'function' ? payloadValue(payload) : payload); } } // Submit form const submitButton = await page.$(`${formSelector} button[type="submit"], ${formSelector} input[type="submit"]`); if (submitButton) { await submitButton.click(); await page.waitForTimeout(2000); // Check response const content = await page.content(); const hasPayload = content.includes(payload); const hasError = content.includes('error') || content.includes('Error'); results.push({ payload, reflected: hasPayload, hasError, sanitized: !hasPayload || hasError }); } } catch (error) { results.push({ payload, reflected: false, hasError: true, sanitized: true, error: error.message }); } } return results; } async function extractNonceFields(page) { return await page.evaluate(() => { const nonceFields = Array.from(document.querySelectorAll('input[name*="nonce"], input[name*="_token"], input[name*="_wpnonce"]')); return nonceFields.map(field => ({ name: field.name, value: field.value, form: field.closest('form')?.id || 'unknown' })); }); } async function takeSecurityScreenshot(page, name) { const screenshotDir = path.join(__dirname, '../../screenshots/security'); await require('fs').promises.mkdir(screenshotDir, { recursive: true }); await page.screenshot({ path: path.join(screenshotDir, `${name}-${Date.now()}.png`), fullPage: true }); } test.describe('Security Framework Tests', () => { test.setTimeout(TEST_TIMEOUT); test.beforeEach(async ({ page }) => { await page.setViewportSize({ width: 1280, height: 720 }); }); test.describe('Role-Based Access Control Tests', () => { test('should enforce trainer role access restrictions', async ({ page }) => { await loginAsUser(page, 'trainer'); // Test allowed pages const allowedPages = USER_ACCESS_MATRIX.trainer.expectedPages; for (const allowedPage of allowedPages) { const response = await page.goto(`${BASE_URL}${allowedPage}`); expect(response?.status()).toBeLessThan(400); console.log(`Trainer access to ${allowedPage}: OK`); } // Test restricted pages const restrictedPages = [ '/wp-admin/', '/master-trainer/master-dashboard/', '/wp-admin/users.php', '/wp-admin/plugins.php' ]; for (const restrictedPage of restrictedPages) { const result = await attemptUnauthorizedAccess(page, `${BASE_URL}${restrictedPage}`); // Should be denied or redirected const isProperlyRestricted = result.isAccessDenied || result.statusCode >= 400 || result.isRedirected; expect(isProperlyRestricted).toBeTruthy(); console.log(`Trainer restriction for ${restrictedPage}:`, isProperlyRestricted ? 'BLOCKED' : 'ALLOWED'); } await takeSecurityScreenshot(page, 'trainer-access-control'); }); test('should enforce master trainer role privileges', async ({ page }) => { try { await loginAsUser(page, 'master_trainer'); // Master trainer should access both trainer and master pages const allowedPages = [ '/trainer/dashboard/', '/master-trainer/master-dashboard/', '/trainer/events/', '/trainer/profile/' ]; for (const allowedPage of allowedPages) { const response = await page.goto(`${BASE_URL}${allowedPage}`); expect(response?.status()).toBeLessThan(400); console.log(`Master trainer access to ${allowedPage}: OK`); } // Still should not access admin pages const restrictedPages = [ '/wp-admin/users.php', '/wp-admin/plugins.php', '/wp-admin/themes.php' ]; for (const restrictedPage of restrictedPages) { const result = await attemptUnauthorizedAccess(page, `${BASE_URL}${restrictedPage}`); const isProperlyRestricted = result.isAccessDenied || result.statusCode >= 400 || result.isRedirected; // Note: Master trainers might have some admin access console.log(`Master trainer restriction for ${restrictedPage}:`, isProperlyRestricted ? 'BLOCKED' : 'ALLOWED'); } } catch (error) { console.log('Master trainer test skipped - user may not exist:', error.message); } }); test('should prevent privilege escalation attempts', async ({ page }) => { await loginAsUser(page, 'trainer'); // Attempt to modify user role via form manipulation await page.goto(`${BASE_URL}/trainer/profile/edit/`); // Try to inject role escalation parameters const escalationAttempts = [ { field: 'role', value: 'administrator' }, { field: 'wp_capabilities', value: 'administrator' }, { field: 'user_role', value: 'hvac_master_trainer' } ]; for (const attempt of escalationAttempts) { try { // Inject hidden field via JavaScript await page.evaluate((field, value) => { const form = document.querySelector('form'); if (form) { const input = document.createElement('input'); input.type = 'hidden'; input.name = field; input.value = value; form.appendChild(input); } }, attempt.field, attempt.value); console.log(`Injected privilege escalation attempt: ${attempt.field} = ${attempt.value}`); } catch (error) { console.log(`Failed to inject ${attempt.field}:`, error.message); } } // Submit form and check if escalation was prevented const submitButton = await page.$('button[type="submit"], input[type="submit"]'); if (submitButton) { await submitButton.click(); await page.waitForTimeout(3000); // User should still be trainer role await page.goto(`${BASE_URL}/master-trainer/master-dashboard/`); const result = await attemptUnauthorizedAccess(page, `${BASE_URL}/master-trainer/master-dashboard/`); expect(result.isAccessDenied || result.isRedirected).toBeTruthy(); } }); test('should validate session security', async ({ page }) => { await loginAsUser(page, 'trainer'); // Check for secure session cookies const cookies = await page.context().cookies(); const sessionCookie = cookies.find(c => c.name.includes('session') || c.name.includes('wordpress')); if (sessionCookie) { expect(sessionCookie.httpOnly).toBeTruthy(); // Note: Secure flag may not be set in development console.log('Session cookie security:', { httpOnly: sessionCookie.httpOnly, secure: sessionCookie.secure }); } // Test session timeout await page.goto(`${BASE_URL}/trainer/dashboard/`); // Simulate session manipulation await page.evaluate(() => { // Try to access session storage try { sessionStorage.setItem('test_key', 'test_value'); localStorage.setItem('test_key', 'test_value'); } catch (e) { console.log('Storage access restricted:', e.message); } }); }); }); test.describe('CSRF Protection Tests', () => { test('should include nonce fields in forms', async ({ page }) => { await loginAsUser(page, 'trainer'); const formsToCheck = [ '/trainer/profile/edit/', '/trainer/events/create/', '/trainer/organizer/manage/' ]; for (const formPage of formsToCheck) { await page.goto(`${BASE_URL}${formPage}`); await page.waitForTimeout(2000); const nonceFields = await extractNonceFields(page); expect(nonceFields.length).toBeGreaterThan(0); console.log(`Nonce fields found on ${formPage}:`, nonceFields.length); // Verify nonce values are not empty nonceFields.forEach(field => { expect(field.value).toBeTruthy(); expect(field.value.length).toBeGreaterThan(5); }); } }); test('should reject forms without valid nonces', async ({ page }) => { await loginAsUser(page, 'trainer'); await page.goto(`${BASE_URL}/trainer/profile/edit/`); // Remove or modify nonce fields await page.evaluate(() => { const nonceFields = document.querySelectorAll('input[name*="nonce"]'); nonceFields.forEach(field => { field.value = 'invalid_nonce_value'; }); }); // Try to submit form const submitButton = await page.$('button[type="submit"], input[type="submit"]'); if (submitButton) { await submitButton.click(); await page.waitForTimeout(3000); // Should show error or reject submission const content = await page.content(); const hasError = content.includes('error') || content.includes('Error') || content.includes('nonce') || content.includes('Security check failed'); console.log('Invalid nonce rejection:', hasError ? 'BLOCKED' : 'ALLOWED'); // Note: Some forms might not validate nonces in development } }); test('should prevent CSRF attacks via external submission', async ({ page, context }) => { await loginAsUser(page, 'trainer'); // Get a legitimate form's nonce await page.goto(`${BASE_URL}/trainer/profile/edit/`); const legitimateNonce = await page.evaluate(() => { const nonceField = document.querySelector('input[name*="nonce"]'); return nonceField ? nonceField.value : null; }); // Create new context (simulating external site) const externalContext = await page.context().browser().newContext(); const externalPage = await externalContext.newPage(); // Try to submit form from external context using stolen nonce const formData = new URLSearchParams({ 'user_email': 'hacker@evil.com', 'nonce': legitimateNonce || 'fake_nonce' }); try { const response = await externalPage.request.post(`${BASE_URL}/trainer/profile/edit/`, { data: formData.toString(), headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }); // Should be rejected (403/405 or redirect) const isBlocked = response.status() >= 400 || response.headers()['location']?.includes('login'); expect(isBlocked).toBeTruthy(); console.log('External CSRF attempt blocked:', isBlocked); } catch (error) { console.log('CSRF attempt failed (good):', error.message); } await externalContext.close(); }); }); test.describe('Input Sanitization Tests', () => { test('should sanitize XSS payloads in event creation', async ({ page }) => { await loginAsUser(page, 'trainer'); await page.goto(`${BASE_URL}/trainer/events/create/`); // Wait for form to load await page.waitForSelector('.hvac-event-form-wrapper, iframe#tec-create-frame', { timeout: 10000 }); const iframe = await page.$('iframe#tec-create-frame'); const context = iframe ? await iframe.contentFrame() : page; const fieldMappings = { 'input[name="post_title"], #tribe-events-title': (payload) => `XSS Test ${payload}`, '#content, textarea[name="post_content"]': (payload) => `Description ${payload}`, 'input[name="venue[Venue]"], #venue-name': (payload) => `Venue ${payload}` }; const results = await testFormWithPayloads(context, 'form', fieldMappings, SECURITY_PAYLOADS.xss); // All XSS payloads should be sanitized results.forEach(result => { expect(result.sanitized).toBeTruthy(); console.log(`XSS payload "${result.payload}" sanitized:`, result.sanitized); }); await takeSecurityScreenshot(page, 'xss-sanitization-test'); }); test('should handle SQL injection attempts safely', async ({ page }) => { await loginAsUser(page, 'trainer'); await page.goto(`${BASE_URL}/trainer/profile/edit/`); const sqlPayloads = SECURITY_PAYLOADS.sqlInjection; for (const payload of sqlPayloads) { // Try SQL injection in various fields const emailField = await page.$('input[type="email"], input[name="email"]'); if (emailField) { await emailField.fill(`test${payload}@example.com`); } const nameField = await page.$('input[name="first_name"], input[name="display_name"]'); if (nameField) { await nameField.fill(`Test${payload}`); } // Submit and check for errors const submitButton = await page.$('button[type="submit"], input[type="submit"]'); if (submitButton) { await submitButton.click(); await page.waitForTimeout(2000); const content = await page.content(); const hasSqlError = content.includes('SQL') || content.includes('mysql') || content.includes('database error'); // Should not expose SQL errors expect(hasSqlError).toBeFalsy(); console.log(`SQL injection payload "${payload}" handled safely`); } // Reset form for next test await page.reload(); await page.waitForTimeout(1000); } }); test('should prevent path traversal attacks', async ({ page }) => { await loginAsUser(page, 'trainer'); const pathPayloads = SECURITY_PAYLOADS.pathTraversal; for (const payload of pathPayloads) { // Test path traversal in file upload fields await page.goto(`${BASE_URL}/trainer/profile/edit/`); const fileInput = await page.$('input[type="file"]'); if (fileInput) { try { // Create temporary file with malicious name const fs = require('fs'); const tmpPath = path.join(__dirname, 'temp_test_file.txt'); fs.writeFileSync(tmpPath, 'test content'); await fileInput.setInputFiles(tmpPath); const submitButton = await page.$('button[type="submit"], input[type="submit"]'); if (submitButton) { await submitButton.click(); await page.waitForTimeout(3000); // Check if file was processed safely const content = await page.content(); const hasTraversalContent = content.includes('/etc/passwd') || content.includes('windows/system32'); expect(hasTraversalContent).toBeFalsy(); console.log(`Path traversal payload "${payload}" blocked`); } // Cleanup if (fs.existsSync(tmpPath)) { fs.unlinkSync(tmpPath); } } catch (error) { console.log(`Path traversal test for "${payload}" failed safely:`, error.message); } } } }); test('should escape output in templates', async ({ page }) => { await loginAsUser(page, 'trainer'); // Test output escaping in various templates const pagesToCheck = [ '/trainer/dashboard/', '/trainer/profile/', '/trainer/events/' ]; for (const pagePath of pagesToCheck) { await page.goto(`${BASE_URL}${pagePath}`); // Check for unescaped content patterns const unsafePatterns = await page.evaluate(() => { const content = document.body.innerHTML; const patterns = [ /]*>[^<]*<\/script>/gi, /javascript:[^"'>\s]*/gi, /on\w+\s*=\s*[^>]*/gi ]; return patterns.map(pattern => ({ pattern: pattern.toString(), matches: content.match(pattern) || [] })); }); unsafePatterns.forEach(result => { if (result.matches.length > 0) { console.log(`Potentially unsafe content on ${pagePath}:`, result.matches); } }); } }); }); test.describe('Authentication Security Tests', () => { test('should enforce strong password requirements', async ({ page }) => { await page.goto(`${BASE_URL}/wp-login.php?action=register`); // If registration is available, test password requirements const registerForm = await page.$('#registerform, .register-form'); if (registerForm) { const weakPasswords = [ '123456', 'password', 'abc123', '1234', 'test' ]; for (const weakPassword of weakPasswords) { await page.fill('#user_pass, input[name="pass1"]', weakPassword); // Check for password strength indicator await page.waitForTimeout(500); const strengthIndicator = await page.evaluate(() => { const indicator = document.querySelector('.password-strength, #pass-strength-result'); return indicator ? indicator.textContent : ''; }); console.log(`Password "${weakPassword}" strength:`, strengthIndicator); } } else { console.log('Registration form not available for testing'); } }); test('should implement account lockout after failed attempts', async ({ page }) => { // Test with intentionally wrong credentials const fakeUser = { email: 'nonexistent@example.com', password: 'WrongPassword123!' }; for (let attempt = 1; attempt <= 5; attempt++) { await page.goto(`${BASE_URL}/wp-login.php`); await page.fill('#user_login', fakeUser.email); await page.fill('#user_pass', fakeUser.password); await page.click('#wp-submit'); await page.waitForTimeout(1000); const errorMessage = await page.evaluate(() => { const error = document.querySelector('#login_error, .login-error'); return error ? error.textContent : ''; }); console.log(`Failed login attempt ${attempt}:`, errorMessage); // Check if account gets locked after multiple attempts if (attempt >= 3 && errorMessage.includes('locked')) { console.log('Account lockout mechanism detected'); break; } } }); test('should secure password reset process', async ({ page }) => { await page.goto(`${BASE_URL}/wp-login.php?action=lostpassword`); const resetForm = await page.$('#lostpasswordform'); if (resetForm) { // Test with valid email await page.fill('#user_login', 'test_trainer@example.com'); await page.click('#wp-submit'); await page.waitForTimeout(2000); const message = await page.evaluate(() => { return document.body.textContent; }); // Should not reveal whether email exists const isSecure = !message.includes('does not exist') && !message.includes('not found') && message.includes('sent') || message.includes('check'); expect(isSecure).toBeTruthy(); console.log('Password reset process secure:', isSecure); } }); test('should validate session hijacking protection', async ({ browser }) => { // Create two contexts to simulate session hijacking const context1 = await browser.newContext(); const context2 = await browser.newContext(); const page1 = await context1.newPage(); const page2 = await context2.newPage(); // Login in first context await loginAsUser(page1, 'trainer'); // Get session cookies from first context const cookies = await context1.cookies(); const sessionCookies = cookies.filter(c => c.name.includes('wordpress') || c.name.includes('session') ); // Try to use session cookies in second context if (sessionCookies.length > 0) { await context2.addCookies(sessionCookies); // Try to access protected page in second context await page2.goto(`${BASE_URL}/trainer/dashboard/`); const isRedirected = page2.url().includes('wp-login.php'); console.log('Session hijacking protection:', isRedirected ? 'ACTIVE' : 'WEAK'); } await context1.close(); await context2.close(); }); }); }); // Export security test configuration module.exports = { testDir: __dirname, timeout: TEST_TIMEOUT, retries: 1, workers: 1, // Sequential for security tests use: { baseURL: BASE_URL, screenshot: 'only-on-failure', video: 'retain-on-failure', trace: 'on-first-retry', ignoreHTTPSErrors: true // For testing purposes } };