178 lines
7.3 KiB
TypeScript
178 lines
7.3 KiB
TypeScript
import { test, expect } from '@playwright/test';
|
|
import { RoleAuthHelper, PublicFormHelper } from '../../helpers/helpers';
|
|
|
|
test.describe('XSS & Input Validation', () => {
|
|
|
|
test('script tag in complaint form is escaped', async ({ page }) => {
|
|
const helper = new PublicFormHelper(page);
|
|
await helper.goToPublicLanding();
|
|
await helper.selectFormType('complaint');
|
|
|
|
const xssPayload = '<script>alert("xss")</script>';
|
|
await page.fill('#id_complainant_name', `XSS Test ${Date.now()}`);
|
|
await page.selectOption('#id_relation_to_patient', 'patient');
|
|
await page.fill('#id_mobile_number', '0501234567');
|
|
await page.fill('#id_patient_name', xssPayload);
|
|
await page.fill('#id_national_id', '1234567890');
|
|
await page.fill('#id_incident_date', '2025-01-15');
|
|
await page.fill('#id_complaint_details', `Test XSS payload: ${xssPayload}`);
|
|
await page.click('#submit_btn');
|
|
|
|
await page.waitForLoadState('domcontentloaded');
|
|
const bodyText = await page.textContent('body');
|
|
expect(bodyText).not.toContain('<script>alert("xss")</script>');
|
|
expect(bodyText).not.toContain('alert("xss")');
|
|
});
|
|
|
|
test('script tag in inquiry form is escaped', async ({ page }) => {
|
|
const helper = new PublicFormHelper(page);
|
|
await helper.goToPublicLanding();
|
|
await helper.selectFormType('inquiry');
|
|
|
|
const nameInput = page.locator('input[name="name"]');
|
|
await nameInput.waitFor({ state: 'visible', timeout: 15000 }).catch(() => {});
|
|
|
|
if (!(await nameInput.isVisible().catch(() => false))) {
|
|
test.skip();
|
|
return;
|
|
}
|
|
|
|
const xssPayload = '<img src=x onerror=alert(1)>';
|
|
await page.fill('input[name="name"]', `XSS Inquiry ${Date.now()}`);
|
|
await page.fill('input[name="email"]', 'xss@test.com');
|
|
await page.fill('input[name="phone"]', '0501234567');
|
|
|
|
const hospitalSelect = page.locator('select[name="hospital"]');
|
|
const opts = hospitalSelect.locator('option');
|
|
if (await opts.count() > 1) {
|
|
await hospitalSelect.selectOption({ index: 1 });
|
|
}
|
|
|
|
await page.selectOption('select[name="category"]', 'general');
|
|
await page.fill('input[name="subject"]', xssPayload);
|
|
await page.fill('textarea[name="message"]', `Body: ${xssPayload}`);
|
|
await page.click('#inqSubmitBtn');
|
|
|
|
await page.waitForLoadState('domcontentloaded');
|
|
const bodyText = await page.textContent('body');
|
|
expect(bodyText).not.toContain('onerror=alert(1)');
|
|
});
|
|
|
|
test('script tag in observation form is escaped', async ({ page }) => {
|
|
const helper = new PublicFormHelper(page);
|
|
await helper.goToPublicLanding();
|
|
await helper.selectFormType('observation');
|
|
|
|
const xssPayload = '<script>document.cookie</script>';
|
|
await page.fill('input[name="title"]', xssPayload);
|
|
await page.fill('textarea[name="description"]', `XSS test: ${xssPayload}`);
|
|
await page.fill('input[name="reporter_name"]', 'XSS Observer');
|
|
await page.fill('input[name="reporter_phone"]', '0501234567');
|
|
await page.click('button[type="submit"]');
|
|
|
|
await page.waitForLoadState('domcontentloaded');
|
|
const bodyText = await page.textContent('body');
|
|
expect(bodyText).not.toContain('<script>document.cookie</script>');
|
|
});
|
|
|
|
test('SQL injection in complaint list search does not crash', async ({ page }) => {
|
|
const auth = new RoleAuthHelper(page);
|
|
await auth.login('hospital_admin');
|
|
|
|
await page.goto('/complaints/');
|
|
await page.waitForLoadState('domcontentloaded');
|
|
|
|
const sqlPayload = "'; DROP TABLE complaints; --";
|
|
const searchInput = page.locator('input[name="search"], #search, input[type="search"]');
|
|
await searchInput.waitFor({ state: 'visible', timeout: 20000 }).catch(() => {});
|
|
if (!(await searchInput.isVisible().catch(() => false))) {
|
|
test.skip();
|
|
return;
|
|
}
|
|
await page.fill('input[name="search"]', sqlPayload);
|
|
await page.waitForTimeout(500);
|
|
|
|
const bodyText = await page.textContent('body');
|
|
expect(bodyText).not.toContain('Server Error');
|
|
expect(bodyText).not.toContain('Traceback');
|
|
expect(bodyText).not.toContain('DROP TABLE');
|
|
});
|
|
|
|
test('oversized text input does not crash complaint form', async ({ page }) => {
|
|
const helper = new PublicFormHelper(page);
|
|
await helper.goToPublicLanding();
|
|
await helper.selectFormType('complaint');
|
|
|
|
const longText = 'A'.repeat(10000);
|
|
await page.fill('#id_complainant_name', `Long Text ${Date.now()}`);
|
|
await page.selectOption('#id_relation_to_patient', 'patient');
|
|
await page.fill('#id_mobile_number', '0501234567');
|
|
await page.fill('#id_patient_name', 'Long Patient');
|
|
await page.fill('#id_national_id', '1234567890');
|
|
await page.fill('#id_incident_date', '2025-01-15');
|
|
await page.fill('#id_complaint_details', longText);
|
|
await page.click('#submit_btn');
|
|
|
|
await page.waitForLoadState('domcontentloaded');
|
|
const bodyText = await page.textContent('body');
|
|
expect(bodyText).not.toContain('Server Error');
|
|
expect(bodyText).not.toContain('Traceback');
|
|
});
|
|
|
|
test('Arabic text and emoji in complaint form works', async ({ page }) => {
|
|
const helper = new PublicFormHelper(page);
|
|
await helper.goToPublicLanding();
|
|
await helper.selectFormType('complaint');
|
|
|
|
await page.fill('#id_complainant_name', `مريض تجريبي ${Date.now()}`);
|
|
await page.selectOption('#id_relation_to_patient', 'patient');
|
|
await page.fill('#id_mobile_number', '0501234567');
|
|
await page.fill('#id_patient_name', 'اسم المريض 😊');
|
|
await page.fill('#id_national_id', '1234567890');
|
|
await page.fill('#id_incident_date', '2025-01-15');
|
|
await page.fill('#id_complaint_details', 'شكوى تجريبية من المريض 🏥');
|
|
await page.click('#submit_btn');
|
|
|
|
await page.waitForLoadState('domcontentloaded');
|
|
const bodyText = await page.textContent('body');
|
|
expect(bodyText).not.toContain('Server Error');
|
|
expect(bodyText).not.toContain('Traceback');
|
|
});
|
|
|
|
test('email injection in patient field is handled safely', async ({ page }) => {
|
|
const helper = new PublicFormHelper(page);
|
|
await helper.goToPublicLanding();
|
|
await helper.selectFormType('complaint');
|
|
|
|
const emailInjection = 'test@test.com\nBCC:evil@evil.com\nSubject:phishing';
|
|
await page.fill('#id_complainant_name', `Email Inject ${Date.now()}`);
|
|
await page.selectOption('#id_relation_to_patient', 'patient');
|
|
await page.fill('#id_email', emailInjection);
|
|
await page.fill('#id_mobile_number', '0501234567');
|
|
await page.fill('#id_patient_name', 'Email Inject Patient');
|
|
await page.fill('#id_national_id', '1234567890');
|
|
await page.fill('#id_incident_date', '2025-01-15');
|
|
await page.fill('#id_complaint_details', 'Email injection test');
|
|
await page.click('#submit_btn');
|
|
|
|
await page.waitForLoadState('domcontentloaded');
|
|
const bodyText = await page.textContent('body');
|
|
expect(bodyText).not.toContain('Server Error');
|
|
expect(bodyText).not.toContain('Traceback');
|
|
});
|
|
|
|
test('CSRF token required for POST to complaint create', async ({ request }) => {
|
|
const resp = await request.fetch('http://localhost:8000/api/v1/complaints/', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
data: {
|
|
title: 'CSRF test',
|
|
description: 'testing',
|
|
},
|
|
});
|
|
expect([200, 201, 403, 401, 404, 405]).toContain(resp.status());
|
|
});
|
|
});
|