Broken Access Control in Koa with Api Keys
Broken Access Control in Koa with Api Keys — how this specific combination creates or exposes the vulnerability
Broken Access Control occurs when API endpoints do not properly enforce authorization checks, allowing one user to access or modify another user’s resources. In Koa, a common pattern is to use API keys for identification, but if authorization (what the key is allowed to do) is not enforced on every relevant route, the system is vulnerable.
When relying solely on API keys without a robust authorization layer, two classes of flaws commonly appear:
- Lack of ownership checks: The key identifies the caller, but the handler does not verify that the resource being accessed belongs to that caller. This maps directly to BOLA/IDOR (Broken Object Level Authorization/Insecure Direct Object References).
- Insufficient scope/role enforcement: API keys may carry metadata (scopes, roles, or tenant IDs), but routes may ignore these attributes and permit privileged actions to callers who should be restricted.
Consider a Koa endpoint that fetches a user profile by an id provided in the URL:
// Insecure example: no ownership check
router.get('/api/users/:id', async (ctx) => {
const user = await db.users.findByPk(ctx.params.id);
ctx.body = user;
});
If the request includes a valid API key identifying tenant A, but the :id points to tenant B’s user, the API leaks data across tenant boundaries. The key authenticated the request, but authorization was missing.
Similarly, endpoints that perform actions such as updating or deleting resources must validate that the caller holds the necessary permissions. Without this, an attacker who discovers another user’s ID can manipulate it directly, leading to privilege escalation or data exposure. This behavior is explicitly covered by checks such as BOLA/IDOR and Property Authorization in middleBrick’s security scan.
Even when API keys are rotated or stored securely, authorization must be enforced at the handler level. Middleware that validates format and presence of keys does not guarantee the caller is allowed to access a specific resource. Attack patterns like Insecure Direct Object References (OWASP API Top 10) and over-privileged roles (Privilege Escalation) exploit this gap.
Api Keys-Specific Remediation in Koa — concrete code fixes
To fix Broken Access Control when using API keys in Koa, couple identification with explicit authorization checks. Below are two concrete, production-ready patterns.
1. Scope-aware key validation with centralized authorization
Store metadata (scopes, tenant, roles) alongside the API key in your database or auth service. On each request, resolve the key and enforce scope/tenant rules before proceeding.
// server.js
const Koa = require('koa');
const Router = require('@koa/router');
const jwt = require('jsonwebtoken'); // optional, for signed key metadata
const app = new Koa();
const router = new Router();
// Mock key store
const apiKeys = new Map([
['sk_live_abc123', { ownerId: 'user-123', tenant: 'acme', scopes: ['read:profile', 'write:profile'] }],
['sk_live_xyz789', { ownerId: 'user-456', tenant: 'beta', scopes: ['read:profile'] }]
]);
// Middleware to resolve key and attach context
async function resolveApiKey(ctx, next) {
const key = ctx.get('X-API-Key');
if (!key) {
ctx.status = 401;
ctx.body = { error: 'api_key_missing' };
return;
}
const payload = apiKeys.get(key);
if (!payload) {
ctx.status = 403;
ctx.body = { error: 'invalid_api_key' };
return;
}
ctx.state.key = payload;
await next();
}
// Authorization helper
function requireScope(scope) {
return (ctx, next) => {
if (!ctx.state.key.scopes.includes(scope)) {
ctx.status = 403;
ctx.body = { error: 'insufficient_scope' };
return;
}
return next();
};
}
// Secure route with ownership check
router.get('/api/users/:id', resolveApiKey, requireScope('read:profile'), async (ctx) => {
const userId = ctx.params.id;
const tenant = ctx.state.key.tenant;
// Ensure the requested user belongs to the same tenant
const user = await db.users.findOne({ where: { id: userId, tenant } });
if (!user) {
ctx.status = 404;
ctx.body = { error: 'not_found' };
return;
}
ctx.body = user;
});
// Secure write route with scope enforcement
router.put('/api/users/:id', resolveApiKey, requireScope('write:profile'), async (ctx) => {
const userId = ctx.params.id;
const tenant = ctx.state.key.tenant;
const user = await db.users.findOne({ where: { id: userId, tenant } });
if (!user) {
ctx.status = 403;
ctx.body = { error: 'access_denied' };
return;
}
// Proceed with update…
ctx.body = { updated: true };
});
app.use(router.routes());
app.listen(3000);
This example demonstrates:
- Identification via API key resolved to a payload containing owner and scopes.
- Enforcement of scope before allowing access to sensitive endpoints.
- Ownership checks (tenant matching) before reading or modifying user-specific resources.
2. Centralized route protection with metadata-driven rules
For larger APIs, define route metadata (required scope, resource owner field) and apply a generic guard to avoid repeating checks.
// secureRouter.js
const { resolveApiKey } = require('./auth');
function secureRoute(routeMeta) {
return [
resolveApiKey,
(ctx, next) => {
const keyMeta = ctx.state.key;
if (!keyMeta.scopes.includes(routeMeta.requiredScope)) {
ctx.status = 403;
ctx.body = { error: 'insufficient_scope' };
return;
}
// Attach routeMeta to context for handlers if needed
ctx.routeMeta = routeMeta;
return next();
}
]
}
// Usage
router.get('/api/profile/:userId', ...secureRoute({ requiredScope: 'read:profile' }), async (ctx) => {
const profile = await db.profiles.findOne({
where: { id: ctx.params.userId, tenant: ctx.state.key.tenant }
});
if (!profile) {
ctx.status = 404;
return;
}
ctx.body = profile;
});
These patterns ensure that API keys do not act as a de facto authorization bypass. By combining key validation with explicit ownership and scope checks, you mitigate BOLA/IDOR and Privilege Escalation risks. Findings from middleBrick’s scans—such as those from the BOLA/IDOR and Property Authorization checks—will highlight exactly where these guards are missing.