Allow-List Validation: Accepting Only Known-Good Input
Input Validation: The First Line of Defense Against Injection
Input validation is the process of ensuring that user-supplied data conforms to expected formats, types, and ranges before the application processes it. The most effective validation strategy is allow-list validation — accepting only input that matches a known-good pattern — rather than deny-list validation — rejecting input that matches known-bad patterns.
Why Allow-List Beats Deny-List
Deny-list validation attempts to block known dangerous patterns:
# DENY-LIST — blocks known-bad patterns (INSUFFICIENT)
def is_safe(input):
dangerous = ['<script>', 'SELECT', 'DROP', 'UNION', 'OR 1=1']
for pattern in dangerous:
if pattern in input:
return False
return True
This approach fails because:
- Attackers find gaps: The deny-list cannot anticipate every possible attack payload. URL encoding, case variation, and novel syntax all bypass static lists.
- Context-dependent danger: The string
SELECTis dangerous in a SQL context but harmless in a blog post. Deny-lists cannot account for context. - Maintenance burden: New attack patterns emerge constantly. The deny-list requires continuous updates.
Allow-list validation inverts the logic — it defines what is allowed and rejects everything else:
# ALLOW-LIST — accepts only known-good patterns
import re
def validate_username(username):
# Only allow alphanumeric characters, 3-20 characters long
if not re.match(r'^[a-zA-Z0-9]{3,20}$', username):
raise ValueError('Invalid username format')
return username
def validate_age(age):
# Only allow integers between 0 and 150
try:
age_int = int(age)
if not (0 <= age_int <= 150):
raise ValueError
return age_int
except (ValueError, TypeError):
raise ValueError('Invalid age')
Validation at Trust Boundaries
Validation must occur at every trust boundary — any point where data crosses from a less-trusted zone to a more-trusted zone:
- External input: HTTP request parameters, headers, body, file uploads
- Inter-process communication: Message queues, RPC calls between services
- Database output: Data retrieved from the database may have been modified by another service or contain legacy data
- File system: Configuration files, uploaded files, cached data
Critical principle: Client-side validation (JavaScript in the browser) is a usability feature, not a security control. Attackers can bypass it entirely by sending direct HTTP requests. Server-side validation is mandatory.
Validation Strategies by Data Type
Strings: Use regular expressions to define exact patterns.
# Email validation (simplified)
EMAIL_RE = re.compile(r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$')
Numbers: Parse to the correct type and validate range.
# Integer with range
value = int(input_str)
if not (1 <= value <= 100):
raise ValueError
Enumerations: Validate against a fixed set of allowed values.
ALLOWED_SORT_FIELDS = {'name', 'date', 'price'}
if sort_field not in ALLOWED_SORT_FIELDS:
raise ValueError('Invalid sort field')
Dates: Parse with a strict format and validate range.
from datetime import datetime
try:
date = datetime.strptime(date_str, '%Y-%m-%d')
except ValueError:
raise ValueError('Invalid date format')
Files: Validate file type by content (magic bytes), not just extension. Limit file size.
import magic
file_type = magic.from_buffer(file_content, mime=True)
if file_type not in ('image/png', 'image/jpeg'):
raise ValueError('Invalid file type')
Context-Aware Output Encoding
Output Encoding: Escaping Data for the Correct Context
Input validation reduces the attack surface, but it cannot eliminate all injection risks. Output encoding is the complementary defense — it ensures that data is safely rendered in the specific context where it appears, regardless of what the data contains.
Why Both Are Necessary
Consider a username field that allows hyphens (for names like mary-jane). An attacker registers the username mary-jane"><script>alert(1)</script>. Input validation rejects it because it contains < and >. But what if the username is mary-jane'onmouseover='alert(1)? This passes validation (no angle brackets) but is dangerous in an HTML attribute context.
Output encoding handles this by escaping the data for the specific context where it will be rendered.
Encoding by Context
HTML Body Context
When data appears between HTML tags:
<div>USER_DATA_HERE</div>
Escape <, >, &, ", and ':
import html
safe = html.escape(user_input)
# < > & " ' become < > & " '
HTML Attribute Context
When data appears inside an HTML attribute value:
<input value="USER_DATA_HERE">
Escape quotes in addition to HTML entities:
safe = html.escape(user_input, quote=True)
# Also escapes " and '
JavaScript Context
When data appears inside a <script> block or JavaScript string:
<script>
var name = 'USER_DATA_HERE';
</script>
Use JSON encoding for JavaScript string values:
const safe = JSON.stringify(userInput);
// Escapes quotes, backslashes, and control characters
URL Context
When data appears in a URL parameter:
https://example.com/search?q=USER_DATA_HERE
Use percent-encoding:
const safe = encodeURIComponent(userInput);
CSS Context
When data appears in a CSS value:
.content::before { content: "USER_DATA_HERE"; }
CSS encoding is complex. The safest approach is to use a strict allow-list of allowed characters:
import re
safe = re.sub(r'[^a-zA-Z0-9\s\-_]', '', user_input)
Common Encoding Mistakes
1. Using the wrong encoder for the context:
# WRONG — HTML encoding does not protect JavaScript context
safe = html.escape(user_input)
return f'<script>var name = "{safe}"</script>'
# Attacker input: "; alert(1); //
# HTML escape does not escape quotes in JS context
2. Double-encoding:
# WRONG — encoding already-encoded data
safe = html.escape(html.escape(user_input))
# Results in &lt; instead of <
3. Encoding at the wrong layer:
# WRONG — encoding before storage
user_input = html.escape(request.POST['comment'])
db.store(user_input) # Stores encoded data
# Later retrieval shows encoded entities to users
Encode at the point of output, not at the point of input.
Practice: Fix the Output Encoding
The following code has an output encoding vulnerability. Identify and fix it:
@app.route('/greet')
def greet():
name = request.args.get('name', 'Guest')
# VULNERABLE — name is inserted into HTML without encoding
return f'<h1>Hello, {name}!</h1>'
An attacker supplies name=<script>alert(1)</script> and the script executes.
Fix:
import html
@app.route('/greet')
def greet():
name = request.args.get('name', 'Guest')
safe_name = html.escape(name)
return f'<h1>Hello, {safe_name}!</h1>'
The html.escape() function converts < to <, > to >, and & to &, preventing the browser from interpreting the input as HTML.
Practice Problems
Implement allow-list validation for the following inputs: 1. A username: 3-20 characters, alphanumeric and underscores only 2. A sorting field: must be one of 'name', 'date', 'price', 'rating' 3. A page number: positive integer, maximum 10,000 4. A hex color code: exactly 6 hex characters (e.g., #FF5733)
Solution
```python
import re
def validate_username(username):
if not re.match(r'^[a-zA-Z0-9_]{3,20}$', username):
raise ValueError('Username must be 3-20 alphanumeric characters or underscores')
return username
def validate_sort_field(sort_field):
allowed = {'name', 'date', 'price', 'rating'}
if sort_field not in allowed:
raise ValueError(f'Sort field must be one of: {allowed}')
return sort_field
def validate_page_number(page):
try:
page_int = int(page)
if page_int < 1 or page_int > 10000:
raise ValueError
return page_int
except (ValueError, TypeError):
raise ValueError('Page number must be a positive integer (1-10000)')
def validate_hex_color(color):
if not re.match(r'^#[0-9A-Fa-f]{6}$', color):
raise ValueError('Color must be a 6-digit hex code (e.g., #FF5733)')
return color
``` For each scenario, identify the output context and the correct encoding: 1. A user's display name is shown on their profile page: `<div class="name">USER_NAME</div>` 2. A search term is placed in a JavaScript variable: `<script>var query = 'SEARCH_TERM';</script>` 3. A product ID is used in a URL: `<a href="/product/PRODUCT_ID">View</a>` 4. A user's bio is placed in an HTML attribute: `<div title="USER_BIO">Hover me</div>`
Solution
1. **HTML body context** — use `html.escape()` to convert `<`, `>`, `&` to entities. The data appears between HTML tags.
2. **JavaScript context** — use `JSON.stringify()` to safely encode the string for JavaScript. HTML entity encoding does not protect against JavaScript injection.
3. **URL context** — use `encodeURIComponent()` to percent-encode the product ID for safe inclusion in a URL path segment.
4. **HTML attribute context** — use `html.escape(input, quote=True)` to also escape quotes (`"` and `'`), preventing attribute breakout attacks. Quiz
1. Why is allow-list validation more effective than deny-list validation?
2. Why is client-side (JavaScript) validation insufficient as a security control?
3. A user's comment is stored in the database and later displayed on a page. Where should output encoding be applied?
4. An attacker submits the username `john" onmouseover="alert(1)`. The application validates that usernames contain only letters, numbers, and hyphens, but the validation is case-insensitive and allows the input through due to a bug. Which output context is most dangerous for this payload?
5. Which of the following is a common output encoding mistake?
Flashcards
Question
What is the difference between allow-list and deny-list validation?
Click to reveal answer
Answer
Allow-list validation accepts only input that matches known-good patterns and rejects everything else. Deny-list validation rejects input that matches known-bad patterns. Allow-lists are more secure because the default is rejection and they cannot be bypassed by novel attack patterns.
Question
Why is client-side validation insufficient as a security control?
Click to reveal answer
Answer
Client-side validation runs in the attacker's browser, which they fully control. Attackers can bypass it by sending direct HTTP requests. Server-side validation is mandatory because it runs in a trusted environment.
Question
When should output encoding be applied?
Click to reveal answer
Answer
At the point of output — when data is rendered in the response. Not before storage (which causes double-encoding). Input validation and output encoding are complementary: validation reduces the attack surface, encoding ensures safe rendering.
Question
What encoding is needed for data placed in an HTML attribute context?
Click to reveal answer
Answer
HTML entity encoding with quote escaping (`html.escape(input, quote=True)` in Python). This converts `<`, `>`, `&` to entities and also escapes `"` and `'` to prevent attribute breakout attacks.
Question
What is double-encoding and why is it a problem?
Click to reveal answer
Answer
Double-encoding occurs when already-encoded data is encoded again. For example, `<` becomes `&lt;`. The browser renders the literal text `<` instead of `<`. Encode once, at the point of output, using the correct encoder for the context.
Revision Notes
Key Takeaways
- 1. Allow-list validation accepts only known-good formats and rejects everything else — more secure than deny-lists
- 2. Server-side validation is mandatory; client-side validation is a usability feature, not a security control
- 3. Output encoding must match the specific context (HTML body, attribute, JavaScript, URL, CSS)
- 4. Encode at the point of output, not at the point of input — encoding before storage causes double-encoding
- 5. Input validation and output encoding are complementary defenses — both are necessary
Interview Tips
- • Explain why allow-list validation is superior to deny-list with a concrete example of a deny-list bypass
- • Demonstrate context-aware output encoding with specific examples for HTML body, attribute, JavaScript, and URL contexts
- • Discuss why client-side validation is insufficient and where server-side validation belongs in a multi-tier architecture
- • Connect input validation and output encoding to the OWASP Top 10 — they are the primary defenses against injection (A03)
Cheat Sheet
Input Validation and Output Encoding Cheat Sheet
Phase: Application Security and Secure Coding | Level: intermediate
Input Validation:
- Allow-list (accept known-good) beats deny-list (reject known-bad)
- Validate at every trust boundary — server-side is mandatory
- Client-side validation is a usability feature, not a security control
- Validate by data type: strings (regex), numbers (range), enums (set membership), dates (strict format), files (magic bytes)
Output Encoding by Context:
- HTML body:
html.escape(input)— escapes< > & " ' - HTML attribute:
html.escape(input, quote=True)— also escapes quotes - JavaScript:
JSON.stringify(input)— safe for JS string values - URL:
encodeURIComponent(input)— percent-encoding - CSS: strict allow-list of allowed characters
Common Mistakes:
- Using the wrong encoder for the context
- Double-encoding already-encoded data
- Encoding at input instead of output
- Relying on client-side validation alone
Key points:
- Allow-list validation accepts only known-good formats instead of blocking known-bad strings
- Output encoding escapes data for HTML, JavaScript, SQL, or shell contexts appropriately
- Validation belongs at trust boundaries on the server, regardless of any client-side checks
- Encode at the point of output, not at the point of input
Tags: secure-coding, injection