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.argsandrequest.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 becomeintorfloat, 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:
- 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.
- 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.
- Logic Errors in Pagination/Filtering: Non-integer strings in
LIMIT/OFFSETclauses 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 recordsEven with parameterized queries, some databases (e.g., PostgreSQL) will error if binding a string to an integer parameter, potentially causing denial-of-service.
- Boolean Parameter Misinterpretation: Flask treats any non-missing value as
Truein 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(), andrequest.jsonaccesses 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=intinget()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:
- Sends Mismatched Types: For parameters defined as
integerin the OpenAPI spec, middleBrick submits string values (e.g.,"page": "abc"), non-numeric strings, and floating-point numbers. - 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).
- Cross-References OpenAPI Specs: middleBrick resolves
$refin 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/usersThe 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 ID | Name | Severity |
|---|---|---|
| CWE-20 | Improper Input Validation | HIGH |
| CWE-22 | Path Traversal | HIGH |
| CWE-74 | Injection | CRITICAL |
| CWE-77 | Command Injection | CRITICAL |
| CWE-78 | OS Command Injection | CRITICAL |
| CWE-79 | Cross-site Scripting (XSS) | HIGH |
| CWE-89 | SQL Injection | CRITICAL |
| CWE-90 | LDAP Injection | HIGH |
| CWE-91 | XML Injection | HIGH |
| CWE-94 | Code Injection | CRITICAL |
Frequently Asked Questions
Can type confusion in Flask lead to SQL injection?
"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.