Fixing Common Pentest Findings in Web APIs

In this article, we cover how to identify and remediate the most common penetration testing findings impacting web APIs. You will learn practical strategies to fix critical vulnerabilities like Broken Object-Level Authorization (BOLA) and prevent excessive data exposure, along with best practices for robust API security. We provide concrete code examples and discuss production readiness considerations.

Ozan Kılıç

11 min read
0

/

Fixing Common Pentest Findings in Web APIs

Fixing Common Pentest Findings in Web APIs


Most teams prioritize API functionality and speed to market. But this often leads to significant security vulnerabilities at scale, exposing sensitive data or enabling unauthorized actions. As a penetration tester, I consistently find critical flaws in web APIs that could have been prevented with fundamental security practices.


TL;DR


  • Broken Object-Level Authorization (BOLA) remains a top threat, allowing unauthorized access to resources by manipulating object IDs.

  • Excessive data exposure is prevalent, with APIs often returning more information than strictly necessary for the client.

  • Implement granular, attribute-based access control checks at every API endpoint accessing specific resources.

  • Rigorously filter and sanitize all API responses, returning only the essential data required by the client's context.

  • Proactive security design and thorough validation are more effective than reactive patching after a breach or pentest.


The Problem


In 2026, web APIs form the backbone of modern applications, making their security paramount. Yet, during penetration tests, it's not uncommon to uncover vulnerabilities that bypass entire layers of application logic. I frequently encounter API endpoints designed with an implicit trust model, assuming that clients will only request data they are authorized to view or modify. This assumption often results in Critical or High severity findings. For instance, a common scenario involves an application where users can retrieve their own profile data using `/api/v1/users/{userid}`. If the application merely checks for an authenticated session without verifying if `userid` belongs to the requesting user, an attacker can enumerate and access any user's profile by simply incrementing or changing the ID. Such flaws, often categorized as Broken Object-Level Authorization (BOLA), consistently rank among the most critical issues, allowing unauthorized access to sensitive user data or system configurations. Teams commonly report 30-50% of critical findings in pentests stemming from insufficient authorization checks across their API surfaces.


How It Works: Mitigating API Security Vulnerabilities


Addressing the most common pentest findings in web APIs requires a shift from reactive security measures to a proactive, design-time approach. We'll focus on two pervasive issues: Broken Object-Level Authorization (BOLA) and Excessive Data Exposure. Understanding the mechanisms behind these vulnerabilities and their robust countermeasures is crucial for secure API development practices.


Strengthening Broken Object-Level Authorization (BOLA)


BOLA, also known as IDOR (Insecure Direct Object Reference), occurs when an API endpoint uses an identifier for an object but fails to perform sufficient authorization checks to ensure the requesting user has permission to interact with that specific object. Attackers can manipulate these identifiers (e.g., `userid`, `orderid`, `document_id`) to access or modify resources belonging to other users. The core issue lies in inadequate authorization logic at the resource level.


To fix BOLA, every API call that references an object must validate two key aspects:

  1. Authentication: Is the user logged in and who they claim to be?

  2. Authorization: Does the authenticated user have explicit permission to access this specific object? This check must consider ownership, role-based access control (RBAC), or attribute-based access control (ABAC).


Consider a Node.js (Express) example where a user wants to fetch a specific order.


// Vulnerable endpoint: Lacks object-level authorization check
app.get('/api/v1/orders/:orderId', async (req, res) => {
    const { orderId } = req.params;
    // Assuming req.user is populated after authentication middleware
    if (!req.user) {
        return res.status(401).send('Unauthorized');
    }
    // Fetches any order, only checking if *any* user is authenticated
    const order = await Order.findById(orderId);
    if (!order) {
        return res.status(404).send('Order not found');
    }
    res.json(order);
});


To remediate this, you must introduce a robust authorization check that ties the requested resource to the authenticated user's identity or permissions.


// Secure endpoint: Implements object-level authorization
app.get('/api/v1/orders/:orderId', async (req, res) => {
    const { orderId } = req.params;
    const userId = req.user.id; // User ID from authenticated session/token

    if (!userId) {
        return res.status(401).send('Unauthorized');
    }

    // Fetches the order and explicitly checks if it belongs to the authenticated user
    const order = await Order.findOne({ _id: orderId, userId: userId });

    if (!order) {
        // Return 404 to avoid information leakage about non-existent vs. unauthorized orders
        return res.status(404).send('Order not found or unauthorized');
    }
    res.json(order);
});

