Skip to main content
Esprit CLI detects access control failures including IDOR (Insecure Direct Object References), BOLA (Broken Object-Level Authorization), and BFLA (Broken Function-Level Authorization). These vulnerabilities lead to cross-account data exposure and unauthorized state changes.
Treat every object reference and privileged action as untrusted until proven bound to the caller on every request.

Vulnerability Types

IDOR / BOLA (Object-Level)

  • Horizontal access - Access another user’s objects
  • Vertical access - Access privileged/admin-only objects
  • Cross-tenant - Break isolation in multi-tenant systems

BFLA (Function-Level)

  • Privileged actions - Invoke admin/staff-only functions
  • Feature gates - Bypass premium/paid feature restrictions
  • State changes - Execute unauthorized mutations

Attack Surface Coverage

Esprit analyzes authorization across:

Reference Locations

  • Path parameters (/users/:id, /orgs/:orgId/projects/:projectId)
  • Query strings (?userId=123&resourceId=456)
  • Request body (JSON, form data)
  • HTTP headers and cookies
  • JWT claims
  • GraphQL arguments
  • WebSocket messages
  • gRPC message fields

Identifier Types

  • Sequential integers
  • UUID/ULID/CUID
  • Snowflake IDs
  • Slug/handle strings
  • Composite keys (orgId:userId)
  • Opaque tokens
  • Base64/hex-encoded references

IDOR Detection

Esprit tests object-level authorization:

Horizontal Access

// Vulnerable code: src/api/invoices.js:34
app.get('/api/invoices/:id', authenticate, async (req, res) => {
  const invoice = await Invoice.findById(req.params.id);
  // VULNERABLE: No ownership check!
  res.json(invoice);
});
Esprit test flow:
  1. Create resources as User A and User B
  2. User A requests User B’s invoice ID
  3. Detect unauthorized access
Exploitation:
# User A (ID: 123) accesses their invoice
curl -H "Authorization: Bearer user-a-token" \
  https://api.example.com/api/invoices/inv_abc123

# User A accesses User B's invoice (IDOR!)
curl -H "Authorization: Bearer user-a-token" \
  https://api.example.com/api/invoices/inv_xyz789

Vertical Access

// Vulnerable code: src/api/users.js:67
app.get('/api/users/:id/admin-notes', authenticate, async (req, res) => {
  // VULNERABLE: Basic users can access admin-only data
  const notes = await AdminNote.find({ userId: req.params.id });
  res.json(notes);
});

GraphQL IDOR

# Vulnerable resolver
type Query {
  user(id: ID!): User
  invoice(id: ID!): Invoice
}

# Resolver implementation - VULNERABLE
const resolvers = {
  Query: {
    user: (_, { id }) => User.findById(id),  // No authz check
    invoice: (_, { id }) => Invoice.findById(id)  // No authz check
  }
};
Exploitation:
query IDORTest {
  # Query own data
  me { id email }
  
  # Query other users (IDOR!)
  u1: user(id: "user_456") { 
    email 
    phoneNumber
    billing { cardLast4 }
  }
  
  u2: user(id: "user_789") { 
    email 
    ssn  # Sensitive PII
  }
}
GraphQL requires resolver-level authorization checks. Top-level authentication is not sufficient.

High-Value Targets

Esprit prioritizes testing:

Financial Resources

  • Invoices, payment methods, transactions
  • Billing history, subscription details
  • Credits, refunds, gift cards

Sensitive Data

  • User profiles with PII/PHI/PCI data
  • Healthcare/education records
  • HR documents, payroll
  • Private messages, notifications
  • Audit logs, activity feeds

Export/Report Endpoints

// Vulnerable: src/api/exports.js:89
app.get('/api/exports/:jobId/download', authenticate, async (req, res) => {
  const job = await ExportJob.findById(req.params.jobId);
  // VULNERABLE: No ownership validation
  res.download(job.filePath);
});

Multi-Tenant Resources

