27c9a81c0a
Bump workspace and backend module to Go 1.26 to satisfy module toolchain requirements and allow dependency tooling (Renovate) to run. Regenerated backend module checksums.
292 lines
11 KiB
TypeScript
292 lines
11 KiB
TypeScript
import { test, expect } from '@playwright/test';
|
|
|
|
/**
|
|
* Phase 4 Integration: WAF & Rate Limit Interaction
|
|
*
|
|
* Purpose: Validate WAF and rate limiting work independently and together
|
|
* Scenarios: Module enforcement, request handling, interaction
|
|
* Success: Malicious requests blocked, rate limited requests blocked appropriately
|
|
*/
|
|
|
|
test.describe('INT-002: WAF & Rate Limit Interaction', () => {
|
|
const testProxy = {
|
|
domain: 'waf-test.local',
|
|
target: 'http://localhost:3001',
|
|
description: 'Test proxy for WAF and rate limit',
|
|
};
|
|
|
|
test.beforeEach(async ({ page }) => {
|
|
await page.goto('/', { waitUntil: 'networkidle' });
|
|
await page.waitForSelector('[role="main"]', { timeout: 5000 });
|
|
});
|
|
|
|
test.afterEach(async ({ page }) => {
|
|
// Cleanup: Delete test proxy
|
|
try {
|
|
await page.goto('/proxy-hosts', { waitUntil: 'networkidle' });
|
|
const proxyRow = page.locator(`text=${testProxy.domain}`).first();
|
|
if (await proxyRow.isVisible()) {
|
|
const deleteButton = proxyRow.locator('..').getByRole('button', { name: /delete/i }).first();
|
|
await deleteButton.click();
|
|
|
|
const confirmButton = page.getByRole('button', { name: /confirm|delete/i }).first();
|
|
if (await confirmButton.isVisible()) {
|
|
await confirmButton.click();
|
|
}
|
|
await page.waitForLoadState('networkidle');
|
|
}
|
|
} catch {
|
|
// Ignore cleanup errors
|
|
}
|
|
});
|
|
|
|
// INT-002-1: WAF blocks malicious request (403)
|
|
test('WAF blocks malicious SQL injection payload', async ({ page }) => {
|
|
await test.step('Create proxy with WAF enabled', async () => {
|
|
await page.goto('/proxy-hosts', { waitUntil: 'networkidle' });
|
|
|
|
const addButton = page.getByRole('button', { name: /add|create/i }).first();
|
|
await addButton.click();
|
|
|
|
await page.getByLabel(/domain/i).fill(testProxy.domain);
|
|
await page.getByLabel(/target|forward/i).fill(testProxy.target);
|
|
await page.getByLabel(/description/i).fill(testProxy.description);
|
|
|
|
const wafToggle = page.locator('input[type="checkbox"][name*="waf"], [class*="waf"] input[type="checkbox"]').first();
|
|
if (await wafToggle.isVisible()) {
|
|
const isChecked = await wafToggle.isChecked();
|
|
if (!isChecked) {
|
|
await wafToggle.click();
|
|
}
|
|
}
|
|
|
|
const submitButton = page.getByRole('button', { name: /create|submit/i }).first();
|
|
await submitButton.click();
|
|
await page.waitForLoadState('networkidle');
|
|
});
|
|
|
|
await test.step('Send malicious SQL injection payload', async () => {
|
|
const start = Date.now();
|
|
|
|
const response = await page.request.get(
|
|
`http://127.0.0.1:8080/?id=1' OR '1'='1`,
|
|
{ ignoreHTTPSErrors: true }
|
|
);
|
|
|
|
const duration = Date.now() - start;
|
|
console.log(`✓ Malicious request responded in ${duration}ms with status ${response.status()}`);
|
|
|
|
expect(response.status()).toBe(403);
|
|
});
|
|
});
|
|
|
|
// INT-002-2: Rate limiting blocks excessive requests (429)
|
|
test('Rate limiting blocks requests exceeding threshold', async ({ page }) => {
|
|
await test.step('Create proxy with rate limiting enabled', async () => {
|
|
await page.goto('/proxy-hosts', { waitUntil: 'networkidle' });
|
|
|
|
const addButton = page.getByRole('button', { name: /add|create/i }).first();
|
|
await addButton.click();
|
|
|
|
await page.getByLabel(/domain/i).fill(testProxy.domain);
|
|
await page.getByLabel(/target|forward/i).fill(testProxy.target);
|
|
await page.getByLabel(/description/i).fill(testProxy.description);
|
|
|
|
const rateLimitToggle = page.locator('input[type="checkbox"][name*="rate"], [class*="rate"] input[type="checkbox"]').first();
|
|
if (await rateLimitToggle.isVisible()) {
|
|
const isChecked = await rateLimitToggle.isChecked();
|
|
if (!isChecked) {
|
|
await rateLimitToggle.click();
|
|
}
|
|
}
|
|
|
|
// Set rate limit to 3 requests per 10 seconds
|
|
const limitInput = page.locator('input[name*="limit"]').first();
|
|
if (await limitInput.isVisible()) {
|
|
await limitInput.fill('3');
|
|
}
|
|
|
|
const submitButton = page.getByRole('button', { name: /create|submit/i }).first();
|
|
await submitButton.click();
|
|
await page.waitForLoadState('networkidle');
|
|
});
|
|
|
|
await test.step('Send requests up to limit (should succeed)', async () => {
|
|
for (let i = 0; i < 3; i++) {
|
|
const response = await page.request.get(
|
|
`http://127.0.0.1:8080/test-${i}`,
|
|
{ ignoreHTTPSErrors: true }
|
|
);
|
|
expect([200, 404, 503]).toContain(response.status()); // Acceptable responses
|
|
}
|
|
});
|
|
|
|
await test.step('Send request exceeding limit (should be rate limited)', async () => {
|
|
const response = await page.request.get(
|
|
`http://127.0.0.1:8080/test-over-limit`,
|
|
{ ignoreHTTPSErrors: true }
|
|
);
|
|
expect(response.status()).toBe(429);
|
|
});
|
|
});
|
|
|
|
// INT-002-3: WAF and rate limit enforced independently
|
|
test('WAF enforces regardless of rate limit status', async ({ page }) => {
|
|
await test.step('Create proxy with both WAF and rate limiting', async () => {
|
|
await page.goto('/proxy-hosts', { waitUntil: 'networkidle' });
|
|
|
|
const addButton = page.getByRole('button', { name: /add|create/i }).first();
|
|
await addButton.click();
|
|
|
|
await page.getByLabel(/domain/i).fill(testProxy.domain);
|
|
await page.getByLabel(/target|forward/i).fill(testProxy.target);
|
|
await page.getByLabel(/description/i).fill(testProxy.description);
|
|
|
|
const wafToggle = page.locator('input[type="checkbox"][name*="waf"]').first();
|
|
if (await wafToggle.isVisible()) {
|
|
const isChecked = await wafToggle.isChecked();
|
|
if (!isChecked) {
|
|
await wafToggle.click();
|
|
}
|
|
}
|
|
|
|
const rateLimitToggle = page.locator('input[type="checkbox"][name*="rate"]').first();
|
|
if (await rateLimitToggle.isVisible()) {
|
|
const isChecked = await rateLimitToggle.isChecked();
|
|
if (!isChecked) {
|
|
await rateLimitToggle.click();
|
|
}
|
|
}
|
|
|
|
const submitButton = page.getByRole('button', { name: /create|submit/i }).first();
|
|
await submitButton.click();
|
|
await page.waitForLoadState('networkidle');
|
|
});
|
|
|
|
await test.step('Malicious request blocked by WAF (403)', async () => {
|
|
const response = await page.request.get(
|
|
`http://127.0.0.1:8080/?id=1' UNION SELECT NULL--`,
|
|
{ ignoreHTTPSErrors: true }
|
|
);
|
|
expect(response.status()).toBe(403);
|
|
});
|
|
|
|
await test.step('Legitimate requests respect rate limit', async () => {
|
|
const limit = 3;
|
|
const responses = [];
|
|
|
|
for (let i = 0; i < limit + 2; i++) {
|
|
const response = await page.request.get(
|
|
`http://127.0.0.1:8080/valid-${i}`,
|
|
{ ignoreHTTPSErrors: true }
|
|
);
|
|
responses.push(response.status());
|
|
}
|
|
|
|
// First N should be 200/404, remaining should be 429
|
|
expect(responses[responses.length - 1]).toBe(429);
|
|
});
|
|
});
|
|
|
|
// INT-002-4: Request within limit but triggers WAF
|
|
test('Malicious request gets 403 (WAF) not 429 (rate limit)', async ({ page }) => {
|
|
await test.step('Create proxy with both modules', async () => {
|
|
await page.goto('/proxy-hosts', { waitUntil: 'networkidle' });
|
|
|
|
const addButton = page.getByRole('button', { name: /add|create/i }).first();
|
|
await addButton.click();
|
|
|
|
await page.getByLabel(/domain/i).fill(testProxy.domain);
|
|
await page.getByLabel(/target|forward/i).fill(testProxy.target);
|
|
await page.getByLabel(/description/i).fill(testProxy.description);
|
|
|
|
const wafToggle = page.locator('input[type="checkbox"][name*="waf"]').first();
|
|
if (await wafToggle.isVisible()) {
|
|
const isChecked = await wafToggle.isChecked();
|
|
if (!isChecked) {
|
|
await wafToggle.click();
|
|
}
|
|
}
|
|
|
|
const rateLimitToggle = page.locator('input[type="checkbox"][name*="rate"]').first();
|
|
if (await rateLimitToggle.isVisible()) {
|
|
const isChecked = await rateLimitToggle.isChecked();
|
|
if (!isChecked) {
|
|
await rateLimitToggle.click();
|
|
}
|
|
}
|
|
|
|
const submitButton = page.getByRole('button', { name: /create|submit/i }).first();
|
|
await submitButton.click();
|
|
await page.waitForLoadState('networkidle');
|
|
});
|
|
|
|
await test.step('WAF error (403) takes priority over rate limit (429)', async () => {
|
|
// First legitimate request
|
|
await page.request.get(`http://127.0.0.1:8080/valid-1`, { ignoreHTTPSErrors: true });
|
|
|
|
// Second legitimate request
|
|
await page.request.get(`http://127.0.0.1:8080/valid-2`, { ignoreHTTPSErrors: true });
|
|
|
|
// Third legitimate request
|
|
await page.request.get(`http://127.0.0.1:8080/valid-3`, { ignoreHTTPSErrors: true });
|
|
|
|
// Fourth would trigger rate limit, but...
|
|
// Malicious request should get 403 (WAF), not 429 (rate limit)
|
|
const maliciousResponse = await page.request.get(
|
|
`http://127.0.0.1:8080/?id=1' AND SLEEP(5)--`,
|
|
{ ignoreHTTPSErrors: true }
|
|
);
|
|
|
|
// Should be 403 from WAF, not 429 from rate limiter
|
|
expect(maliciousResponse.status()).toBe(403);
|
|
});
|
|
});
|
|
|
|
// INT-002-5: Request exceeds limit (429) without malicious content
|
|
test('Clean request gets 429 when rate limit exceeded', async ({ page }) => {
|
|
await test.step('Setup proxy with rate limiting', async () => {
|
|
await page.goto('/proxy-hosts', { waitUntil: 'networkidle' });
|
|
|
|
const addButton = page.getByRole('button', { name: /add|create/i }).first();
|
|
await addButton.click();
|
|
|
|
await page.getByLabel(/domain/i).fill(testProxy.domain);
|
|
await page.getByLabel(/target|forward/i).fill(testProxy.target);
|
|
await page.getByLabel(/description/i).fill(testProxy.description);
|
|
|
|
const rateLimitToggle = page.locator('input[type="checkbox"][name*="rate"]').first();
|
|
if (await rateLimitToggle.isVisible()) {
|
|
const isChecked = await rateLimitToggle.isChecked();
|
|
if (!isChecked) {
|
|
await rateLimitToggle.click();
|
|
}
|
|
}
|
|
|
|
// Set limit to 2 requests
|
|
const limitInput = page.locator('input[name*="limit"]').first();
|
|
if (await limitInput.isVisible()) {
|
|
await limitInput.fill('2');
|
|
}
|
|
|
|
const submitButton = page.getByRole('button', { name: /create|submit/i }).first();
|
|
await submitButton.click();
|
|
await page.waitForLoadState('networkidle');
|
|
});
|
|
|
|
await test.step('Send clean requests and verify rate limiting', async () => {
|
|
// Request 1 - OK
|
|
const res1 = await page.request.get(`http://127.0.0.1:8080/clean-1`, { ignoreHTTPSErrors: true });
|
|
expect([200, 404, 503]).toContain(res1.status());
|
|
|
|
// Request 2 - OK
|
|
const res2 = await page.request.get(`http://127.0.0.1:8080/clean-2`, { ignoreHTTPSErrors: true });
|
|
expect([200, 404, 503]).toContain(res2.status());
|
|
|
|
// Request 3 - Rate limited
|
|
const res3 = await page.request.get(`http://127.0.0.1:8080/clean-3`, { ignoreHTTPSErrors: true });
|
|
expect(res3.status()).toBe(429);
|
|
});
|
|
});
|
|
});
|