HIGH mass assignmentflaskapi keys

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 IDNameSeverity
CWE-915Mass Assignment HIGH

Frequently Asked Questions

Can API keys alone prevent mass assignment in Flask endpoints?
No. API keys authenticate the caller but do not restrict which fields can be modified. Without explicit field allowlisting or schema validation, mass assignment risks remain regardless of how the request is authenticated.
Should I accept client-supplied field lists for updates if I use API keys?
Avoid accepting client-defined field mappings for updates. Use a server-side allowlist or a validated schema (e.g., Marshmallow or Pydantic) to control which properties can be set, treating API keys only as authentication credentials.