Input Validation and Output Encoding
Input validation checks every value that enters an application against the rules of its field and rejects what does not fit, on the server, before the value is used. Output encoding converts a stored value into the form that is safe for the place it is written to, so that a browser, a URL parser or a database treats it as data, never as code: in an HTML page < becomes <, in a URL a space becomes %20, and a database receives the value as a query parameter.
Example: a customer called O'Brien & Sons is valid input that no rule should reject. The apostrophe and the ampersand become dangerous only at the exit, if a page or a query is glued together from text. Validation decides what gets in; encoding decides how it is written out. An application needs both, and a tester checks both.
Why it matters
Injection and cross-site scripting (XSS), which the next two lessons test in detail, share one root cause: a value from outside reaches an interpreter such as a database, a shell or a browser and runs as code. Injection is one of the OWASP Top 10:2025 categories and includes XSS, and the two controls that prevent it are exactly these: validate what comes in, encode what goes out. One unchecked entry point or one unencoded output is enough.
For a tester this is productive ground, because the defects hide in the gaps between layers: a form that validates while the API does not, a list page that encodes while the export does not. Functional tests with tidy values never reach those gaps. A few harmless marker values, carried from every entry point to every place the value appears, find them in minutes, without attack tools, on systems you are authorised to test.
Input validation: allow-list rules, enforced on the server
The OWASP Input Validation Cheat Sheet asks for two checks on every value: its syntax, the expected type and format (type, length, range, format, membership of a fixed set), and its semantics, whether the value makes sense for the operation: the visit date is not in the past, the quantity does not exceed the stock. Testers usually call them syntactic and semantic validation. Rules are written as allow-lists: define what is accepted and reject everything else. A block-list (deny-list) of forbidden strings is always incomplete, because a dangerous value can be spelled in more ways than any list holds, and it rejects honest data: blocking the apostrophe rejects O'Brien without making any query safe. The cheat sheet lists deny-listing and "cleaning" input among its common pitfalls. Free text has an allow-list too, only a wider one: one character encoding agreed across components, malformed input rejected, legitimate punctuation and scripts kept, a length limit, and, where values are compared, one Unicode normalisation applied before validation and storage (so that é typed as one character or as e plus an accent mark is the same value).
Browser checks such as required, maxlength and a pattern are feedback, not a control: as in the lesson on testing web forms, anyone can edit the page or send the request without it. The cheat sheet is explicit: validate on the server even when the browser checks the same fields, and validate data from internal APIs, partner feeds and queues too. The form is only one entry point: the JSON API the mobile app calls, a bulk import, a query string, a header such as Accept-Language, a cookie and a webhook all bring values from outside. The tester proves that every path enforces every rule.
- Write each rule so that it can be tested:
^ORD-[0-9]{5}$for an order number,ORD-and exactly five digits (the anchors^and$make the whole value match, as the cheat sheet asks; without them the pattern also acceptsxx ORD-12345 <b>); an integer from 1 to 20 for a quantity; one value from the stored list for a country; for a company name 2 to 80 characters of Unicode letters, digits, spaces and' - & ., so the apostrophe and the ampersand stay legal. - Copy the form's request from the Network panel as cURL, change one field to a value the page refuses, send it with a test account and expect a
4xxthat names the field and the rule, nothing stored, and no stack trace or SQL in the message. - Send the same set through the API and the import: 81 characters, a quantity of 0 and of 21, country
ZZ, and the request below, in whichcompanyandnotespass their rules whilequantityandcountryfail. Any path that accepts what the form refuses is a finding. - Watch for silent repair: a server that cuts 81 characters to 80 or strips
<without a message changes the user's data. Unless the requirement allows it, report it.
POST /api/customers HTTP/1.1
Host: crm.test.example
Content-Type: application/json
{"company": "O'Brien & Sons", "quantity": 21, "country": "ZZ",
"notes": "Discount < 10% until <b>Ramadan</b>"}
HTTP/1.1 422 Unprocessable Content
Content-Type: application/json
{"errors": [{"field": "quantity", "rule": "integer 1-20"},
{"field": "country", "rule": "not a known country code"}]}Try it: Regex Tester Try it: Negative Test Generator Try it: Special Character Dataset Generator
Output encoding: one value, one encoding per context
Encoding happens at the moment of output, in the code that writes the value into a page, a link, a script or a query, and it depends on where the value lands. The OWASP Cross Site Scripting Prevention Cheat Sheet gives each HTML context its own rules: element content, attribute values, JavaScript, CSS and URLs. The database has its equivalent: a parameterised query sends the value separately from the SQL statement, so the database never parses it as SQL; the Injection Prevention Cheat Sheet treats escaping the input as a last resort. The stored value never changes, and a value encoded for the wrong context stays unsafe, because HTML entities inside a URL or a script string mean nothing there.
- HTML element content:
&becomes&,<becomes<,>becomes>,"becomes"and'becomes'('and'are the same character), so the page shows the characters as typed. - HTML attribute: the same entities, inside a quoted attribute such as
titleorvalue; an unquoted attribute can be broken out of with a space and many other characters. An event attribute such asonclickis a script context: the browser decodes the entities before the script runs, so they protect nothing there. - URL parameter: percent-encoding, so a space becomes
%20,&becomes%26and, in strict RFC 3986 encoding,'becomes%27(many encoders leave the apostrophe as it is); unencoded, the&in the name starts a second parameter. A URL written intohrefis then attribute-encoded as well. - JavaScript and JSON: string escaping by a maintained encoder, not quotes escaped by hand:
"as\",\as\\, plus<as\u003cwhen the data sits inside an HTML page, so a value cannot close the script. Safer still: keep the value out of the script, for example in adata-attribute read withdataset, and write it into the page withtextContent, neverinnerHTML. - SQL: a parameterised query or prepared statement keeps
O'Brienas data without touching the text.
Stored value O'Brien & Sons
HTML content O'Brien & Sons
HTML attribute title="O'Brien & Sons"
URL parameter ?q=O%27Brien%20%26%20Sons
JSON / JavaScript {"company": "O'Brien & Sons"} (nothing to escape here)
SQL WHERE company = ? (value bound as a parameter)
One character, three contexts: < -> < (HTML) %3C (URL) \u003c (script)Try it: HTML Entity Encoder Try it: URL Encoder Try it: JSON Escape / Unescape
Why both: validation at the door, encoding at every exit
Neither control replaces the other. Validation cannot reject O'Brien, Al-Harbi & Sons or a note saying price < 50, because they are correct data; only encoding keeps them harmless on the way out. Encoding cannot reject a quantity of -1, a ten-megabyte note or a country that does not exist, because they display perfectly well; only validation keeps bad data out. The Input Validation Cheat Sheet says the same: validation does not replace parameterised queries for SQL or context-aware output encoding against XSS.
The order is fixed: input, validate (accept or reject, never repair silently), store the accepted value unchanged, encode for the context at every output. Storing an already encoded value breaks the chain, because every other output then receives wrong data: the e-mail shows & as text, the CSV export carries entities, and a page that encodes correctly shows & on screen because its source now holds &amp;, the double-encoding defect. Sanitisation is a third, separate operation for the few fields that must accept markup, such as a rich-text editor: a sanitiser library keeps only an allow-list of tags and attributes, and the result is still encoded wherever it leaves as plain text.
- 1Input arrivesAny entry point: form, JSON API, import, query string, header, cookie, webhook
O'Brien & Sons - 2Validate on the serverAllow-list rule per field: type, length, range, format, set, then the business meaningAllow-list
Branch: Rejected:
4xxnaming the field and the rule; nothing stored, nothing repaired silentlyGate: Accepted values only, unchanged
- 3Store as validatedThe stored value never changes: no entities, nothing stripped, nothing cut
O'Brien & SonsBranch: Defect: stored already encoded, so a page shows
&as text (&amp;in its source) and e-mails and exports carry raw entitiesFail - 4Encode at output, per contextIn the code that writes the value, at the moment it leavesOne value, one encoding per context
- 5HTML content or quoted attributeEntities
O'Brien & Sons - 6URL parameterPercent-encoding
?q=O%27Brien%20%26%20Sons - 7JavaScript or JSON stringString escaping of
"and\, and<as\u003cinside a page; nothing to escape here"O'Brien & Sons" - 8SQLParameterised query: the value never touches the statement
WHERE company = ?
Branch: Defect: encoded for the wrong context, such as HTML entities inside a URL or a script string, where they protect nothingFail
- 9OutputPage, link, script, query, e-mail, CSV export, API response: every exit, shown as typed
O'Brien & Sons
Then:The browser, URL parser or database treats the value as data, never as code
Worked example: one customer through its outputs
A CRM in your test environment, with written permission to test it, has the customer form from above. The tester saves a customer with the company O'Brien & Sons and the note Discount < 10% until <b>Ramadan</b>, once through the form and once through the API with a valid quantity and country, then visits every place the values appear: the customer list, the tooltip on the name (a title attribute), the "find similar" link, the CSV export and the welcome e-mail.
- Customer list: both values shown as typed; the source has
',&and<. Pass. - Tooltip: shows only
O. The source readstitle='O'Brien & Sons': the apostrophe ended the single-quoted attribute and the rest landed outside it. Finding: no attribute encoding. Stop and report; the XSS lesson continues from here. - Find-similar link:
/customers?q=O'Brien & Sonswithout percent-encoding; the search runs forO'Brienand the server receives a second parameter namedSons. Finding: no URL encoding, and a functional defect for every name with an ampersand. - CSV export: the note arrives as
Discount < 10% until <b>Ramadan</b>. Finding: the export reused HTML-encoded text, so whoever downloads it gets wrong data; encoding for the wrong context. - Welcome e-mail:
Ramadanin bold. Finding: the template inserts the note as markup, so text written by one user is rendered in another's mailbox; report it as HTML injection with restricted visibility and let the security lead judge severity. - Entry points: through the API,
quantity: 21andcountry: ZZreturn422naming both fields, nothing stored. Pass. Through the import, an 81-character company name is accepted and cut to 80 without a message. Finding: the import validates less than the form.
Common mistakes
- Treating
required,maxlengthor a pattern in the browser as protection, and never sending the request without the page. - Testing the main form only, while the API, the import and the mobile client reach the same database with weaker rules.
- Writing block-lists such as "reject
<script>" instead of allow-list rules; the first spelling the list does not contain gets through. - Encoding before storing, or storing an already encoded value, which puts
&amp;into a page's source and raw entities into e-mails and exports. - Encoding for the wrong context, for example HTML entities inside a URL or a script string, where they protect nothing.
- Using script payloads instead of harmless markers, or testing a system without written permission.
The QA perspective
Validation rules are test basis. In refinement, ask for every new field: type, length, range, format, the allowed set, what the user sees on rejection, and where the value will be displayed and in which contexts. Write the answers into the acceptance criteria so that developer, tester and API documentation agree, then derive the test cases with equivalence partitioning and boundary value analysis as in the test design module.
During testing, keep one marker set and carry it through every entry point to every output, including e-mails, PDFs, exports and admin screens; record request, response and page source, not screenshots alone, with tokens masked. Report a missing server check or a missing encoding as a security finding with restricted visibility, name the entry point and the exact output, and agree severity with the security lead. After the fix, confirmation-test every output and keep the markers in the regression suite: a new template, export or client reopens the gap.
Practice exercise
Review the validation and encoding of a booking form
A clinic booking site in your test environment, which you have written permission to test, has a form with Name (2 to 60 characters: letters, spaces, apostrophe and hyphen), Mobile (05 and eight digits), Reason (free text, up to 500 characters) and Slot (one of the offered times). Below are one API request with its response and three fragments of page source where the saved values appear.
Decide for each of the five observations whether it passes or is a finding: the accepted name, the accepted slot and the three fragments. Name the missing control (a validation rule, server-side enforcement, or encoding and its context) and write a bug title. Then list the next checks you would run.
POST /api/bookings
{"name": "Sara <b>qa</b>", "mobile": "0512345678",
"reason": "Follow-up, pain < 3/10 since 'Sunday'", "slot": "2026-11-03T25:00"}
HTTP/1.1 201 Created
{"id": 8821, "name": "Sara <b>qa</b>", "slot": "2026-11-03T25:00"}
Fragment 1: reception list
<td>Sara <b>qa</b></td>
Fragment 2: doctor's screen
<p class="reason">Follow-up, pain < 3/10 since 'Sunday'</p>
Fragment 3: reminder page
<a href="/r?name=Sara <b>qa</b>&id=8821">Reschedule</a>- Compare the request's values with the four rules and read the response: which rules did the server enforce and which did it not?
- For each fragment, name the output context and the encoding it requires, then compare with what the source shows.
- Order the findings by risk and write one bug title for each.
Show the model answer
Name: Sara <b>qa</b> contains <, > and /, which the allow-list does not permit, yet 201 Created echoes it. Finding: the server does not enforce the name rule that the form shows. Bug title: "POST /api/bookings accepts markup characters in name that the form rejects".
Slot: 2026-11-03T25:00 is not one of the offered times and hour 25 does not exist, yet it was stored. Finding: no set or semantic validation on slot. Bug title: "Booking API stores a slot that was never offered".
Fragment 1: <b>qa</b> is correct element-content encoding; the receptionist sees the name as typed. Pass.
Fragment 2: the reason is written raw. It displays correctly only because < is followed by a space, which browsers read as text; a reason containing <b> would render as markup on the doctor's screen. Finding: missing HTML encoding of stored patient text, a probable stored XSS (input saved once and rendered for every later viewer, the subject of the XSS lesson). Bug title: "Doctor's screen shows the booking reason without HTML encoding". Report with restricted visibility and do not probe further.
Fragment 3: the href is built without percent-encoding: the space, <, > and / should be %20, %3C, %3E and %2F, and a name containing &, which gets through because the server does not enforce the name rule, would start a new parameter. Finding: missing URL encoding, also a functional defect. Bug title: "Reschedule link breaks for names with special characters".
Order: fragment 2 first, then the two validation gaps, then the link. Mobile and reason are valid in this request, so it does not show whether the server enforces their rules. Next checks: an invalid mobile number and a 501-character reason through the API, expecting a 422 that names the field; the same values through the mobile app's endpoint and the import; the confirmation e-mail and the PDF appointment letter; and the severity of each finding agreed with the security lead.
Frequently asked questions
Is client-side validation enough when the API is only used by our own pages?
No. From the browser there is no private API: whatever the page can send, a person with developer tools can send differently. The OWASP Input Validation Cheat Sheet requires validation on the server even when the browser checks the same fields; browser checks stay useful for fast feedback.
Should the application store the encoded value so that every output is safe?
No. Store the value as validated and encode at each output for its context. A stored < is wrong data in an e-mail, an export or an API response, and a page that encodes correctly shows it as < on screen, because its source then holds &lt;. Rich text is the exception, handled by a sanitiser rather than by encoding.
What is the difference between validation, sanitisation and encoding?
Validation accepts or rejects a value against rules and changes nothing. Sanitisation changes the value by removing or altering parts, such as tags a rich-text field may not contain. Encoding changes only the representation for one output context, and the stored value stays the same. Tests look for a missing one of the three, and for sanitisation that happens silently where a rejection was expected.
Can I find these defects without security tools?
Yes. Harmless markers such as an apostrophe, an ampersand, angle brackets around a word, an over-long string and an out-of-range number, plus developer tools and a command-line client to send requests without the page, find missing checks and missing encoding. Proving that a weakness can be exploited is the security team's job; a missing control is already a defect worth reporting.
References
- OWASP Cheat Sheet Series: Input ValidationAllow-list validation, syntactic vs. semantic checks, and why client-side validation is never a security control.
- OWASP Cheat Sheet Series: Cross Site Scripting PreventionPreventing XSS: output encoding for each context, sanitizing user-authored HTML, safe DOM sinks and framework protections, with CSP only as defence in depth.
- OWASP Cheat Sheet Series: Injection PreventionPreventing SQL, OS-command and other injection through parameterisation, validation and escaping.
- OWASP Top 10:2025The ten most critical web application security risks, with how to test for and prevent each.
Practise with Mutqan tools
Test regular expressions live with match highlighting, capture groups and replace preview.
Generate negative test cases for form fields and APIs: empty, null, wrong type, too long, injection, invalid format.
Generate inputs full of special characters: quotes, SQL/HTML/JSON metacharacters, control chars, emoji and path tricks.
Escape HTML special characters (<, >, &, quotes) into entities to prevent XSS.
Percent-encode text for safe use in URLs, query strings and form data.
Escape text for safe embedding inside a JSON string, or unescape a JSON string literal.
Go deeper
Your progress on this topic
Not started
Mark the topic complete when you have studied it and done the exercise.
Sign in to track your progress
Mark topics complete, continue where you left off and unlock specialisation exams.