HIGH type confusionflask

Type Confusion in Flask

How Type Confusion Manifests in Flask

Type confusion in Flask APIs occurs when the framework's request parsing returns data in an unexpected format (typically strings) that the application logic incorrectly assumes matches a different type (e.g., integer, boolean). This mismatch can bypass security checks, corrupt data operations, or trigger injection vulnerabilities. Flask's request object has distinct behaviors that create specific attack surfaces:

  • request.args and request.form: Always return strings (or lists of strings) regardless of expected types. For example, request.args.get('page') yields '1' even if the client intended an integer.
  • request.json: Parses JSON according to RFC 8259—numbers become int or float, but strings containing digits (e.g., {"id": "123"}) remain strings. Flask does not coerce types based on OpenAPI specs.
  • Route converters (e.g., <int:user_id>): Convert path parameters to the specified type, but query/form/json data bypass this safety net.

Common Flask-specific attack patterns:

  1. Authorization Bypass via String/Integer Mismatch: When comparing user-supplied IDs (strings) against integer database keys or session values. Example:
@app.route('/api/profile')
def get_profile():
    # VULNERABLE: request.args.get() returns string
    requested_id = request.args.get('user_id')
    if requested_id == current_user.id:  # current_user.id is integer
        abort(403)
    # Attacker passes any string (e.g., "1") to bypass check
    return get_user_data(requested_id)

Here, '1' == 1 evaluates to False, so the authorization check fails open.

  1. NoSQL/ORM Query Manipulation: Using string parameters in queries expecting typed values. With MongoDB (via PyMongo):
@app.route('/api/users')
def search_users():
    age = request.args.get('age')  # string
    # VULNERABLE: age is string; {"$eq": "20"} matches differently than {"$eq": 20}
    users = db.users.find({"age": age})
    return jsonify(list(users))

An attacker can send age=20%24ne=null (URL-encoded 20$ne=null) to alter query semantics if the driver interprets string operators.

  1. Logic Errors in Pagination/Filtering: Non-integer strings in LIMIT/OFFSET clauses or numeric comparisons can cause exceptions or return excessive data:
@app.route('/api/orders')
def list_orders():
    limit = request.args.get('limit', 10)  # string '10'
    # VULNERABLE: if limit is used in raw SQL without cast
    query = f"SELECT * FROM orders LIMIT {limit}"
    # Attacker sends limit=1000,1000 to dump all records

Even with parameterized queries, some databases (e.g., PostgreSQL) will error if binding a string to an integer parameter, potentially causing denial-of-service.

  1. Boolean Parameter Misinterpretation: Flask treats any non-missing value as True in conditional checks. If an endpoint expects a boolean but receives a string like 'false':
@app.route('/api/feature')
def toggle_feature():
    enabled = request.args.get('enabled')  # string 'false'
    if enabled:  # 'false' is truthy → feature enabled against user intent
        enable_feature()
    return {'status': 'ok'}

This violates least privilege and can expose sensitive functionality.

Flask-Specific Detection

Manual Detection:

  • Search codebase for request.args.get(), request.form.get(), and request.json accesses without explicit type conversion.
  • Identify comparisons between request-derived values and typed variables (e.g., if user_id == some_int).
  • Check raw SQL/ORM queries that interpolate request parameters without casting (e.g., f"... WHERE id = {value}").
  • Review pagination/filtering endpoints for missing type=int in get() calls.

Dynamic Scanning with middleBrick:

middleBrick's black-box scanner probes for type confusion through its Input Validation and Property Authorization checks, which are part of its 12 parallel security tests. The scanner:

  1. Sends Mismatched Types: For parameters defined as integer in the OpenAPI spec, middleBrick submits string values (e.g., "page": "abc"), non-numeric strings, and floating-point numbers.
  2. Observes Behavioral Changes: It analyzes responses for:
    • HTTP 200 with data that should be restricted (indicating authorization bypass).
    • HTTP 500 errors from unhandled type errors (information leakage).
    • Differential responses between typed and mistyped probes (e.g., different record counts).
  3. Cross-References OpenAPI Specs: middleBrick resolves $ref in Swagger/OpenAPI definitions to identify expected types, then tests runtime tolerance for type violations.

Example scan using middleBrick's CLI:

# Scan a Flask API endpoint
middlebrick scan https://api.example.com/v1/users

