--- name: Form Validation Breaker description: Exhaustive testing of form validation logic including boundary values, injection payloads, encoding edge cases, and client-server validation bypass techniques version: 1.0.0 author: Pramod license: MIT tags: [form-validation, input-testing, boundary-testing, injection-testing, xss-prevention, validation-bypass, fuzzing] testingTypes: [security, e2e] frameworks: [playwright] languages: [typescript, javascript] domains: [web] agents: [claude-code, cursor, github-copilot, windsurf, codex, aider, continue, cline, zed, bolt, gemini-cli, amp] --- # Form Validation Breaker Skill You are an expert QA security engineer specializing in form validation testing. When the user asks you to test form inputs, break validation logic, or verify server-side protection against malicious input, follow these detailed instructions. ## Core Principles 1. **Client-side validation is a convenience, not a defense** -- Every form must have server-side validation that mirrors or exceeds client-side rules. Testing must bypass client-side checks to verify the server rejects invalid input. 2. **Boundary values reveal more bugs than random values** -- The edges of valid ranges (min, max, min-1, max+1, zero, empty, null) are where validation logic most commonly fails. 3. **Encoding matters as much as content** -- The same character can be represented in UTF-8, URL encoding, HTML entities, Unicode escapes, and base64. Validation that blocks `', category: 'xss', expectedResult: 'reject', description: 'Basic script injection' }, { name: `${fieldName}_xss_img`, value: '', category: 'xss', expectedResult: 'reject', description: 'Image onerror handler' }, { name: `${fieldName}_xss_svg`, value: '', category: 'xss', expectedResult: 'reject', description: 'SVG onload handler' }, { name: `${fieldName}_xss_event`, value: '" onfocus="alert(1)" autofocus="', category: 'xss', expectedResult: 'reject', description: 'Attribute injection with event handler' }, { name: `${fieldName}_xss_href`, value: 'javascript:alert(1)', category: 'xss', expectedResult: 'reject', description: 'JavaScript protocol in URL context' }, { name: `${fieldName}_xss_encoded`, value: '<script>alert(1)</script>', category: 'xss', expectedResult: 'reject', description: 'HTML entity encoded script tag' }, { name: `${fieldName}_xss_unicode`, value: '\u003cscript\u003ealert(1)\u003c/script\u003e', category: 'xss', expectedResult: 'reject', description: 'Unicode escaped script tag' }, { name: `${fieldName}_xss_mixed_case`, value: '', category: 'xss', expectedResult: 'reject', description: 'Mixed case script tag' }, { name: `${fieldName}_xss_null_byte`, value: 'alert(1)', category: 'xss', expectedResult: 'reject', description: 'Null byte in script tag' }, // SQL injection vectors { name: `${fieldName}_sqli_basic`, value: "' OR '1'='1", category: 'sqli', expectedResult: 'reject', description: 'Basic SQL injection' }, { name: `${fieldName}_sqli_union`, value: "' UNION SELECT * FROM users--", category: 'sqli', expectedResult: 'reject', description: 'UNION-based SQL injection' }, { name: `${fieldName}_sqli_drop`, value: "'; DROP TABLE users;--", category: 'sqli', expectedResult: 'reject', description: 'DROP TABLE injection' }, { name: `${fieldName}_sqli_comment`, value: "admin'--", category: 'sqli', expectedResult: 'reject', description: 'Comment-based authentication bypass' }, { name: `${fieldName}_sqli_blind`, value: "' AND 1=1--", category: 'sqli', expectedResult: 'reject', description: 'Blind SQL injection probe' }, { name: `${fieldName}_sqli_time`, value: "' OR SLEEP(5)--", category: 'sqli', expectedResult: 'reject', description: 'Time-based blind SQL injection' }, // Command injection { name: `${fieldName}_cmd_pipe`, value: '| ls -la', category: 'command', expectedResult: 'reject', description: 'Pipe command injection' }, { name: `${fieldName}_cmd_semicolon`, value: '; cat /etc/passwd', category: 'command', expectedResult: 'reject', description: 'Semicolon command injection' }, { name: `${fieldName}_cmd_backtick`, value: '`whoami`', category: 'command', expectedResult: 'reject', description: 'Backtick command injection' }, { name: `${fieldName}_cmd_subshell`, value: '$(cat /etc/passwd)', category: 'command', expectedResult: 'reject', description: 'Subshell command injection' }, // Path traversal { name: `${fieldName}_path_traversal`, value: '../../../etc/passwd', category: 'path', expectedResult: 'reject', description: 'Directory traversal' }, { name: `${fieldName}_path_null_byte`, value: '../../etc/passwd%00.jpg', category: 'path', expectedResult: 'reject', description: 'Null byte path traversal' }, // LDAP injection { name: `${fieldName}_ldap`, value: '*)(uid=*))(|(uid=*', category: 'ldap', expectedResult: 'reject', description: 'LDAP injection' }, // Template injection { name: `${fieldName}_ssti`, value: '{{7*7}}', category: 'template', expectedResult: 'reject', description: 'Server-side template injection' }, { name: `${fieldName}_ssti_jinja`, value: '{{ config.items() }}', category: 'template', expectedResult: 'reject', description: 'Jinja2 template injection' }, ]; } export function generateEncodingPayloads(fieldName: string): TestPayload[] { return [ // Unicode edge cases { name: `${fieldName}_zero_width_space`, value: 'test\u200Bvalue', category: 'encoding', expectedResult: 'reject', description: 'Zero-width space character' }, { name: `${fieldName}_zero_width_joiner`, value: 'test\u200Dvalue', category: 'encoding', expectedResult: 'reject', description: 'Zero-width joiner character' }, { name: `${fieldName}_bidi_override`, value: '\u202Emalicious\u202C', category: 'encoding', expectedResult: 'reject', description: 'Right-to-left override character' }, { name: `${fieldName}_homoglyph`, value: '\u0430dmin', category: 'encoding', expectedResult: 'reject', description: 'Cyrillic "a" homoglyph for "admin"' }, { name: `${fieldName}_emoji`, value: 'test value 🎉🚀💯', category: 'encoding', expectedResult: 'accept', description: 'Emoji characters (should be accepted if field allows unicode)' }, { name: `${fieldName}_combining_chars`, value: 'te\u0301st', category: 'encoding', expectedResult: 'accept', description: 'Combining diacritical marks' }, { name: `${fieldName}_surrogate_pair`, value: 'test \uD83D\uDE00 value', category: 'encoding', expectedResult: 'accept', description: 'Surrogate pair emoji' }, { name: `${fieldName}_null_char`, value: 'test\x00value', category: 'encoding', expectedResult: 'reject', description: 'Null character in string' }, { name: `${fieldName}_backspace`, value: 'test\x08value', category: 'encoding', expectedResult: 'reject', description: 'Backspace control character' }, { name: `${fieldName}_bell`, value: 'test\x07value', category: 'encoding', expectedResult: 'reject', description: 'Bell control character' }, // URL encoding { name: `${fieldName}_double_url_encode`, value: '%253Cscript%253E', category: 'encoding', expectedResult: 'reject', description: 'Double URL-encoded script tag' }, { name: `${fieldName}_overlong_utf8`, value: '%C0%BCscript%C0%BE', category: 'encoding', expectedResult: 'reject', description: 'Overlong UTF-8 encoding' }, ]; } ``` ## The Form Breaker Fixture The fixture provides utilities for filling forms, bypassing client-side validation, and capturing validation responses. ```typescript // tests/fixtures/form-breaker.fixture.ts import { test as base, Page, expect } from '@playwright/test'; export interface ValidationResult { fieldName: string; payload: string; payloadCategory: string; clientSideBlocked: boolean; serverSideBlocked: boolean; errorMessage: string; httpStatus?: number; responseBody?: string; } export class FormBreaker { constructor(private page: Page) {} /** * Fill a form field, bypassing any client-side maxlength or pattern restrictions */ async fillFieldBypassingValidation( selector: string, value: string ): Promise { await this.page.evaluate( ({ sel, val }) => { const element = document.querySelector(sel) as HTMLInputElement; if (!element) throw new Error(`Element not found: ${sel}`); // Remove client-side constraints element.removeAttribute('maxlength'); element.removeAttribute('minlength'); element.removeAttribute('pattern'); element.removeAttribute('required'); element.removeAttribute('min'); element.removeAttribute('max'); element.removeAttribute('step'); element.type = 'text'; // Override type constraints // Set value directly, bypassing React/Vue controlled component logic const nativeInputValueSetter = Object.getOwnPropertyDescriptor( HTMLInputElement.prototype, 'value' )!.set!; nativeInputValueSetter.call(element, val); // Dispatch events to trigger framework change handlers element.dispatchEvent(new Event('input', { bubbles: true })); element.dispatchEvent(new Event('change', { bubbles: true })); }, { sel: selector, val: value } ); } /** * Submit a form by intercepting the submit event and sending raw data */ async submitFormWithRawData( formSelector: string, data: Record ): Promise<{ status: number; body: string }> { // Intercept form submission to capture the response const [response] = await Promise.all([ this.page.waitForResponse( (resp) => resp.request().method() === 'POST', { timeout: 10000 } ).catch(() => null), this.page.evaluate( ({ sel, formData }) => { const form = document.querySelector(sel) as HTMLFormElement; if (!form) throw new Error(`Form not found: ${sel}`); // Remove form validation form.setAttribute('novalidate', 'true'); // Fill fields for (const [name, value] of Object.entries(formData)) { const field = form.querySelector(`[name="${name}"]`) as HTMLInputElement; if (field) { field.removeAttribute('required'); field.removeAttribute('pattern'); field.removeAttribute('maxlength'); const setter = Object.getOwnPropertyDescriptor( HTMLInputElement.prototype, 'value' )!.set!; setter.call(field, value); field.dispatchEvent(new Event('input', { bubbles: true })); field.dispatchEvent(new Event('change', { bubbles: true })); } } // Submit the form form.submit(); }, { sel: formSelector, formData: data } ), ]); if (response) { return { status: response.status(), body: await response.text().catch(() => ''), }; } return { status: 0, body: '' }; } /** * Send form data directly via API, completely bypassing the browser form */ async submitViaApi( url: string, data: Record, method: 'POST' | 'PUT' | 'PATCH' = 'POST' ): Promise<{ status: number; body: string }> { const response = await this.page.request.fetch(url, { method, headers: { 'Content-Type': 'application/json' }, data: JSON.stringify(data), }); return { status: response.status(), body: await response.text(), }; } /** * Check if a validation error message is displayed on the page */ async getValidationErrors(): Promise { return await this.page.evaluate(() => { const errors: string[] = []; // HTML5 validation messages document.querySelectorAll(':invalid').forEach((el) => { const input = el as HTMLInputElement; if (input.validationMessage) { errors.push(`[${input.name || input.id}]: ${input.validationMessage}`); } }); // Common error display patterns const errorSelectors = [ '[class*="error"]', '[class*="invalid"]', '[role="alert"]', '.field-error', '.form-error', '.validation-error', '[data-testid*="error"]', '[aria-invalid="true"]', ]; for (const selector of errorSelectors) { document.querySelectorAll(selector).forEach((el) => { const text = (el as HTMLElement).textContent?.trim(); if (text && text.length > 0 && text.length < 500) { errors.push(text); } }); } return [...new Set(errors)]; }); } } export const test = base.extend<{ formBreaker: FormBreaker }>({ formBreaker: async ({ page }, use) => { const breaker = new FormBreaker(page); await use(breaker); }, }); export { expect } from '@playwright/test'; ``` ## Writing the Tests ### Boundary Value Testing ```typescript // tests/form-validation/boundary-values.spec.ts import { test, expect } from '../fixtures/form-breaker.fixture'; import { generateBoundaryPayloads } from '../helpers/payload-generator'; test.describe('Form Boundary Value Testing', () => { test.beforeEach(async ({ page }) => { const baseUrl = process.env.BASE_URL || 'http://localhost:3000'; await page.goto(`${baseUrl}/signup`, { waitUntil: 'networkidle' }); }); test('email field should enforce valid email format', async ({ page, formBreaker }) => { const invalidEmails = [ 'plainaddress', '@missing-local.com', 'missing-at-sign.com', 'missing-domain@.com', 'missing-tld@domain.', 'spaces in@email.com', 'double@@email.com', '.leading-dot@email.com', 'trailing-dot.@email.com', 'multiple...dots@email.com', 'email@-leading-hyphen.com', 'email@domain..double-dot.com', '