Broken Access Control in Koa with Mutual Tls
Broken Access Control in Koa with Mutual Tls — how this specific combination creates or exposes the vulnerability
Broken Access Control occurs when an API fails to enforce proper authorization checks, allowing one user to access or modify resources belonging to another. In Koa, developers often assume that enabling Mutual Transport Layer Security (Mutual TLS) is sufficient for access control. This is incorrect: Mutual TLS provides strong identity verification for the client and server, but it does not enforce authorization. If your Koa routes lack explicit role- or scope-based checks, an authenticated client with a valid certificate can still reach endpoints they should not have access to.
Mutual TLS binds a certificate to a client identity (for example, a username, organizational unit, or API consumer ID). This identity is typically available in Koa via the TLS socket properties (e.g., ctx.peerCertificate). However, if you only verify that a certificate exists and skip mapping that identity to permissions, you have authentication without authorization — a classic Broken Access Control pitfall. Attackers who obtain a valid client certificate (through theft, weak provisioning, or overly permissive trust stores) can pivot across user boundaries by changing identifiers in requests, especially if your endpoints rely on predictable resource IDs (BOLA/IDOR patterns).
Consider a Koa endpoint that retrieves user profiles by ID without verifying that the requesting certificate maps to that user:
// Insecure: assumes certificate identity is enough
app.use(async (ctx) => {
if (ctx.path.startsWith('/api/users/')) {
const userId = ctx.params.id;
const cert = ctx.peerCertificate;
// Only presence of cert is checked, not ownership of userId
ctx.body = await getUserProfile(userId);
}
});
In this scenario, a certificate issued to alice can request /api/users/123 and receive data for user 123 if the route does not compare the certificate identity to the resource owner. This misalignment between authentication (certificate) and authorization (resource ownership or role) is a root cause of Broken Access Control in Koa when using Mutual TLS. Compounded with improper input validation on IDs and missing rate limiting, such endpoints are vulnerable to enumeration and privilege escalation.
Additionally, if your Koa application inspects claims or roles from parsed certificate fields (e.g., Extended Key Usage or custom OIDs) but does not enforce least-privilege checks at each route, you risk over-permissive access. For example, a certificate that should be limited to read-only operations might still invoke write endpoints if your middleware does not validate scopes or roles explicitly. This aligns with OWASP API Top 10 A01:2023 — Broken Access Control — and can expose sensitive data or enable unauthorized modifications even when Mutual TLS is in place.
Mutual Tls-Specific Remediation in Koa — concrete code fixes
To remediate Broken Access Control while using Mutual TLS in Koa, you must combine certificate validation with explicit authorization checks. Never rely on the presence of a client certificate alone. Instead, extract identity and attributes from the certificate and enforce them in your route handlers or middleware.
Below is a secure pattern that maps certificate fields to user permissions and enforces access at the route level:
const Koa = require('koa');
const https = require('https');
const fs = require('fs');
const app = new Koa();
// Load CA that signed client certificates
const ca = fs.readFileSync('ca.pem');
// Middleware to extract and validate certificate identity
app.use(async (ctx, next) => {
const cert = ctx.peerCertificate;
if (!cert || !cert.subject) {
ctx.status = 401;
ctx.body = { error: 'certificate required' };
return;
}
// Map certificate fields to internal identity
const userId = cert.subject.O; // e.g., organizationalUnit or custom field
if (!userId) {
ctx.status = 403;
ctx.body = { error: 'invalid certificate claims' };
return;
}
// Attach identity to context for downstream use
ctx.state.user = { id: userId, roles: (cert.tunnel || '').split(',') };
await next();
});
// Authorization middleware example
const requireRole = (requiredRole) => {
return (ctx) => {
if (!ctx.state.user.roles.includes(requiredRole)) {
ctx.status = 403;
ctx.body = { error: 'insufficient scope' };
return Promise.reject('forbidden');
}
return Promise.resolve();
};
};
// Secure route: enforce ownership or role
app.use(async (ctx) => {
if (ctx.path.startsWith('/api/users/')) {
const userId = ctx.params.id;
const requester = ctx.state.user;
// Ensure requester matches resource or has admin role
if (requester.id !== userId && !requester.roles.includes('admin')) {
ctx.status = 403;
ctx.body = { error: 'access denied to this resource' };
return;
}
ctx.body = await getUserProfile(userId);
}
});
// Example server options for Mutual TLS
const server = https.createServer({
cert: fs.readFileSync('server-cert.pem'),
key: fs.readFileSync('server-key.pem'),
requestCert: true,
rejectUnauthorized: true,
ca: ca,
}, app.callback());
server.listen(3000);
Key points in this remediation:
- Always validate the certificate and reject unauthorized connections at the TLS layer (
rejectUnauthorized: true). - Map certificate fields (e.g., Organization, Organizational Unit, or custom OIDs) to an internal identity and attach it to
ctx.statefor downstream checks. - Implement explicit authorization checks per route, comparing the certificate-bound identity to the resource owner or required roles/scopes.
- Avoid treating certificate presence as an authorization grant; combine Mutual TLS with fine-grained access control logic.
By following this pattern, you maintain the mutual authentication benefits of Mutual TLS while preventing Broken Access Control scenarios in Koa applications.