SD
Sma DasSecurity Engineer
Sma Das Signature

Cybersecurity professional writing about security research, programming, and technology.

hello@sma-das.com

Pages

  • About
  • Blogs
  • Contact

Topics

  • Cybersecurity
  • Programming
  • Malware Analysis

Connect

  • LinkedIn
  • GitHub
  • Email

© 2026 Sma Das. All rights reserved.

Privacy PolicyTerms of Use
SD
Sma DasSecurity Engineer

Sma Das Signature

Cybersecurity professional writing about security research, programming, and technology.

hello@sma-das.com

Pages

  • About
  • Blogs
  • Contact

Topics

  • Cybersecurity
  • Programming
  • Malware Analysis

Connect

  • LinkedIn
  • GitHub
  • Email

© 2026 Sma Das. All rights reserved.

Privacy PolicyTerms of Use
SD
Sma DasSecurity Engineer
Back to blog

10 API Security Mistakes I See Over and Over Again

SD
Sma Das•Monday, December 15, 2025
cybersecurityprogrammingweb-securityapi
10 API Security Mistakes I See Over and Over Again

Share

Share

Sma Das Signature

Cybersecurity professional writing about security research, programming, and technology.

hello@sma-das.com

Pages

  • About
  • Blogs
  • Contact

Topics

  • Cybersecurity
  • Programming
  • Malware Analysis

Connect

  • LinkedIn
  • GitHub
  • Email

© 2026 Sma Das. All rights reserved.

Privacy PolicyTerms of Use

Table of Contents

Introduction

APIs are the backbone of modern applications. They power everything from mobile apps to microservices architectures. But with great power comes great responsibility—and unfortunately, great vulnerability.

After conducting security reviews on hundreds of APIs across various industries, I've noticed the same mistakes appearing repeatedly. This article documents the top 10, complete with examples and remediation strategies.

1. Broken Object Level Authorization (BOLA)

This is the #1 API vulnerability according to OWASP, and for good reason. It's everywhere.

The Problem

GET /api/v1/users/12345/orders HTTP/1.1
Authorization: Bearer <user_token>

If user 12345's orders are returned regardless of which user's token is provided, you have BOLA.

Real-World Example

# Vulnerable code
@app.route('/api/orders/<order_id>')
@require_auth
def get_order(order_id):
    order = Order.query.get(order_id)
    return jsonify(order.to_dict())  # No ownership check!

The Fix

# Fixed code
@app.route('/api/orders/<order_id>')
@require_auth
def get_order(order_id):
    order = Order.query.get(order_id)
    if order.user_id != current_user.id:
        abort(403)
    return jsonify(order.to_dict())

2. Excessive Data Exposure

APIs often return entire objects when the client only needs a few fields.

The Problem

// Request: GET /api/users/me
// Response includes WAY too much:
{
  "id": 12345,
  "email": "user@example.com",
  "password_hash": "$2b$12$...",
  "ssn": "123-45-6789",
  "internal_notes": "High-value customer",
  "api_keys": ["sk_live_..."],
  "role": "user",
  "permissions": ["read", "write"]
}

The Fix

Create response DTOs that only include necessary fields:

class UserPublicSchema(Schema):
    id = fields.Int()
    email = fields.Email()
    name = fields.Str()
    # Only public fields!

@app.route('/api/users/me')
@require_auth
def get_current_user():
    return UserPublicSchema().dump(current_user)

3. Lack of Rate Limiting

Without rate limiting, APIs are vulnerable to:

  • Brute force attacks
  • Denial of service
  • Resource exhaustion
  • Credential stuffing

Simple Rate Limiting Implementation

from flask_limiter import Limiter
from flask_limiter.util import get_remote_address

limiter = Limiter(
    app,
    key_func=get_remote_address,
    default_limits=["100 per minute"]
)

@app.route('/api/login', methods=['POST'])
@limiter.limit("5 per minute")  # Stricter for auth endpoints
def login():
    # Authentication logic
    pass

@app.route('/api/password-reset', methods=['POST'])
@limiter.limit("3 per hour")  # Very strict for sensitive operations
def password_reset():
    # Reset logic
    pass

Rate Limiting Headers

Always inform clients of their limits:

HTTP/1.1 200 OK
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 95
X-RateLimit-Reset: 1640000000

4. Broken Authentication

I see these authentication mistakes constantly:

Weak Token Generation

# WRONG - predictable tokens
token = base64.b64encode(f"{user_id}:{timestamp}".encode())

# RIGHT - cryptographically secure
token = secrets.token_urlsafe(32)

Missing Token Expiration

# JWT without expiration - DANGEROUS
jwt.encode({"user_id": 123}, SECRET)

# JWT with proper expiration
jwt.encode({
    "user_id": 123,
    "exp": datetime.utcnow() + timedelta(hours=1),
    "iat": datetime.utcnow()
}, SECRET)

