Api Key Exposure in Hapi
How Api Key Exposure Manifests in Hapi
Api Key Exposure in Hapi applications typically occurs through improper handling of authentication credentials in HTTP headers, query parameters, or request bodies. The most common vulnerability pattern involves sending API keys in the Authorization header without proper validation, or worse, logging these credentials inadvertently.
In Hapi, API key exposure often manifests in route handlers where developers directly access request headers without sanitization. Consider this vulnerable pattern:
const Hapi = require('@hapi/hapi');
const init = async () => {
const server = Hapi.server({ port: 3000 });
server.route({
method: 'GET',
path: '/api/data',
handler: (request, h) => {
const apiKey = request.headers.authorization; // Exposed in logs
// Vulnerable: apiKey logged without sanitization
console.log(`Processing request with API key: ${apiKey}`);
return { data: 'sensitive information' };
}
});
await server.start();
console.log('Server running on %s', server.info.uri);
};
init().catch(err => {
console.error(err);
process.exit(1);
});This code exposes the API key through server logs, which attackers can access through log aggregation services or compromised logging infrastructure. The vulnerability compounds when API keys are sent in URLs:
server.route({
method: 'GET',
path: '/api/data',
handler: (request, h) => {
const apiKey = request.query.apiKey; // Exposed in browser history, server logs
// API key now appears in access logs and browser history
return { data: 'sensitive information' };
}
});Another Hapi-specific manifestation occurs with route options where authentication is improperly configured:
server.route({
method: 'POST',
path: '/api/update',
options: {
auth: {
strategy: 'simple',
mode: 'required'
}
},
handler: (request, h) => {
// If auth fails, request object still contains partial header data
const authHeader = request.headers.authorization;
// Potential exposure if error handling logs raw headers
if (!authHeader) {
console.error(`Auth failed: ${JSON.stringify(request.headers)}`);
return h.response('Unauthorized').code(401);
}
return { success: true };
}
});The vulnerability extends to plugin development where Hapi's plugin system can inadvertently expose credentials through plugin options or internal state sharing between plugins.
Hapi-Specific Detection
Detecting API key exposure in Hapi applications requires examining both code patterns and runtime behavior. Start with static analysis of your route definitions and handler implementations.
Code-level detection focuses on identifying dangerous patterns:
// Dangerous pattern: direct header access without validation
const apiKey = request.headers['x-api-key'];
// Dangerous pattern: logging sensitive headers
console.log(`Request from ${request.info.remoteAddress} with key: ${request.headers.authorization}`);
// Dangerous pattern: query parameter API keys
const apiKey = request.query.api_key;
// Dangerous pattern: exposing in response objects
return { apiKey: request.headers.authorization };
Runtime detection involves monitoring what gets logged and transmitted. Hapi's request lifecycle provides hooks for inspection:
server.ext('onRequest', (request, h) => {
const authHeader = request.headers.authorization;
// Detect potential exposure
if (authHeader && authHeader.length > 50) {
console.warn('Suspicious authorization header detected');
}
return h.continue;
});
// Log sanitization extension
server.ext('onLog', (event, h) => {
if (event.tags.includes('request')) {
// Remove sensitive headers from log events
const sanitized = { ...event.data };
delete sanitized.headers;
return h.continue;
}
return h.continue;
});For comprehensive detection, use middleBrick's API security scanner which specifically tests for API key exposure patterns in Hapi applications. The scanner examines:
- Authorization header handling and validation
- Query parameter authentication
- Header logging and data exposure
- Response object construction that might leak credentials
- Plugin configuration for authentication
middleBrick's scanning process for Hapi applications takes 5-15 seconds and provides a security risk score with specific findings about API key exposure vulnerabilities. The scanner tests unauthenticated attack surfaces and identifies where API keys might be exposed through improper implementation patterns.
Hapi-Specific Remediation
Remediating API key exposure in Hapi requires implementing proper authentication validation, header sanitization, and secure logging practices. Start with robust authentication validation:
const Hapi = require('@hapi/hapi');
const Boom = require('@hapi/boom');
const validateApiKey = async (apiKey) => {
// Implement proper validation logic
// This should check against a secure store, not hardcoded values
const validKeys = ['valid-api-key-123']; // In production, use database or secret manager
return validKeys.includes(apiKey);
};
const init = async () => {
const server = Hapi.server({ port: 3000 });
// Secure authentication scheme
server.auth.scheme('api_key', (server, options) => ({
authenticate: async (request, h) => {
const authHeader = request.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
throw Boom.unauthorized('Missing or invalid API key format');
}
const apiKey = authHeader.substring(7); // Remove 'Bearer ' prefix
try {
const isValid = await validateApiKey(apiKey);
if (!isValid) {
throw Boom.unauthorized('Invalid API key');
}
// Authenticated successfully
return h.authenticated({
credentials: { apiKey },
artifacts: { apiKey }
});
} catch (err) {
throw Boom.badImplementation('Authentication validation failed', err);
}
}
}));
// Register authentication strategy
server.auth.strategy('api_key', 'api_key');
server.route({
method: 'GET',
path: '/api/data',
options: {
auth: {
strategy: 'api_key',
mode: 'required'
}
},
handler: (request, h) => {
// At this point, authentication is validated
// No need to access raw headers
return { data: 'secure information' };
}
});
await server.start();
console.log('Server running on %s', server.info.uri);
};
init().catch(err => {
console.error(err);
process.exit(1);
});Implement secure logging with header sanitization:
const Hapi = require('@hapi/hapi');
const init = async () => {
const server = Hapi.server({ port: 3000 });
// Custom request lifecycle extension for secure logging
server.ext('onRequest', (request, h) => {
// Create a sanitized copy of headers for logging
const sanitizedHeaders = { ...request.headers };
// Remove or mask sensitive headers
const sensitiveHeaders = ['authorization', 'x-api-key', 'api-key'];
sensitiveHeaders.forEach(header => {
if (sanitizedHeaders[header]) {
sanitizedHeaders[header] = 'REDACTED';
}
});
// Store sanitized headers for later use
request.app.sanitizedHeaders = sanitizedHeaders;
return h.continue;
});
// Secure error handling
server.ext('onPreResponse', (request, h) => {
const response = request.response;
if (response.isBoom && response.output.statusCode >= 400) {
// Log errors without sensitive data
const { sanitizedHeaders } = request.app || {};
const logData = {
method: request.method,
path: request.path,
statusCode: response.output.statusCode,
headers: sanitizedHeaders || 'unavailable'
};
request.logger.error('Request error', logData);
}
return h.continue;
});
// Route with secure error handling
server.route({
method: 'POST',
path: '/api/update',
options: {
auth: {
strategy: 'api_key',
mode: 'required'
}
},
handler: (request, h) => {
try {
// Process request securely
return { success: true };
} catch (err) {
// Error handling without exposing credentials
request.logger.error('Handler error', {
error: err.message,
method: request.method,
path: request.path
});
return Boom.badImplementation('Internal server error');
}
}
});
await server.start();
console.log('Server running on %s', server.info.uri);
};
init().catch(err => {
console.error(err);
process.exit(1);
});Additional remediation includes implementing rate limiting to prevent credential brute force attacks:
const Hapi = require('@hapi/hapi');
const RateLimit = require('hapi-rate-limit');
const init = async () => {
const server = Hapi.server({ port: 3000 });
// Configure rate limiting
await server.register({
plugin: RateLimit,
options: {
userLimit: 100, // 100 requests per user
userCache: {
expiresIn: 60 * 60 * 1000, // 1 hour
segment: 'user-auth'
}
}
});
// ... rest of server setup
};