Exhaustive testing of form validation logic including boundary values, injection payloads, encoding edge cases, and client-server validation bypass techniques
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.
<script> but allows %3Cscript%3E is broken.Organize your form validation testing suite with this structure:
tests/
form-validation/
boundary-values.spec.ts
injection-payloads.spec.ts
encoding-edge-cases.spec.ts
file-upload-validation.spec.ts
client-bypass.spec.ts
multi-step-forms.spec.ts
real-time-validation.spec.ts
fixtures/
form-breaker.fixture.ts
helpers/
payload-generator.ts
boundary-calculator.ts
encoding-transformer.ts
validation-reporter.ts
payloads/
xss-vectors.json
sql-injection.json
unicode-edge-cases.json
playwright.config.ts
The payload generator creates targeted test inputs for different validation scenarios.
// tests/helpers/payload-generator.ts
export interface TestPayload {
name: string;
value: string;
category: string;
expectedResult: 'accept' | 'reject';
description: string;
}
export function generateBoundaryPayloads(
fieldName: string,
options: {
minLength?: number;
maxLength?: number;
minValue?: number;
maxValue?: number;
required?: boolean;
type?: 'text' | 'email' | 'number' | 'url' | 'phone' | 'date';
}
): TestPayload[] {
const payloads: TestPayload[] = [];
const { minLength = 0, maxLength = 255, minValue, maxValue, required = true, type = 'text' } = options;
// Empty and whitespace
if (required) {
payloads.push({
name: `${fieldName}_empty`,
value: '',
category: 'boundary',
expectedResult: 'reject',
description: 'Empty string on required field',
});
payloads.push({
name: `${fieldName}_whitespace_only`,
value: ' ',
category: 'boundary',
expectedResult: 'reject',
description: 'Whitespace-only string on required field',
});
payloads.push({
name: `${fieldName}_tab_only`,
value: '\t\t',
category: 'boundary',
expectedResult: 'reject',
description: 'Tab-only string on required field',
});
payloads.push({
name: `${fieldName}_newline_only`,
value: '\n\n',
category: 'boundary',
expectedResult: 'reject',
description: 'Newline-only string on required field',
});
}
// Length boundaries
if (minLength > 0) {
payloads.push({
name: `${fieldName}_below_min_length`,
value: 'a'.repeat(minLength - 1),
category: 'boundary',
expectedResult: 'reject',
description: `String of length ${minLength - 1} (min is ${minLength})`,
});
payloads.push({
name: `${fieldName}_at_min_length`,
value: 'a'.repeat(minLength),
category: 'boundary',
expectedResult: 'accept',
description: `String of length ${minLength} (exact minimum)`,
});
}
if (maxLength) {
payloads.push({
name: `${fieldName}_at_max_length`,
value: 'a'.repeat(maxLength),
category: 'boundary',
expectedResult: 'accept',
description: `String of length ${maxLength} (exact maximum)`,
});
payloads.push({
name: `${fieldName}_above_max_length`,
value: 'a'.repeat(maxLength + 1),
category: 'boundary',
expectedResult: 'reject',
description: `String of length ${maxLength + 1} (above maximum)`,
});
payloads.push({
name: `${fieldName}_extreme_length`,
value: 'a'.repeat(maxLength * 10),
category: 'boundary',
expectedResult: 'reject',
description: `String of length ${maxLength * 10} (extreme overflow)`,
});
}
// Numeric boundaries
if (type === 'number' && minValue !== undefined && maxValue !== undefined) {
payloads.push(
{ name: `${fieldName}_below_min`, value: String(minValue - 1), category: 'boundary', expectedResult: 'reject', description: `Value ${minValue - 1} (below minimum ${minValue})` },
{ name: `${fieldName}_at_min`, value: String(minValue), category: 'boundary', expectedResult: 'accept', description: `Value ${minValue} (exact minimum)` },
{ name: `${fieldName}_at_max`, value: String(maxValue), category: 'boundary', expectedResult: 'accept', description: `Value ${maxValue} (exact maximum)` },
{ name: `${fieldName}_above_max`, value: String(maxValue + 1), category: 'boundary', expectedResult: 'reject', description: `Value ${maxValue + 1} (above maximum ${maxValue})` },
{ name: `${fieldName}_negative_zero`, value: '-0', category: 'boundary', expectedResult: 'accept', description: 'Negative zero' },
{ name: `${fieldName}_float`, value: '3.14159', category: 'boundary', expectedResult: 'reject', description: 'Float value in integer field' },
{ name: `${fieldName}_scientific`, value: '1e10', category: 'boundary', expectedResult: 'reject', description: 'Scientific notation' },
{ name: `${fieldName}_infinity`, value: 'Infinity', category: 'boundary', expectedResult: 'reject', description: 'Infinity value' },
{ name: `${fieldName}_nan`, value: 'NaN', category: 'boundary', expectedResult: 'reject', description: 'NaN value' }
);
}
return payloads;
}
export function generateInjectionPayloads(fieldName: string): TestPayload[] {
return [
// XSS vectors
{ name: `${fieldName}_xss_script`, value: '<script>alert("XSS")</script>', category: 'xss', expectedResult: 'reject', description: 'Basic script injection' },
{ name: `${fieldName}_xss_img`, value: '<img src=x onerror=alert(1)>', category: 'xss', expectedResult: 'reject', description: 'Image onerror handler' },
{ name: `${fieldName}_xss_svg`, value: '<svg onload=alert(1)>', 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: '<ScRiPt>alert(1)</sCrIpT>', category: 'xss', expectedResult: 'reject', description: 'Mixed case script tag' },
{ name: `${fieldName}_xss_null_byte`, value: '<scr\x00ipt>alert(1)</script>', 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 fixture provides utilities for filling forms, bypassing client-side validation, and capturing validation responses.
// 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<void> {
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<string, string>
): 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<string, string>,
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<string[]> {
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';
// 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',
'<script>@email.com',
'email@domain.com<script>',
];
for (const email of invalidEmails) {
await formBreaker.fillFieldBypassingValidation('input[name="email"]', email);
// Try to submit
const submitButton = page.getByRole('button', { name: /sign up|register|submit/i });
if (await submitButton.isVisible()) {
await submitButton.click();
await page.waitForTimeout(1000);
}
// Check that the form was not successfully submitted
const errors = await formBreaker.getValidationErrors();
const currentUrl = page.url();
// Either there should be validation errors visible,
// or the URL should not have changed to a success page
const wasRejected = errors.length > 0 || currentUrl.includes('signup');
expect(wasRejected, `Email "${email}" should have been rejected`).toBe(true);
// Reset the form
await page.reload();
}
});
test('username field should enforce length boundaries', async ({ page, formBreaker }) => {
const payloads = generateBoundaryPayloads('username', {
minLength: 3,
maxLength: 50,
required: true,
type: 'text',
});
for (const payload of payloads) {
await formBreaker.fillFieldBypassingValidation(
'input[name="username"]',
payload.value
);
const submitButton = page.getByRole('button', { name: /sign up|register|submit/i });
if (await submitButton.isVisible()) {
await submitButton.click();
await page.waitForTimeout(1000);
}
const errors = await formBreaker.getValidationErrors();
if (payload.expectedResult === 'reject') {
expect(
errors.length > 0 || page.url().includes('signup'),
`Payload "${payload.name}" should have been rejected: ${payload.description}`
).toBe(true);
}
await page.reload();
}
});
test('password field should enforce complexity requirements', async ({
page,
formBreaker,
}) => {
const weakPasswords = [
'123',
'password',
'12345678',
'abcdefgh',
'ABCDEFGH',
'!@#$%^&*',
'aA1', // Too short but meets complexity
' '.repeat(20), // Whitespace only
];
for (const password of weakPasswords) {
await formBreaker.fillFieldBypassingValidation(
'input[name="password"]',
password
);
const submitButton = page.getByRole('button', { name: /sign up|register|submit/i });
if (await submitButton.isVisible()) {
await submitButton.click();
await page.waitForTimeout(1000);
}
const errors = await formBreaker.getValidationErrors();
expect(
errors.length > 0 || page.url().includes('signup'),
`Weak password "${password}" should have been rejected`
).toBe(true);
await page.reload();
}
});
});
// tests/form-validation/injection-payloads.spec.ts
import { test, expect } from '../fixtures/form-breaker.fixture';
import { generateInjectionPayloads } from '../helpers/payload-generator';
test.describe('Injection Payload Testing', () => {
test('search field should sanitize XSS payloads', async ({ page, formBreaker }) => {
const baseUrl = process.env.BASE_URL || 'http://localhost:3000';
await page.goto(`${baseUrl}/search`, { waitUntil: 'networkidle' });
const xssPayloads = generateInjectionPayloads('search').filter(
(p) => p.category === 'xss'
);
for (const payload of xssPayloads) {
await formBreaker.fillFieldBypassingValidation(
'input[name="q"], input[type="search"], input[name="search"]',
payload.value
);
<!-- Content truncated for initial SEO render. Open the source file tab for the full file. -->
npx skills add PramodDutta/Form Validation Breaker下载完整 Skill 目录,包含 SKILL.md 及所有相关文件
Search for places (restaurants, cafes, etc.) via Google Places API proxy on localhost.
Interact with GitHub using the `gh` CLI. Use `gh issue`, `gh pr`, `gh run`, and `gh api` for issues, PRs, CI runs, and advanced queries.
Create or update AgentSkills. Use when designing, structuring, or packaging skills with scripts, references, and assets.
Start voice calls via the OpenClaw voice-call plugin.
Notion API for creating and managing pages, databases, and blocks.
Gemini CLI for one-shot Q&A, summaries, and generation.
Category:developer
Tags:form-validation, input-testing, boundary-testing, injection-testing, xss-prevention, validation-bypass, fuzzing