Mass Assignment in Flask with Api Keys
Mass Assignment in Flask with Api Keys — how this specific combination creates or exposes the vulnerability
Mass Assignment occurs when a Flask endpoint binds incoming request data directly to a model or object without explicit allowlisting of fields. Combining this pattern with API key handling can unintentionally expose or modify sensitive attributes. For example, if a request JSON is passed to a model constructor or model.update() without restriction, a client could supply extra keys such as is_admin, role, or api_key and escalate privileges or overwrite critical configuration.
Consider a typical pattern where an API key is expected in an HTTP header, but the endpoint also merges JSON body data into a database record:
from flask import Flask, request, jsonify
from flask_sqlalchemy import SQLAlchemy
app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///test.db'
db = SQLAlchemy(app)
class ServiceConfig(db.Model):
id = db.Column(db.Integer, primary_key=True)
api_key = db.Column(db.String(64), nullable=False)
rate_limit = db.Column(db.Integer, default=100)
debug_mode = db.Column(db.Boolean, default=False)
@app.route('/config', methods=['POST'])
def update_config():
data = request.get_json()
config = ServiceConfig.query.first_or_404()
# Risky: mass assignment without field filtering
for key, value in data.items():
setattr(config, key, value)
db.session.commit()
return jsonify({'status': 'updated'})
In the example above, an API key sent in the header can be used to authenticate the request, but the JSON body is mass-assigned. An attacker who obtains or guesses a valid API key could send {"debug_mode": true, "rate_limit": 999999} and alter behavior or leak data. Even if the API key is rotated, over-permissive assignment remains a vector for unintended state changes.
Another scenario involves user registration or token refresh endpoints where an API key is generated and returned. If the creation logic uses mass assignment from user-controlled JSON, an attacker might supply fields like key_type or permissions to produce a more privileged key:
from flask import request
from itsdangerous import TimedJSONWebSignatureSerializer as Serializer
def generate_api_key(user_id, permissions):
s = Serializer(app.config['SECRET_KEY'], expires_in=3600)
return s.dumps({'user_id': user_id, 'permissions': permissions}).decode('utf-8')
@app.route('/token', methods=['POST'])
def issue_token():
data = request.get_json()
# Risky: trusting all incoming keys
key = generate_api_key(data['user_id'], data.get('permissions', 'read'))
return jsonify({'api_key': key})
Here, if permissions is not strictly validated, an attacker can self-assign administrative capabilities. The presence of an API key does not mitigate insecure object creation; it only authenticates the caller. Therefore, explicit field filtering and schema validation are essential when API keys are used for authentication but request data still drives object state.
Api Keys-Specific Remediation in Flask — concrete code fixes
To secure Flask endpoints that use API keys, avoid mass assignment by explicitly extracting and validating only the fields you intend to update. Use structured validation with libraries such as Marshmallow or Pydantic, and ensure API keys themselves are never user-controlled mutable fields.
Safe update pattern with allowlisting:
from flask import Flask, request, jsonify
from flask_sqlalchemy import SQLAlchemy
app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///test.db'
db = SQLAlchemy(app)
class ServiceConfig(db.Model):
id = db.Column(db.Integer, primary_key=True)
api_key = db.Column(db.String(64), nullable=False)
rate_limit = db.Column(db.Integer, default=100)
debug_mode = db.Column(db.Boolean, default=False)
ALLOWED_FIELDS = {'rate_limit', 'debug_mode'}
@app.route('/config', methods=['POST'])
def update_config():
data = request.get_json()
config = ServiceConfig.query.first_or_404()
for key, value in data.items():
if key in ALLOWED_FIELDS:
setattr(config, key, value)
else:
# Optionally log or ignore unexpected fields
pass
db.session.commit()
return jsonify({'status': 'updated'})
When issuing API keys, keep key generation server-side and do not accept key metadata from the client:
from flask import Flask, request, jsonify
from itsdangerful import TimedJSONWebSignatureSerializer as Serializer
import secrets
app = Flask(__name__)
app.config['SECRET_KEY'] = secrets.token_hex(32)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///keys.db'
def generate_api_key(user_id, scope):
s = Serializer(app.config['SECRET_KEY'], expires_in=3600)
return s.dumps({'user_id': user_id, 'scope': scope}).decode('utf-8')
@app.route('/keys', methods=['POST'])
def issue_key():
data = request.get_json()
# Validate user_id and scope server-side; do not trust client-supplied key attributes
user_id = data['user_id']
scope = data.get('scope', 'read')
if scope not in {'read', 'write', 'admin'}:
return jsonify({'error': 'invalid scope'}), 400
key = generate_api_key(user_id, scope)
return jsonify({'api_key': key})
For request bodies that map to models, prefer explicit schemas. With Marshmallow:
from flask import Flask, request, jsonify
from flask_sqlalchemy import SQLAlchemy
from marshmallow import Schema, fields, ValidationError
app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///test.db'
db = SQLAlchemy(app)
class ServiceConfig(db.Model):
id = db.Column(db.Integer, primary_key=True)
api_key = db.Column(db.String(64), nullable=False)
rate_limit = db.Column(db.Integer, default=100)
debug_mode = db.Column(db.Boolean, default=False)
class ConfigSchema(Schema):
rate_limit = fields.Int(required=False)
debug_mode = fields.Bool(required=False)
schema = ConfigSchema()
@app.route('/config', methods=['POST'])
def update_config():
data = request.get_json()
try:
updates = schema.load(data)
except ValidationError as err:
return jsonify({'errors': err.messages}), 400
config = ServiceConfig.query.first_or_404()
for key, value in updates.items():
setattr(config, key, value)
db.session.commit()
return jsonify({'status': 'updated'})Related CWEs: propertyAuthorization
| CWE ID | Name | Severity |
|---|---|---|
| CWE-915 | Mass Assignment | HIGH |