// Vulnerable: src/api/organizations.js:123
app.get('/api/orgs/:orgId/projects', authenticate, async (req, res) => {
  // VULNERABLE: User in Org A can access Org B's projects
  const projects = await Project.find({ orgId: req.params.orgId });
  res.json(projects);
});

Broken Function-Level Authorization

Esprit detects action-level authorization failures:

Admin Action Bypass

// Vulnerable code: src/api/admin.js:45
app.post('/api/users/:id/promote', authenticate, async (req, res) => {
  // VULNERABLE: Missing role check
  await User.updateOne(
    { _id: req.params.id },
    { $set: { role: 'admin' } }
  );
  res.json({ success: true });
});
Exploitation:
# Basic user promotes themselves to admin
curl -X POST \
  -H "Authorization: Bearer basic-user-token" \
  https://api.example.com/api/users/self/promote

Alternate Method Bypass

// Vulnerable: Different authz for POST vs PUT
app.post('/api/users', requireAdmin, createUser);  // Admin only
app.put('/api/users/:id', authenticate, updateUser);  // Any authenticated user!

// Attacker uses PUT to modify role field
curl -X PUT \
  -H "Authorization: Bearer user-token" \
  -d '{"role": "admin"}' \
  https://api.example.com/api/users/123

GraphQL Mutation Bypass

# Admin mutation exposed without authorization
mutation PromoteUser($id: ID!) {
  updateUser(id: $id, role: ADMIN) {
    id
    role
  }
}
// Vulnerable resolver
Mutation: {
  updateUser: async (_, { id, role }) => {
    // VULNERABLE: No role check
    return User.findByIdAndUpdate(id, { role }, { new: true });
  }
}

Advanced Techniques

Esprit tests sophisticated authorization bypasses:

Batch Operation IDOR

// Vulnerable: Bulk delete without per-item check
app.delete('/api/documents/bulk', authenticate, async (req, res) => {
  const { ids } = req.body;
  // VULNERABLE: Only validates ownership of first ID
  const first = await Document.findById(ids[0]);
  if (first.ownerId === req.user.id) {
    await Document.deleteMany({ _id: { $in: ids } });
  }
  res.json({ deleted: ids.length });
});
Exploitation:
# Delete own document plus others' documents
curl -X DELETE \
  -H "Authorization: Bearer user-token" \
  -d '{"ids": ["myDoc", "otherUserDoc1", "otherUserDoc2"]}' \
  https://api.example.com/api/documents/bulk

Parameter Pollution

// Vulnerable: Parameter precedence issues
app.get('/api/files', authenticate, async (req, res) => {
  // Express takes last value for duplicate params
  const userId = req.query.userId;  // Could be array or last value
  const files = await File.find({ ownerId: userId });
  res.json(files);
});
Exploitation:
# Backend sees userId=456 (last value)
curl 'https://api.example.com/api/files?userId=123&userId=456' \
  -H "Authorization: Bearer user-token"

WebSocket Authorization

// Vulnerable: One-time handshake auth
io.use((socket, next) => {
  const token = socket.handshake.auth.token;
  socket.user = verifyToken(token);
  next();
});

// VULNERABLE: No per-message authorization
socket.on('read:document', async (docId) => {
  const doc = await Document.findById(docId);
  socket.emit('document:data', doc);  // No ownership check!
});

Microservices Header Trust

// API Gateway
app.use((req, res, next) => {
  const user = verifyToken(req.headers.authorization);
  req.headers['x-user-id'] = user.id;
  req.headers['x-user-role'] = user.role;
  proxy.web(req, res, { target: 'http://backend' });
});

// Backend Service - VULNERABLE
app.get('/api/sensitive', (req, res) => {
  // VULNERABLE: Trusts X-User-Id header without verification
  const userId = req.headers['x-user-id'];
  const data = getSensitiveData(userId);
  res.json(data);
});
Exploitation:
# Bypass gateway and call backend directly with forged headers
curl http://backend:3000/api/sensitive \
  -H "X-User-Id: admin" \
  -H "X-User-Role: admin"

Remediation

Esprit recommends:

Enforce Object-Level Authorization