The report maps findings to OWASP API Top 10 categories (e.g., Broken Object Property Level Authorization for bypasses, Security Misconfiguration for missing validation). Each finding includes the parameter name, sent payload, observed response, and severity.

Flask-Specific Remediation

1. Enforce Types for Query/Form Parameters

Use the type argument in request.args.get() and request.form.get():

@app.route('/api/orders')
def list_orders():
    # Safe: converts to integer or returns None
    page = request.args.get('page', type=int, default=1)
    limit = request.args.get('limit', type=int, default=50)
    if page < 1 or limit < 1:
        abort(400, "Invalid pagination")
    # Now page and limit are integers
    return get_orders(page, limit)

type=int rejects non-integer strings (e.g., "abc") and returns None, allowing explicit validation.

2. Validate JSON Payloads with Explicit Casting

Flask does not auto-convert JSON values. Validate manually or with a schema library. Native approach:

@app.route('/api/user', methods=['POST'])
def update_user():
    data = request.get_json(silent=True) or {}
    # Explicit type checks
    if 'age' in data and not isinstance(data['age'], int):
        abort(400, "'age' must be integer")
    if 'active' in data and not isinstance(data['active'], bool):
        abort(400, "'active' must be boolean")
    # Safe to use typed values
    user = User.query.get(current_user.id)
    user.age = data.get('age', user.age)
    user.active = data.get('active', user.active)
    db.session.commit()
    return jsonify(user.to_dict())

For complex APIs, integrate marshmallow or pydantic:

from marshmallow import Schema, fields

class UserSchema(Schema):
    age = fields.Int(required=True)
    active = fields.Bool(required=True)

schema = UserSchema()

@app.route('/api/user', methods=['POST'])
def update_user():
    errors = schema.validate(request.get_json())
    if errors:
        return jsonify(errors), 400
    # validated_data has correct types
    data = schema.load(request.get_json())
    ...

3. Use Route Converters for Path Parameters

Flask's built-in converters (<int:id>, <float:price>) ensure path parameters are typed before view execution:

@app.route('/api/product/<int:product_id>')
def get_product(product_id):  # product_id is integer
    product = Product.query.get_or_404(product_id)
    return jsonify(product.to_dict())

4. Avoid String Interpolation in Queries

Never concatenate request values into SQL/ORM queries. Use parameterized statements:

# UNSAFE:
query = f"SELECT * FROM users WHERE id = {user_id}"

# SAFE (SQLAlchemy):
user = User.query.filter_by(id=user_id).first()

# SAFE (raw SQL with params):
result = db.session.execute(text("SELECT * FROM users WHERE id = :id"), {'id': user_id})

5. Normalize Boolean Parameters

For query flags like ?active=true, normalize to boolean:

def str_to_bool(value):
    if isinstance(value, bool):
        return value
    if isinstance(value, str):
        return value.lower() in ('true', '1', 'yes')
    return bool(value)

active = str_to_bool(request.args.get('active', False))

These practices ensure Flask handles types consistently, eliminating confusion that leads to authorization bypasses or injection.

Related CWEs: inputValidation

CWE IDNameSeverity
CWE-20Improper Input Validation HIGH
CWE-22Path Traversal HIGH
CWE-74Injection CRITICAL
CWE-77Command Injection CRITICAL
CWE-78OS Command Injection CRITICAL
CWE-79Cross-site Scripting (XSS) HIGH
CWE-89SQL Injection CRITICAL
CWE-90LDAP Injection HIGH
CWE-91XML Injection HIGH
CWE-94Code Injection CRITICAL

Frequently Asked Questions

Can type confusion in Flask lead to SQL injection?
Yes. If request parameters (strings) are interpolated into SQL queries without casting, an attacker can supply non-integer values that alter query logic. For example, a string "1 OR 1=1" in an integer parameter can turn WHERE id = {value} into WHERE id = 1 OR 1=1. Always use parameterized queries and validate types.
How does middleBrick detect type confusion without source code access?
middleBrick sends probe requests with type-mismatched payloads (e.g., strings for integer parameters) and analyzes responses. If an API returns HTTP 200 with unauthorized data or exhibits differential behavior versus correctly typed requests, it flags a potential type confusion vulnerability under Input Validation or Property Authorization. The scanner cross-references OpenAPI specs to know expected types.