Token in URL

# WRONG - tokens in URL are logged everywhere
GET /api/data?token=abc123

# RIGHT - tokens in headers
GET /api/data
Authorization: Bearer abc123

5. SQL Injection (Yes, Still)

Despite being well-known, SQL injection still appears in APIs:

The Problem

@app.route('/api/search')
def search():
    query = request.args.get('q')
    # VULNERABLE!
    results = db.execute(f"SELECT * FROM products WHERE name LIKE '%{query}%'")
    return jsonify(results)

The Fix

@app.route('/api/search')
def search():
    query = request.args.get('q')
    # Parameterized query - safe
    results = db.execute(
        "SELECT * FROM products WHERE name LIKE :query",
        {"query": f"%{query}%"}
    )
    return jsonify(results)

6. Mass Assignment

Accepting all client-provided fields can lead to privilege escalation:

The Problem

@app.route('/api/users/<id>', methods=['PUT'])
@require_auth
def update_user(id):
    user = User.query.get(id)
    # DANGEROUS - accepts ANY field from request
    for key, value in request.json.items():
        setattr(user, key, value)  # Including 'role' and 'is_admin'!
    db.session.commit()

The Fix

ALLOWED_UPDATE_FIELDS = {'name', 'email', 'bio', 'avatar_url'}

@app.route('/api/users/<id>', methods=['PUT'])
@require_auth
def update_user(id):
    user = User.query.get(id)
    
    for key, value in request.json.items():
        if key in ALLOWED_UPDATE_FIELDS:
            setattr(user, key, value)
    
    db.session.commit()

7. Improper Error Handling

Error messages often leak sensitive information:

The Problem

// Too detailed error response
{
  "error": "DatabaseError",
  "message": "Connection to postgres://admin:password123@db.internal:5432/prod failed",
  "stack_trace": "..."
}

The Fix

@app.errorhandler(Exception)
def handle_exception(e):
    # Log the full error internally
    app.logger.error(f"Unhandled exception: {e}", exc_info=True)
    
    # Return generic message to client
    return jsonify({
        "error": "internal_server_error",
        "message": "An unexpected error occurred"
    }), 500

8. Missing Security Headers

APIs often forget important security headers:

@app.after_request
def add_security_headers(response):
    response.headers['X-Content-Type-Options'] = 'nosniff'
    response.headers['X-Frame-Options'] = 'DENY'
    response.headers['Content-Security-Policy'] = "default-src 'none'"
    response.headers['Cache-Control'] = 'no-store'
    response.headers['Pragma'] = 'no-cache'
    return response

9. Insecure Direct Object References

Beyond BOLA, IDORs can appear in subtle ways:

File Access

# VULNERABLE
@app.route('/api/files/<filename>')
def get_file(filename):
    return send_file(f'/uploads/{filename}')  # Path traversal possible!

# FIXED
@app.route('/api/files/<filename>')
def get_file(filename):
    # Validate filename
    if '..' in filename or filename.startswith('/'):
        abort(400)
    
    safe_path = os.path.join('/uploads', secure_filename(filename))
    if not safe_path.startswith('/uploads/'):
        abort(400)
    
    return send_file(safe_path)

10. Lack of Input Validation

APIs must validate all input rigorously:

Schema Validation Example

from marshmallow import Schema, fields, validate, ValidationError

class CreateOrderSchema(Schema):
    product_id = fields.Int(required=True, validate=validate.Range(min=1))
    quantity = fields.Int(required=True, validate=validate.Range(min=1, max=100))
    shipping_address = fields.Nested(AddressSchema, required=True)
    notes = fields.Str(validate=validate.Length(max=500))

@app.route('/api/orders', methods=['POST'])
@require_auth
def create_order():
    try:
        data = CreateOrderSchema().load(request.json)
    except ValidationError as e:
        return jsonify({"errors": e.messages}), 400
    
    # Process validated data
    order = Order.create(**data)
    return jsonify(order.to_dict()), 201

Security Checklist

Before deploying any API, verify:

CategoryCheck
AuthenticationJWT properly validated, tokens expire
AuthorizationObject-level access controls in place
InputAll input validated and sanitized
OutputNo sensitive data leakage
Rate LimitingImplemented on all endpoints
LoggingSecurity events logged (not sensitive data)
HTTPSTLS 1.2+ required, HSTS enabled
HeadersSecurity headers configured

Conclusion

API security isn't rocket science, but it requires diligence. The mistakes outlined here are preventable with proper awareness and secure coding practices.

My recommendation: Implement security reviews as part of your development process, not as an afterthought. Automated tools can catch many of these issues, but human review is essential for logic flaws like BOLA.


Want a security review of your API? Reach out through the contact page to discuss.