// SAFE: Validate ownership
app.get('/api/invoices/:id', authenticate, async (req, res) => {
  const invoice = await Invoice.findOne({
    _id: req.params.id,
    userId: req.user.id  // Enforce ownership
  });
  
  if (!invoice) {
    return res.status(404).json({ error: 'Not found' });
  }
  
  res.json(invoice);
});

Use Authorization Middleware

// SAFE: Reusable authorization middleware
function requireOwnership(resourceType) {
  return async (req, res, next) => {
    const resource = await resourceType.findById(req.params.id);
    
    if (!resource) {
      return res.status(404).json({ error: 'Not found' });
    }
    
    if (resource.ownerId.toString() !== req.user.id.toString()) {
      return res.status(403).json({ error: 'Forbidden' });
    }
    
    req.resource = resource;
    next();
  };
}

// Usage
app.get('/api/documents/:id', 
  authenticate, 
  requireOwnership(Document),
  (req, res) => {
    res.json(req.resource);
  }
);

GraphQL Authorization

// SAFE: Resolver-level authorization
const resolvers = {
  Query: {
    user: async (_, { id }, context) => {
      const user = await User.findById(id);
      
      // Enforce authorization
      if (context.user.id !== id && !context.user.isAdmin) {
        throw new Error('Unauthorized');
      }
      
      return user;
    },
    
    invoice: async (_, { id }, context) => {
      const invoice = await Invoice.findById(id);
      
      if (invoice.userId !== context.user.id) {
        throw new Error('Unauthorized');
      }
      
      return invoice;
    }
  },
  
  Mutation: {
    updateUser: async (_, { id, input }, context) => {
      // Function-level authz: Only admins can change roles
      if (input.role && !context.user.isAdmin) {
        throw new Error('Forbidden');
      }
      
      // Object-level authz: Users can only update themselves
      if (context.user.id !== id && !context.user.isAdmin) {
        throw new Error('Unauthorized');
      }
      
      return User.findByIdAndUpdate(id, input, { new: true });
    }
  }
};

Multi-Tenant Isolation

// SAFE: Always scope queries by tenant
class TenantRepository {
  constructor(orgId) {
    this.orgId = orgId;
  }
  
  async find(query) {
    return Model.find({ 
      ...query, 
      orgId: this.orgId  // Always enforce tenant scope
    });
  }
  
  async findById(id) {
    return Model.findOne({ 
      _id: id, 
      orgId: this.orgId 
    });
  }
}

// Usage
app.get('/api/projects/:id', authenticate, async (req, res) => {
  const repo = new TenantRepository(req.user.orgId);
  const project = await repo.findById(req.params.id);
  
  if (!project) {
    return res.status(404).json({ error: 'Not found' });
  }
  
  res.json(project);
});
Esprit validates that authorization checks are consistently applied across all transports: REST, GraphQL, gRPC, and WebSocket.

Detection Output

Esprit provides detailed access control findings:
[HIGH] Horizontal IDOR in invoice endpoint
Location: src/api/invoices.js:34
Type: Broken Object-Level Authorization

Vulnerable Code:
  app.get('/api/invoices/:id', authenticate, async (req, res) => {
    const invoice = await Invoice.findById(req.params.id);
    res.json(invoice);  // No ownership check
  });

Proof:
  User A (ID: usr_123):
    GET /api/invoices/inv_abc → 200 OK (own invoice)
    GET /api/invoices/inv_xyz → 200 OK (User B's invoice - IDOR!)
  
  Response contains:
    - User B's billing address
    - Payment method last 4 digits
    - Transaction history

Impact:
  - Cross-account data exposure (PCI data)
  - Privacy violation (GDPR Article 32)
  - Enumeration of all invoices via ID iteration

Remediation:
  const invoice = await Invoice.findOne({
    _id: req.params.id,
    userId: req.user.id
  });

Next Steps

SSRF Detection

Find server-side request forgery vulnerabilities

Business Logic

Detect workflow and invariant violations

Build docs developers (and LLMs) love