HIGH bola idorstrapibearer tokens

Bola Idor in Strapi with Bearer Tokens

Bola Idor in Strapi with Bearer Tokens — how this specific combination creates or exposes the vulnerability

Broken Object Level Authorization (BOLA) occurs when an API exposes one user’s resource through an ID (or other key) without verifying that the requesting user is authorized to access that specific resource. Strapi, by default, provides a powerful admin API and a public API. When Bearer Tokens are used for authentication, a BOLA vulnerability can arise if token validation is performed but ownership or scope checks are omitted on individual record endpoints.

Consider a Strapi backend exposing a REST or GraphQL endpoint such as /api/users/:id or /api/orders/:orderId. If the API validates the presence of a valid Bearer Token (via an Authorization header like Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6...) but does not confirm that the resource identified by :id belongs to the subject represented by the token’s payload (e.g., a user ID or tenant ID), an authenticated attacker can manipulate the ID to access or modify other users’ data. This is a classic BOLA scenario: authentication is present, but authorization is incomplete.

In Strapi, this often maps to the following conditions:

  • Authentication via Bearer Token: Strapi plugins or custom policies validate the token and attach a user object to the request. This is correctly implemented when the token is verified and a user record is loaded.
  • Missing ownership or tenant scoping: The endpoint does not enforce that, for example, a userId in the token matches the id in the URL, or that a content entry belongs to the user’s organization.
  • Excessive data exposure: Without proper checks, responses may return sensitive fields such as email, password hashes, or internal flags for resources the caller should not see.

Real-world examples include endpoints like GET /api/profiles/:profileId where the profile’s user field is not compared to the authenticated user extracted from the Bearer Token, or GraphQL queries that fetch records by ID without a resolver-level scope check. Attack patterns include IDOR enumeration, unauthorized read/write, and data exfiltration. This intersects with the broader category of BOLA/IDOR in middleBrick’s 12 security checks, which specifically tests for missing resource-level authorization even when authentication mechanisms like Bearer Tokens are in place.

Bearer Tokens-Specific Remediation in Strapi — concrete code fixes

Remediation focuses on ensuring that every request that accesses or modifies a resource validates not only the token, but also that the resource belongs to the authenticated subject. Below are concrete, syntactically correct examples for Strapi v4 (the current major line) using policies and controllers, including Bearer Token handling.

1. Policy that attaches user from Bearer Token

Create a custom policy src/policies/validate-user.js that extracts and verifies the Bearer Token (if using JWT) and ensures the user exists. Strapi’s auth plugins can also be leveraged, but a custom policy gives explicit control.

// src/policies/validate-user.js
'use strict';

/**
 * Policy to validate Bearer Token and attach user to request.
 * Assumes JWT payload contains a `userId` or `id`.
 */
module.exports = async (ctx, next) => {
  const authHeader = ctx.request.header['authorization'] || '';
  const token = authHeader.startsWith('Bearer ') ? authHeader.slice(7) : null;

  if (!token) {
    ctx.status = 401;
    ctx.body = { error: 'Unauthorized', message: 'Missing Bearer Token' };
    return;
  }

  try {
    // Replace with your actual JWT verification logic or service
    const payload = verifyJwt(token, process.env.JWT_SECRET);
    const user = await strapi.entityService.findOne('api::user.user', payload.userId);
    if (!user) {
      ctx.status = 401;
      ctx.body = { error: 'Unauthorized', message: 'Invalid token' };
      return;
    }
    ctx.state.user = user;
    await next();
  } catch (err) {
    ctx.status = 401;
    ctx.body = { error: 'Unauthorized', message: 'Invalid or expired token' };
  }
};

// Helper JWT verification (example using jsonwebtoken)
function verifyJwt(token, secret) {
  const jwt = require('jsonwebtoken');
  return jwt.verify(token, secret);
}

2. Controller enforcing ownership on record access

In your controller, compare the authenticated user’s ID with the resource’s user reference before returning or modifying data.

// src/api/order/controllers/order.js
'use strict';

module.exports = {
  async find(ctx) {
    const { user } = ctx.state; // attached by policy
    const { id } = ctx.params;

    const order = await strapi.db.query('api::order.order').findOne({
      where: { id, user: user.id }, // enforce ownership
    });

    if (!order) {
      ctx.status = 403;
      ctx.body = { error: 'Forbidden', message: 'You do not have access to this order' };
      return;
    }

    ctx.body = order;
  },
  async update(ctx) {
    const { user } = ctx.state;
    const { id } = ctx.params;

    const order = await strapi.db.query('api::order.order').findOne({
      where: { id, user: user.id },
    });

    if (!order) {
      ctx.status = 403;
      ctx.body = { error: 'Forbidden', message: 'You cannot update this order' };
      return;
    }

    const updated = await strapi.db.query('api::order.order').update({ where: { id }, data: ctx.request.body });
    ctx.body = updated;
  },
};

3. GraphQL resolver scoping

If using GraphQL, scope the resolver to ensure the record’s user matches the authenticated user.

// src/api/order/resolvers.js
'use strict';

module.exports = {
  Query: {
    order: async (_, { id }, { state }) => {
      const { user } = state;
      const order = await strapi.db.query('api::order.order').findOne({
        where: { id, user: user.id },
        populate: ['products'],
      });
      if (!order) {
        throw new Error('Order not found or access denied');
      }
      return order;
    },
  },
};

4. Using middleware for global scoping (optional)

For REST endpoints, Strapi middlewares can enforce tenant or user scoping globally for a route prefix, reducing repetitive checks.

// src/middlewares/scope-by-user/index.js
'use strict';

module.exports = (config, { strapi }) => {
  return async (ctx, next) => {
    const authHeader = ctx.request.header['authorization'] || '';
    const token = authHeader.startsWith('Bearer ') ? authHeader.slice(7) : null;
    if (token) {
      try {
        const payload = verifyJwt(token, process.env.JWT_SECRET);
        ctx.state.user = { id: payload.userId };
      } catch (err) {
        // allow public endpoints to proceed; handled by controller/policy
      }
    }
    await next();
  };
};

These patterns ensure that Bearer Token authentication is paired with explicit ownership checks, effectively mitigating BOLA risks in Strapi APIs. The examples include realistic error handling, use of Strapi’s entity service, and alignment with common authentication libraries.

Related CWEs: bolaAuthorization

CWE IDNameSeverity
CWE-250Execution with Unnecessary Privileges HIGH
CWE-639Insecure Direct Object Reference CRITICAL
CWE-732Incorrect Permission Assignment HIGH

Frequently Asked Questions

How can I test if my Strapi endpoints are vulnerable to BOLA with Bearer Tokens?
Use an authenticated request with a valid Bearer Token, then modify the resource ID (e.g., increment :id) and observe whether you can access another user’s data. middleBrick’s BOLA checks include this pattern and will flag missing ownership validation even when tokens are accepted.
Does enabling role-based permissions in Strapi fully prevent BOLA with Bearer Tokens?
Role-based permissions restrict what operations a role can perform but do not automatically scope records to a user. You must explicitly enforce ownership checks in policies, controllers, or resolvers to prevent BOLA, even when roles are configured.