Skip to content
intermediate Phase 8 · Application Security and Secure Coding

Input Validation and Output Encoding

Constrain inputs by allow-list and encode outputs by context.

55m
0 problems
Topic Progress 0%

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:

  1. Attackers find gaps: The deny-list cannot anticipate every possible attack payload. URL encoding, case variation, and novel syntax all bypass static lists.
  2. Context-dependent danger: The string SELECT is dangerous in a SQL context but harmless in a blog post. Deny-lists cannot account for context.
  3. 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:

  1. External input: HTTP request parameters, headers, body, file uploads
  2. Inter-process communication: Message queues, RPC calls between services
  3. Database output: Data retrieved from the database may have been modified by another service or contain legacy data
  4. 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 &lt; &gt; &amp; &quot; &#x27;

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 &amp;lt; instead of &lt;

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 &lt;, > to &gt;, and & to &amp;, preventing the browser from interpreting the input as HTML.

Practice Problems

0 / 2 solved
Implement Allow-List Validation

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
```
Identify the Encoding Context

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?

Question 1 options

2. Why is client-side (JavaScript) validation insufficient as a security control?

Question 2 options

3. A user's comment is stored in the database and later displayed on a page. Where should output encoding be applied?

Question 3 options

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?

Question 4 options

5. Which of the following is a common output encoding mistake?

Question 5 options

Flashcards

Question

What is the difference between allow-list and deny-list validation?

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?

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?

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?

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?

Answer

Double-encoding occurs when already-encoded data is encoded again. For example, `&lt;` becomes `&amp;lt;`. The browser renders the literal text `&lt;` 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

Continue Learning