Privilege Escalation in Hapi
How Privilege Escalation Manifests in Hapi
Privilege escalation in Hapi applications often occurs through improper authentication and authorization boundaries. The most common pattern involves route-level authentication that doesn't properly cascade to nested route handlers or middleware. Consider this vulnerable pattern:
const Hapi = require('@hapi/hapi');
const server = Hapi.server({ port: 3000 });
// Admin-only route
const adminRoute = {
method: 'GET',
path: '/admin/users',
options: {
auth: 'admin-strategy'
},
handler: async (request, h) => {
const users = await request.db.users.find();
return users;
}
};
server.route(adminRoute);
// Nested route without auth
const nestedRoute = {
method: 'GET',
path: '/admin/users/{id}',
options: {},
handler: async (request, h) => {
const user = await request.db.users.findById(request.params.id);
return user;
}
};
server.route(nestedRoute);
The nested route inherits no authentication context from its parent path, allowing any authenticated user to access admin user data. Hapi's route isolation means nested routes are completely independent unless explicitly configured.
Another Hapi-specific escalation vector involves plugin scope leakage. When plugins register routes without proper scope isolation, they can access higher-privilege context:
const plugin = {
name: 'user-plugin',
version: '1.0.0',
register: async (server, options) => {
// Plugin has access to server.app.adminContext
server.route({
method: 'GET',
path: '/plugin/admin-data',
handler: async (request, h) => {
return request.server.app.adminContext.getAll();
}
});
}
};
server.register(plugin);
This occurs when plugins are registered with insufficient sandboxing, allowing them to access administrative context objects stored in server.app.
Context injection through Hapi's request lifecycle is another escalation path. Malicious plugins or middleware can inject privileged objects into the request lifecycle:
// Vulnerable context injection
const injectAdminContext = (request, h) => {
if (request.query.debug === 'true') {
request.adminContext = request.server.app.adminContext;
}
return h.continue;
};
server.ext('onRequest', injectAdminContext);
An attacker can trigger debug mode and gain access to admin context through a simple query parameter.
Hapi-Specific Detection
Detecting privilege escalation in Hapi requires examining route configurations, plugin boundaries, and authentication strategies. The most effective approach combines static analysis with runtime scanning.
Static analysis should verify that all routes requiring elevated privileges have proper authentication strategies defined. Use this pattern to audit your routes:
const auditRoutes = (server) => {
const routes = server.table().routes;
const findings = [];
routes.forEach(route => {
if (route.path.includes('admin') || route.path.includes('privileged')) {
if (!route.settings.auth || route.settings.auth === false) {
findings.push({
path: route.path,
method: route.method,
issue: 'Missing authentication on privileged route'
});
}
}
});
return findings;
};
This identifies routes with admin-related paths that lack authentication requirements.
For runtime detection, middleBrick's black-box scanning can identify privilege escalation vulnerabilities by testing unauthenticated access to protected endpoints. The scanner attempts to access admin routes without credentials and analyzes responses:
# Scan your Hapi API with middleBrick
middlebrick scan https://yourapi.com --category "Privilege Escalation"
The scan tests for:
- Unauthenticated access to admin routes
- Authentication bypass through parameter manipulation
- Context injection vulnerabilities
- Plugin boundary violations
middleBrick's OpenAPI analysis also examines your Hapi-generated OpenAPI spec for missing authentication requirements on sensitive endpoints. The scanner cross-references your spec with actual runtime behavior to identify discrepancies.
Plugin boundary analysis is critical for Hapi applications. Use this diagnostic to verify plugin isolation:
const checkPluginBoundaries = (server) => {
const plugins = server.plugins;
const findings = [];
Object.keys(plugins).forEach(pluginName => {
const plugin = plugins[pluginName];
if (plugin.exports && plugin.exports.adminContext) {
findings.push({
plugin: pluginName,
issue: 'Plugin exports privileged context'
});
}
});
return findings;
};
This identifies plugins that improperly expose administrative functionality.
Hapi-Specific Remediation
Remediating privilege escalation in Hapi requires a defense-in-depth approach using Hapi's built-in security features. Start with route-level authentication that cannot be bypassed:
const adminRoute = {
method: 'GET',
path: '/admin/users',
options: {
auth: {
strategy: 'admin-strategy',
access: {
scope: ['admin']
}
},
pre: [
// Verify admin scope before route handler
(request, h) => {
if (!request.auth.credentials.scope.includes('admin')) {
throw Boom.forbidden('Admin privileges required');
}
return h.continue;
}
]
},
handler: async (request, h) => {
const users = await request.db.users.find();
return users;
}
};
This pattern combines strategy authentication with explicit scope checking and pre-handler validation.
For nested routes, use Hapi's route configuration inheritance properly. Define parent routes with authentication and ensure nested routes respect this context:
const adminParentRoute = {
method: 'GET',
path: '/admin/{p*}',
options: {
auth: 'admin-strategy',
handler: (request, h) => {
// Parent route handler can handle multiple sub-paths
const segments = request.params.p.split('/');
if (segments[0] === 'users') {
return handleUsers(request);
}
throw Boom.notFound();
}
}
};
This eliminates the need for separate nested routes that might miss authentication.
Plugin isolation requires explicit scope boundaries. Use Hapi's plugin options to restrict access:
const securePlugin = {
name: 'user-plugin',
version: '1.0.0',
options: {
once: true,
routes: {
prefix: '/plugin',
vhost: 'plugin.yourapp.com'
}
},
register: async (server, options) => {
// Plugin has restricted scope
server.route({
method: 'GET',
path: '/plugin/data',
options: {
auth: 'user-strategy',
pre: [
(request, h) => {
// Validate plugin context
if (!request.server.app.pluginContext) {
throw Boom.internal('Invalid plugin context');
}
return h.continue;
}
]
},
handler: async (request, h) => {
return request.server.app.pluginContext.getData();
}
});
}
};
This pattern restricts plugin routes to specific prefixes and vhosts, preventing scope leakage.
Context injection vulnerabilities require strict input validation. Never expose privileged context based on query parameters:
const secureContextInjection = (request, h) => {
// Never expose admin context based on user input
if (request.query.debug) {
throw Boom.badRequest('Debug mode not available');
}
return h.continue;
};
server.ext('onRequest', secureContextInjection);
Implement comprehensive logging for privilege escalation attempts:
const auditLogger = (request, h) => {
if (request.path.includes('admin') && !request.auth.isAuthenticated) {
request.logger.error('Privilege escalation attempt', {
path: request.path,
ip: request.info.remoteAddress,
userAgent: request.headers['user-agent']
});
}
return h.continue;
};
server.ext('onRequest', auditLogger);
This creates an audit trail for suspicious access patterns.