The critical change is the `userId: userId` clause in the `findOne` query. This ensures that the database only returns an order if its `userId` field matches the authenticated user's ID, thereby enforcing object-level authorization directly at the data access layer.


Preventing Excessive Data Exposure


Excessive data exposure occurs when an API sends back more data than the client explicitly requests or requires. This is a common finding, often resulting from developers querying an entire database record and then relying on the frontend to filter out sensitive fields. This approach is fundamentally flawed because the sensitive data is still transmitted over the network, making it vulnerable to interception, unintended logging, or exposure through client-side vulnerabilities. These are critical OWASP API Security Top 10 issues.


The fix involves explicitly defining what data is returned by each API endpoint. This means filtering sensitive attributes at the server-side, before the response payload is constructed.


Consider a user profile endpoint in a Python (Flask) API that returns all user details:


# Vulnerable endpoint: Returns all user data, including sensitive fields
@app.route('/api/v1/users/<user_id>', methods=['GET'])
def get_user_profile_vulnerable(user_id):
    user = User.query.get(user_id) # Fetches entire user object
    if not user:
        return jsonify({'message': 'User not found'}), 404
    # User object might contain password hashes, internal flags, etc.
    return jsonify(user.to_dict()), 200

The `user.to_dict()` method might serialize all database columns. To prevent excessive data exposure, the API must explicitly select and return only the necessary fields.


# Secure endpoint: Filters sensitive fields before sending
@app.route('/api/v1/users/<user_id>', methods=['GET'])
def get_user_profile_secure(user_id):
    user = User.query.get(user_id)
    if not user:
        return jsonify({'message': 'User not found'}), 404

    # Explicitly select non-sensitive fields to expose
    public_profile = {
        'id': user.id,
        'username': user.username,
        'email': user.email, # Ensure this is okay to expose based on context
        'createdAt': user.created_at.isoformat() # Convert datetime to string
    }
    return jsonify(public_profile), 200

This secure approach constructs a new dictionary containing only the fields deemed safe and necessary for public consumption. This ensures that sensitive internal data, like password hashes or internal API keys, never leave the server.


Step-by-Step Implementation: Securing a User Profile API


Let's walk through securing a simple user profile API, combining both BOLA and excessive data exposure mitigations using Node.js and Express.


For this example, assume a `User` model with fields like `id`, `username`, `email`, `passwordHash`, `isAdmin`, and `lastLogin`. We want a user to fetch their own profile but only display `id`, `username`, and `email`. Admins should be able to fetch any user's profile, including `isAdmin` and `lastLogin`, but never `passwordHash`.


Step 1: Set up a basic Express application and mock database.


First, create a basic server structure and mock some user data.


// app.js
const express = require('express');
const app = express();
const PORT = 3000;

// Mock database for demonstration
const usersDB = [
    { id: 'user1', username: 'alice', email: 'alice@example.com', passwordHash: 'hash123', isAdmin: false, lastLogin: '2026-01-15T10:00:00Z' },
    { id: 'user2', username: 'bob', email: 'bob@example.com', passwordHash: 'hash456', isAdmin: false, lastLogin: '2026-01-14T09:30:00Z' },
    { id: 'user3', username: 'charlie_admin', email: 'charlie@example.com', passwordHash: 'hash789', isAdmin: true, lastLogin: '2026-01-15T11:00:00Z' }
];

// Simple authentication middleware (for demo purposes)
app.use((req, res, next) => {
    // Simulate an authenticated user based on a header
    const authHeader = req.headers['x-user-id'];
    if (authHeader) {
        req.user = usersDB.find(u => u.id === authHeader);
    }
    next();
});

app.listen(PORT, () => {
    console.log(`Server running on http://localhost:${PORT}`);
});


Step 2: Implement the secure profile retrieval endpoint.


We will create a single endpoint `/api/v1/profile/:userId` that handles both BOLA and excessive data exposure based on the requesting user's identity and roles.


// app.js (continued)

