HIGH bola idorexpressdynamodb

Bola Idor in Express with Dynamodb

Bola Idor in Express with Dynamodb — how this specific combination creates or exposes the vulnerability

Broken Object Level Authorization (BOLA) occurs when an API exposes object identifiers (IDs) without verifying that the requesting user is authorized to access the corresponding resource. In Express applications using DynamoDB as the persistence layer, this risk arises when route parameters such as :userId or :recordId are directly used to construct DynamoDB key expressions without ownership or scope checks.

Consider an Express route like /users/:userId/profile. If the handler uses userId from the URL to build a DynamoDB Key and fetch the item, an attacker can change :userId to another valid user ID and read or act on that user’s data. Because DynamoDB does not enforce user-level authorization, the database will return the item if the key exists, and it is up to the application to enforce authorization. Without explicit checks—such as confirming that the authenticated subject matches the requested ID or that the record belongs to an allowed tenant or group—the endpoint is vulnerable.

DynamoDB’s primary-key structure amplifies this: partition keys and sort keys often map directly to identifiers used in routes. If an API exposes a sort key like userId#settings and only validates that the key exists, an attacker can iterate through predictable sort keys (e.g., otherUser#settings) and access unauthorized configuration or sensitive data. Because DynamoDB queries are fast and the service does not inherently understand application-level permissions, the burden falls on the Express layer to enforce least privilege and scope restrictions.

Common root causes include missing ownership validation, over-permissive IAM policies for the DynamoDB credential used by the service, and route designs that trust client-supplied IDs. For example, an endpoint that deletes a record using DELETE /records/:recordId might build a DynamoDB Key from recordId alone, without confirming the record’s owner or access list. This maps directly to OWASP API Top 10 A01:2023 broken object level authorization and can lead to unauthorized read, modify, or delete actions across user boundaries.

In a typical DynamoDB data model, entities might use composite keys such as PK = USER#123 and SK = PROFILE#self. If the Express route extracts only the user ID portion from the PK and uses it as a lookup without verifying that the authenticated principal matches that ID, the authorization boundary collapses. Attackers can enumerate valid IDs or leverage known patterns to traverse other users’ objects. The risk is especially pronounced when APIs expose list endpoints or secondary indexes that broaden the search surface, enabling enumeration beyond the authenticated user’s scope.

To summarize, BOLA in Express with DynamoDB emerges when route identifiers are not scoped to the requesting user or tenant, when authorization checks are omitted or incomplete, and when the application trusts IDs supplied by the client. Because DynamoDB enforces only key-based access and does not perform contextual authorization, the application must implement robust, per-request ownership or role-based checks to prevent this class of vulnerability.

Dynamodb-Specific Remediation in Express — concrete code fixes

Remediation centers on ensuring that every DynamoDB access is constrained by the authenticated subject and, where applicable, tenant or group context. The following patterns assume an authenticated user identity available in req.user (e.g., from a session or JWT) and a DynamoDB DocumentClient configured as docClient.

Enforce ownership by composing keys with the subject

Instead of using the route parameter directly as the partition key, derive the key from the authenticated user. For example, for a profile endpoint:

app.get('/api/users/:userId/profile', async (req, res) => {
  const { userId } = req.params;
  const subjectId = req.user.sub; // authenticated subject

  if (userId !== subjectId) {
    return res.status(403).json({ error: 'Forbidden: cannot access another user profile' });
  }

  const params = {
    TableName: 'Users',
    Key: {
      PK: `USER#${subjectId}`, // use subject, not raw param
      SK: 'PROFILE#self'
    }
  };

  try {
    const { Item } = await docClient.get(params).promise();
    if (!Item) return res.status(404).json({ error: 'Not found' });
    res.json(Item);
  } catch (err) {
    res.status(500).json({ error: 'Internal server error' });
  }
});

Scope queries to tenant or group when applicable

If data is multi-tenant, include a tenant identifier in the key expression and validate it against the request’s context:

app.get('/api/projects/:projectId', async (req, res) => {
  const { projectId } = req.params;
  const subjectId = req.user.sub;
  const tenantId = req.user.tenantId;

  const params = {
    TableName: 'Projects',
    Key: {
      PK: `TENANT#${tenantId}`, // enforce tenant scope
      SK: `PROJECT#${projectId}`
    }
  };

  try {
    const { Item } = await docClient.get(params).promise();
    if (!Item) return res.status(404).json({ error: 'Not found' });
    // Optional: double-check that the user has access to this project
    if (!Item.memberIds.includes(subjectId)) {
      return res.status(403).json({ error: 'Forbidden: user not in project' });
    }
    res.json(Item);
  } catch (err) {
    res.status(500).json({ error: 'Internal server error' });
  }
});

