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:
Create resources as User A and User B
User A requests User B’s invoice ID
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!
});
// 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"
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