// Secure endpoint for user profile retrieval
app.get('/api/v1/profile/:userId', (req, res) => {
    const requestedUserId = req.params.userId;
    const authenticatedUser = req.user; // From our mock auth middleware

    if (!authenticatedUser) {
        return res.status(401).json({ message: 'Authentication required.' });
    }

    let targetUser = usersDB.find(u => u.id === requestedUserId);

    if (!targetUser) {
        return res.status(404).json({ message: 'User not found.' });
    }

    // BOLA Check:
    // A regular user can only access their own profile.
    // An admin user can access any profile.
    if (!authenticatedUser.isAdmin && authenticatedUser.id !== requestedUserId) {
        // Return 404 to avoid leaking information about unauthorized vs. non-existent users
        return res.status(404).json({ message: 'User not found or unauthorized.' });
    }

    // Excessive Data Exposure Check:
    // Define what fields are publicly visible (for self-access or non-admin viewing)
    const publicProfile = {
        id: targetUser.id,
        username: targetUser.username,
        email: targetUser.email,
    };

    // If the authenticated user is an admin, or accessing their own profile,
    // they might see more details, but passwordHash is always excluded.
    if (authenticatedUser.isAdmin || authenticatedUser.id === requestedUserId) {
        // Add more fields for admins or self-access, still filtering sensitive ones
        if (targetUser.isAdmin !== undefined) publicProfile.isAdmin = targetUser.isAdmin;
        if (targetUser.lastLogin) publicProfile.lastLogin = targetUser.lastLogin;
    }

    res.json(publicProfile);
});


Expected Output (using curl):


  • Scenario 1: Authenticated as 'alice', requesting 'alice's profile.

```bash

$ curl -H "X-User-Id: user1" http://localhost:3000/api/v1/profile/user1

```

```json

{

"id": "user1",

"username": "alice",

"email": "alice@example.com",

"isAdmin": false,

"lastLogin": "2026-01-15T10:00:00Z"

}

```

Observation: Alice gets her own profile, including isAdmin and lastLogin, but not passwordHash.


  • Scenario 2: Authenticated as 'alice', trying to request 'bob's profile.

```bash

$ curl -H "X-User-Id: user1" http://localhost:3000/api/v1/profile/user2

```

```json

{

"message": "User not found or unauthorized."

}

```

Observation: BOLA mitigation prevents Alice from seeing Bob's profile. The 404 status code (with a generic message) avoids leaking information.


  • Scenario 3: Authenticated as 'charlie_admin', requesting 'bob's profile.

```bash

$ curl -H "X-User-Id: user3" http://localhost:3000/api/v1/profile/user2

```

```json

{

"id": "user2",

"username": "bob",

"email": "bob@example.com",

"isAdmin": false,

"lastLogin": "2026-01-14T09:30:00Z"

}

```

Observation: Charlie (admin) can view Bob's profile, including `isAdmin` and `lastLogin`, but `passwordHash` is still filtered.


  • Scenario 4: Unauthenticated request.

```bash

$ curl http://localhost:3000/api/v1/profile/user1

```

```json

{

"message": "Authentication required."

}

```

Observation: The initial authentication check works as expected.


Common mistake: Relying solely on the client-side to filter or display data. If your API returns `passwordHash` and expects the frontend to ignore it, you have a vulnerability. The backend is the ultimate arbiter of data access and exposure. Always filter at the source.


Production Readiness


Securing APIs against common pentest findings is not a one-time task but an ongoing commitment. For production systems, consider these additional layers of defense and operational considerations.


Monitoring and Alerting: Implement robust logging for all API requests, including authorization failures and attempts to access unauthorized resources. Set up alerts for suspicious activity patterns, such as multiple consecutive BOLA attempts from a single IP or user, or an unusual volume of requests for sensitive endpoints. Tools like Splunk or Elastic Stack can aggregate logs and identify these anomalies.


Cost Implications: Implementing fine-grained authorization checks and data filtering can introduce a slight overhead, as each request might involve additional database queries or processing logic. However, the cost of a data breach far outweighs these marginal computational expenses. Optimize queries and caching strategies where performance bottlenecks emerge, but never compromise security for perceived minor gains in latency.