Use conditional writes to prevent unauthorized updates

When updating or deleting, include ownership attributes in the condition expression:

app.delete('/api/records/:recordId', async (req, res) => {
  const { recordId } = req.params;
  const subjectId = req.user.sub;

  const params = {
    TableName: 'Records',
    Key: {
      PK: `USER#${subjectId}`,
      SK: `RECORD#${recordId}`
    },
    ConditionExpression: 'attribute_exists(#pk) AND #sk = :skVal',
    ExpressionAttributeNames: {
      '#pk': 'PK',
      '#sk': 'SK'
    },
    ExpressionAttributeValues: {
      ':skVal': `RECORD#${recordId}`
    }
  };

  try {
    await docClient.delete(params).promise();
    res.status(204).end();
  } catch (err) {
    if (err.code === 'ConditionalCheckFailedException') {
      return res.status(403).json({ error: 'Forbidden: record not owned or does not exist' });
    }
    res.status(500).json({ error: 'Internal server error' });
  }
});

Validate and normalize IDs to avoid injection via key composition

Ensure that user-supplied IDs cannot alter the structure of your keys. Treat route parameters as opaque identifiers and map them to canonical keys server-side:

function normalizeRecordId(input) {
  // Basic validation: allow only safe characters
  if (!/^[a-zA-Z0-9_-]+$/.test(input)) {
    throw new Error('Invalid record ID');
  }
  return input;
}

app.get('/api/records/:recordId', async (req, res) => {
  const rawId = normalizeRecordId(req.params.recordId);
  const subjectId = req.user.sub;

  const params = {
    TableName: 'Records',
    IndexName: 'GSI_Owner', // if querying by owner
    KeyConditionExpression: 'PK = :pk AND begins_with(SK, :skPrefix)',
    ExpressionAttributeValues: {
      ':pk': `USER#${subjectId}`,
      ':skPrefix': `RECORD#${rawId}`
    }
  };

  try {
    const { Items } = await docClient.query(params).promise();
    if (!Items || Items.length === 0) {
      return res.status(404).json({ error: 'Not found' });
    }
    res.json(Items[0]);
  } catch (err) {
    res.status(500).json({ error: 'Internal server error' });
  }
});

Least-privilege DynamoDB permissions

Configure the IAM role or user for your Express service to allow only necessary actions on specific resource ARNs. For example, scope down to per-user tables or use condition keys such as dynamodb:LeadingKeys to ensure that requests can only access items where the partition key equals the authenticated user’s ID:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "dynamodb:GetItem",
        "dynamodb:Query"
      ],
      "Resource": "arn:aws:dynamodb:region:account:table/Users",
      "Condition": {
        "ForAllValues:StringEquals": {
          "dynamodb:LeadingKeys": ["${cognito-identity.amazonaws.com:sub}"]
        }
      }
    {
      "Effect": "Allow",
      "Action": [
        "dynamodb:GetItem",
        "dynamodb:Query"
      ],
      "Resource": "arn:aws:dynamodb:region:account:table/Projects",
      "Condition": {
        "dynamodb:LeadingKeys": ["${cognito-identity.amazonaws.com:sub}"]
      }
    }
  ]
}

By combining ownership checks, tenant scoping, careful key design, and least-privilege IAM, you mitigate BOLA in Express applications backed by DynamoDB. These practices ensure that object-level access is always validated against the authenticated subject and its context before any database operation.

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

Can BOLA still happen if I use fine-grained IAM policies on DynamoDB?
Yes. IAM policies control what actions a credential can perform and on which resources, but they do not substitute for per-request authorization in your Express code. An attacker who compromises a credential or exploits a route that trusts client-supplied IDs can still traverse objects unless your application validates ownership or scope for every request.
How do I handle shared resources, such as team-based access, without creating BOLA risks?
Implement scoped access checks: include a team or tenant identifier in DynamoDB keys (e.g., TEAM#{teamId}) and validate that the authenticated user belongs to that team before performing operations. Use index structures like GSIs to query team memberships safely, and avoid exposing raw IDs that do not incorporate tenant or group context.