/** * HVAC Community Events - AJAX Security Comprehensive Test Suite * * Tests for AJAX endpoint security including: * - Nonce verification on all AJAX endpoints * - Rate limiting implementation * - Input sanitization and validation * - Authorization checks and access control * - CSRF protection mechanisms * - Error handling and information disclosure * * AJAX SECURITY AREAS TESTED: * 1. Nonce verification and CSRF protection * 2. Rate limiting and brute force protection * 3. Input sanitization and SQL injection prevention * 4. Authorization and access control * 5. Error handling and information disclosure * 6. Session management and authentication * * @package HVAC_Community_Events * @since 2.0.0 */ const { test, expect } = require('@playwright/test'); const crypto = require('crypto'); // AJAX Security test configuration const AJAX_SECURITY_CONFIG = { BASE_URL: process.env.BASE_URL || 'http://localhost:8080', AJAX_ENDPOINTS: { // WordPress core AJAX endpoints ADMIN_AJAX: '/wp-admin/admin-ajax.php', REST_API: '/wp-json/', // Plugin-specific AJAX endpoints (discovered dynamically) PLUGIN_ENDPOINTS: [ '/wp-json/hvac/v1/', '/wp-admin/admin-ajax.php?action=hvac_', ] }, // Test payloads for various attack vectors ATTACK_PAYLOADS: { // SQL Injection payloads SQL_INJECTION: [ "' OR 1=1 --", "'; DROP TABLE wp_users; --", "' UNION SELECT * FROM wp_options --", "%27%20OR%201=1%20--", "1' UNION SELECT user_pass FROM wp_users WHERE user_login='admin'--" ], // XSS payloads XSS_INJECTION: [ "", "javascript:alert('XSS')", "", "", "');alert('XSS');//" ], // Command injection payloads COMMAND_INJECTION: [ "; cat /etc/passwd", "| cat /etc/passwd", "&& cat /etc/passwd", "`cat /etc/passwd`", "$(cat /etc/passwd)" ], // Path traversal payloads PATH_TRAVERSAL: [ "../../../etc/passwd", "..\\..\\..\\windows\\system32\\drivers\\etc\\hosts", "%2e%2e%2f%2e%2e%2f%2e%2e%2fwp-config.php", "....//....//....//etc/passwd" ], // CSRF payloads CSRF_ATTACKS: [ { nonce: '', description: 'empty nonce' }, { nonce: 'invalid_nonce_12345', description: 'invalid nonce' }, { nonce: 'a'.repeat(100), description: 'oversized nonce' }, { nonce: '', description: 'xss in nonce' } ] }, // Rate limiting configuration RATE_LIMIT_CONFIG: { MAX_REQUESTS: 10, TIME_WINDOW: 60, // seconds RAPID_FIRE_COUNT: 20, RAPID_FIRE_INTERVAL: 100 // milliseconds }, // Authentication test data TEST_USERS: { VALID: { username: 'test_trainer', password: 'test_password_123!' }, INVALID: { username: 'invalid_user', password: 'wrong_password' } } }; /** * AJAX Security Testing Framework */ class AJAXSecurityTestFramework { constructor(page) { this.page = page; this.securityEvents = []; this.requestLogs = []; this.rateLimitTests = []; this.discoveredEndpoints = []; } /** * Enable AJAX security monitoring */ async enableSecurityMonitoring() { // Monitor all AJAX requests this.page.on('request', (request) => { const isAjax = request.url().includes('admin-ajax.php') || request.url().includes('/wp-json/') || request.method() === 'POST' || request.headers()['x-requested-with'] === 'XMLHttpRequest'; if (isAjax) { this.requestLogs.push({ url: request.url(), method: request.method(), headers: request.headers(), timestamp: new Date().toISOString(), postData: request.postData() }); } }); // Monitor responses for security issues this.page.on('response', async (response) => { if (response.request().url().includes('admin-ajax.php') || response.request().url().includes('/wp-json/')) { const responseText = await response.text().catch(() => ''); // Check for information disclosure if (this.containsInformationDisclosure(responseText)) { this.securityEvents.push({ type: 'information_disclosure', url: response.url(), status: response.status(), disclosure: this.extractDisclosedInfo(responseText), timestamp: new Date().toISOString() }); } // Check for error messages that reveal system info if (this.containsSystemInfo(responseText)) { this.securityEvents.push({ type: 'system_info_disclosure', url: response.url(), info: this.extractSystemInfo(responseText), timestamp: new Date().toISOString() }); } } }); // Monitor console errors that might indicate security issues this.page.on('console', (message) => { if (message.type() === 'error' && this.isSecurityRelevantError(message.text())) { this.securityEvents.push({ type: 'console_error', message: message.text(), timestamp: new Date().toISOString() }); } }); } /** * Discover AJAX endpoints by analyzing page JavaScript */ async discoverAjaxEndpoints() { console.log('๐Ÿ” Discovering AJAX endpoints...'); // Navigate to plugin pages to discover endpoints const pluginPages = [ '/trainer/dashboard/', '/master-trainer/master-dashboard/', '/trainer/profile/', '/community-login/' ]; for (const pagePath of pluginPages) { try { const response = await this.page.goto(`${AJAX_SECURITY_CONFIG.BASE_URL}${pagePath}`, { timeout: 10000, waitUntil: 'domcontentloaded' }); if (response && response.status() === 200) { // Extract AJAX endpoints from page scripts const endpoints = await this.page.evaluate(() => { const scripts = Array.from(document.querySelectorAll('script')); const endpoints = new Set(); scripts.forEach(script => { const content = script.textContent || script.innerHTML || ''; // Look for AJAX URLs const ajaxMatches = content.match(/ajax_url['"]*\s*[:=]\s*['"]([^'"]+)['"]/g); if (ajaxMatches) { ajaxMatches.forEach(match => { const url = match.match(/['"]([^'"]+)['"]/); if (url && url[1]) endpoints.add(url[1]); }); } // Look for REST API URLs const restMatches = content.match(/wp-json\/[^'">\s]+/g); if (restMatches) { restMatches.forEach(match => endpoints.add('/' + match)); } // Look for action parameters const actionMatches = content.match(/action['"]*\s*[:=]\s*['"]([^'"]+)['"]/g); if (actionMatches) { actionMatches.forEach(match => { const action = match.match(/['"]([^'"]+)['"]/); if (action && action[1]) { endpoints.add(`/wp-admin/admin-ajax.php?action=${action[1]}`); } }); } }); return Array.from(endpoints); }); this.discoveredEndpoints = [...this.discoveredEndpoints, ...endpoints]; } } catch (error) { console.log(`โš ๏ธ Could not scan ${pagePath}: ${error.message}`); } } // Remove duplicates this.discoveredEndpoints = [...new Set(this.discoveredEndpoints)]; console.log(`๐Ÿ“Š Discovered ${this.discoveredEndpoints.length} AJAX endpoints`); this.discoveredEndpoints.forEach(endpoint => console.log(` โ€ข ${endpoint}`)); } /** * Test AJAX endpoint for nonce validation */ async testNonceValidation(endpoint, action = 'test_action') { console.log(`๐Ÿ”’ Testing nonce validation on: ${endpoint}`); const testResults = []; for (const csrfAttack of AJAX_SECURITY_CONFIG.ATTACK_PAYLOADS.CSRF_ATTACKS) { try { const response = await this.page.request.post(endpoint, { data: { action: action, _wpnonce: csrfAttack.nonce, test_data: 'security_test' }, headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'X-Requested-With': 'XMLHttpRequest' } }); const responseText = await response.text(); const isBlocked = response.status() === 403 || response.status() === 401 || responseText.includes('nonce') || responseText.includes('security') || responseText.includes('permission'); testResults.push({ attack: csrfAttack.description, nonce: csrfAttack.nonce, status: response.status(), blocked: isBlocked, response: responseText.substring(0, 200) }); } catch (error) { testResults.push({ attack: csrfAttack.description, error: error.message, blocked: true }); } } return testResults; } /** * Test rate limiting on AJAX endpoint */ async testRateLimiting(endpoint, action = 'test_action') { console.log(`โฑ๏ธ Testing rate limiting on: ${endpoint}`); const startTime = Date.now(); const requests = []; let rateLimitTriggered = false; let firstBlockedRequest = null; // Rapid fire requests for (let i = 0; i < AJAX_SECURITY_CONFIG.RATE_LIMIT_CONFIG.RAPID_FIRE_COUNT; i++) { try { const requestStart = Date.now(); const response = await this.page.request.post(endpoint, { data: { action: action, test_iteration: i, test_data: 'rate_limit_test' }, headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'X-Requested-With': 'XMLHttpRequest' }, timeout: 10000 }); const requestTime = Date.now() - requestStart; requests.push({ iteration: i, status: response.status(), responseTime: requestTime, timestamp: new Date().toISOString() }); // Check if request was rate limited if (response.status() === 429 || requestTime > 5000) { if (!rateLimitTriggered) { rateLimitTriggered = true; firstBlockedRequest = i; console.log(`๐Ÿ›ก๏ธ Rate limiting triggered at request ${i}`); } } } catch (error) { requests.push({ iteration: i, error: error.message, blocked: true, timestamp: new Date().toISOString() }); if (!rateLimitTriggered && error.message.includes('timeout')) { rateLimitTriggered = true; firstBlockedRequest = i; } } // Small delay between requests await this.page.waitForTimeout(AJAX_SECURITY_CONFIG.RATE_LIMIT_CONFIG.RAPID_FIRE_INTERVAL); } const totalTime = Date.now() - startTime; return { totalRequests: requests.length, totalTime, rateLimitTriggered, firstBlockedRequest, averageResponseTime: requests .filter(r => r.responseTime) .reduce((sum, r) => sum + r.responseTime, 0) / requests.length, requests: requests.slice(0, 5) // Only return first 5 for brevity }; } /** * Test input sanitization with various attack payloads */ async testInputSanitization(endpoint, action = 'test_action') { console.log(`๐Ÿงผ Testing input sanitization on: ${endpoint}`); const testResults = []; const allPayloads = [ ...AJAX_SECURITY_CONFIG.ATTACK_PAYLOADS.SQL_INJECTION.map(p => ({ type: 'sql_injection', payload: p })), ...AJAX_SECURITY_CONFIG.ATTACK_PAYLOADS.XSS_INJECTION.map(p => ({ type: 'xss_injection', payload: p })), ...AJAX_SECURITY_CONFIG.ATTACK_PAYLOADS.COMMAND_INJECTION.map(p => ({ type: 'command_injection', payload: p })), ...AJAX_SECURITY_CONFIG.ATTACK_PAYLOADS.PATH_TRAVERSAL.map(p => ({ type: 'path_traversal', payload: p })) ]; for (const attackTest of allPayloads) { try { const response = await this.page.request.post(endpoint, { data: { action: action, test_input: attackTest.payload, user_data: attackTest.payload, search_query: attackTest.payload }, headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'X-Requested-With': 'XMLHttpRequest' } }); const responseText = await response.text(); // Check if attack was successful (BAD) const attackSuccessful = this.checkForAttackSuccess(attackTest.type, attackTest.payload, responseText); // Check if input was properly sanitized (GOOD) const inputSanitized = !responseText.includes(attackTest.payload) || response.status() === 400 || response.status() === 403; testResults.push({ attackType: attackTest.type, payload: attackTest.payload.substring(0, 50), status: response.status(), attackSuccessful, inputSanitized, responseLength: responseText.length, containsPayload: responseText.includes(attackTest.payload) }); } catch (error) { testResults.push({ attackType: attackTest.type, payload: attackTest.payload.substring(0, 50), error: error.message, blocked: true }); } } return testResults; } /** * Check if an attack was successful based on response */ checkForAttackSuccess(attackType, payload, response) { switch (attackType) { case 'sql_injection': return response.includes('mysql') || response.includes('sql error') || response.includes('database error') || response.includes('wp_users') || response.includes('user_pass'); case 'xss_injection': return response.includes('