Security Hardening:

  • Rate Limiting: Protect against brute-force attacks and enumeration for authentication endpoints and BOLA attempts. A user trying to guess `orderId`s should be rate-limited.

  • Input Validation: Sanitize and validate all input parameters rigorously to prevent injection attacks (SQL, NoSQL, Command Injection) that could bypass authorization or expose data.

  • API Gateway: Utilize an API gateway (e.g., AWS API Gateway, Kong, Apigee) to enforce authentication, apply rate limits, and potentially perform basic input validation or policy enforcement before requests reach your backend services.

  • Automated Security Testing: Integrate SAST (Static Application Security Testing) and DAST (Dynamic Application Security Testing) tools into your CI/CD pipeline. SAST can catch common coding flaws that lead to BOLA or excessive data exposure during development, while DAST can identify runtime vulnerabilities.

  • Regular Penetration Testing: Schedule external penetration tests at least annually, and after significant architectural changes, to uncover subtle or complex vulnerabilities that automated tools might miss.


Edge Cases and Failure Modes:

  • Authentication Token Compromise: If an authentication token is compromised, a malicious actor could impersonate the legitimate user. Ensure tokens have short lifespans and implement robust revocation mechanisms.

  • Permission Matrix Complexity: As roles and permissions grow, the authorization logic can become complex. Use a clear, well-documented permission model (e.g., RBAC or ABAC) to prevent misconfigurations.

  • Performance Degradation: Overly complex authorization logic or excessive database lookups for every object access can degrade performance. Employ caching for authorization decisions where appropriate, but ensure cache invalidation is handled securely.

  • Version Control: Ensure API versions are properly managed. Old API versions might contain unpatched vulnerabilities, so deprecate and remove them responsibly, or ensure consistent security policies across all versions.


Summary & Key Takeaways


Securing web APIs from common pentest findings like Broken Object-Level Authorization (BOLA) and excessive data exposure demands a proactive, security-first mindset throughout the development lifecycle.


  • Implement granular authorization: Validate user permissions against every specific resource access. Never assume the client will request only what it's allowed.

  • Filter data at the source: Explicitly define and return only the data required by the client. Do not rely on client-side filtering to hide sensitive information.

  • Adopt defense-in-depth: Combine secure coding practices with API gateways, rate limiting, and robust input validation.

  • Automate security checks: Integrate SAST/DAST into your CI/CD and conduct regular penetration tests to catch issues early.

  • Monitor relentlessly: Log authorization failures and anomalous access patterns. Be prepared to detect and respond to attacks.

WRITTEN BY

Ozan Kılıç

Penetration tester, OSCP certified. Computer Engineering graduate, Hacettepe University. Writes on vulnerability analysis, penetration testing and SAST.Read more

Responses (0)

    Hottest authors

    View all

    Ahmet Çelik

    Lead Writer · ex-AWS Solutions Architect, 8 yrs · AWS, Terraform, K8s

    Alp Karahan

    Contributor · MongoDB certified, NoSQL specialist · MongoDB, DynamoDB

    Ayşe Tunç

    Lead Writer · Engineering Manager, ex-Meta, Google · System Design, Interviews

    Berk Avcı

    Lead Writer · Principal Backend Eng., API design · REST, GraphQL, gRPC

    Burak Arslan

    Managing Editor · Content strategy, developer marketing

    Cansu Yılmaz

    Lead Writer · Database Architect, 9 yrs Postgres · PostgreSQL, Indexing, Perf

    Popular posts

    View all
    Zeynep Aydın
    ·

    OIDC Implementation for B2B SaaS: Production Guide

    OIDC Implementation for B2B SaaS: Production Guide
    Zeynep Aydın
    ·

    Privacy Compliance: Build Delete & Export Flows Securely

    Privacy Compliance: Build Delete & Export Flows Securely
    Deniz Şahin
    ·

    GKE Autopilot vs Standard for Production in 2026

    GKE Autopilot vs Standard for Production in 2026
    Ahmet Çelik
    ·

    Kubernetes Cost Optimization for Backend Teams

    Kubernetes Cost Optimization for Backend Teams
    Zeynep Aydın
    ·

    OIDC vs OAuth 2.0: A Backend Engineer's Deep Dive

    OIDC vs OAuth 2.0: A Backend Engineer's Deep Dive
    Zeynep Aydın
    ·

    API Rate Limiting Strategies for Public APIs at Scale

    API Rate Limiting Strategies for Public APIs at Scale