Api Key Exposure in Koa
How Api Key Exposure Manifests in Koa
Api Key Exposure in Koa applications typically occurs through several specific code patterns and architectural decisions. The most common manifestation is logging sensitive credentials directly to console or file systems, often through middleware that captures request data for debugging or analytics.
Koa's middleware chain creates unique exposure points. When using body-parser middleware or custom body parsing logic, API keys frequently end up in request bodies that get logged. Consider this problematic pattern:
const Koa = require('koa');
const logger = require('koa-logger');
const app = new Koa();
app.use(logger()); // Logs full request bodies including API keys
app.use(async (ctx) => {
ctx.body = { message: 'OK' };
});
app.listen(3000);
The koa-logger middleware logs entire request bodies, headers, and query parameters by default. If an API key is sent in the Authorization header or request body, it appears in plaintext logs accessible to anyone with server access.
Another Koa-specific exposure pattern occurs with error handling middleware. When exceptions are caught and logged without sanitization:
app.use(async (ctx, next) => {
try {
await next();
} catch (err) {
console.error(err.stack); // May contain API keys in error messages
ctx.status = err.status || 500;
ctx.body = { error: err.message };
}
});
Error messages often include request context, and if an API key was part of the failed request, it gets exposed in stack traces or error responses.
Query parameter exposure is particularly problematic in Koa since URL parameters are easily logged and cached. Developers sometimes pass API keys as query parameters:
// INSECURE: API key in URL
app.use(async (ctx) => {
const apiKey = ctx.query.api_key;
// API key now in server logs, browser history, CDN logs
});
Environment variable misconfiguration also creates exposure. Koa applications often serve health check endpoints that inadvertently expose environment variables:
app.use(async (ctx) => {
if (ctx.path === '/health') {
ctx.body = process.env; // Exposes API keys stored in env vars
}
});
Third-party middleware can be another source. Many Koa middleware packages log request data without considering sensitive information:
const analytics = require('koa-analytics');
app.use(analytics()); // May log full request data including API keys
Rate limiting middleware often logs request identifiers that could include API keys:
const ratelimit = require('koa-ratelimit');
app.use(ratelimit({
db: new Map(),
duration: 60000,
errorMessage: 'Too many requests',
id: (ctx) => ctx.get('Authorization') // Logs API keys from headers
}));
Cross-site request forgery protection in Koa can also leak API keys if implemented incorrectly, especially when using token-based authentication that gets exposed in client-side storage.
Koa-Specific Detection
Detecting API key exposure in Koa applications requires examining both the codebase and runtime behavior. Static analysis should focus on middleware usage patterns and logging configurations.
Code review should identify these specific Koa patterns:
// Search for these dangerous patterns:
// 1. Direct logging of request bodies
app.use(logger());
// 2. Error handling that exposes context
app.use(async (ctx, next) => {
try { await next(); }
catch (err) { console.error(err); }
});
// 3. Health endpoints exposing environment
app.use(async (ctx) => {
if (ctx.path === '/health') ctx.body = process.env;
});
Runtime detection involves monitoring log files for API key patterns. Use regex to scan logs for common API key formats:
const apiKeyRegex = /(?:|")(?:|sk-|AIza|ghp-|xoxb-|xoxp-|xoxa-)[A-Za-z0-9_-]{20,60}(?:|")/g;
// Monitor logs in real-time
const fs = require('fs');
const logStream = fs.createReadStream('/var/log/koa-app.log');
logStream.on('data', (chunk) => {
const matches = chunk.toString().match(apiKeyRegex);
if (matches) {
console.warn('API key exposure detected:', matches);
}
});
Automated scanning with middleBrick provides comprehensive detection across 12 security categories, including API key exposure. The scanner tests unauthenticated endpoints for credential leakage without requiring access credentials:
# Scan Koa API endpoint
middlebrick scan https://api.yourservice.com
# Scan with OpenAPI spec for deeper analysis
middlebrick scan https://api.yourservice.com --spec openapi.json
middleBrick's black-box scanning identifies exposed API keys in responses, headers, and error messages. The scanner tests for common exposure patterns including:
- API keys in response bodies
- Credentials in error stack traces
- Authentication tokens in health check responses
- Sensitive data in API documentation responses
Integration testing should include API key sanitization checks:
const assert = require('assert');
describe('API Key Exposure', () => {
it('should not log API keys', async () => {
const logSpy = jest.spyOn(console, 'log').mockImplementation();
// Make request with API key
await request(app)
.post('/endpoint')
.set('Authorization', 'Bearer test-key-123456')
.send({ data: 'test' });
expect(logSpy).not.toHaveBeenCalledWith(expect.stringContaining('test-key-123456'));
logSpy.mockRestore();
});
});
Network-level detection using middleware that inspects outgoing requests can catch API key transmission over insecure channels:
app.use(async (ctx, next) => {
const protocol = ctx.protocol;
const apiKey = ctx.get('Authorization');
if (protocol !== 'https' && apiKey) {
console.warn('API key sent over insecure connection:', ctx.href);
}
await next();
});
Koa-Specific Remediation
Remediating API key exposure in Koa requires a multi-layered approach focusing on logging, error handling, and request processing. Start with secure logging middleware that sanitizes sensitive data:
const Koa = require('koa');
const app = new Koa();
// Custom secure logger
app.use(async (ctx, next) => {
const start = Date.now();
try {
await next();
// Log without sensitive data
const logData = {
method: ctx.method,
url: ctx.url,
status: ctx.status,
responseTime: Date.now() - start,
ip: ctx.ip
};
console.log(JSON.stringify(logData));
} catch (err) {
// Sanitize error logs
const sanitizedError = {
message: err.message,
status: err.status || 500,
stack: err.stack ? err.stack.split('\n').slice(0, 3).join('\n') : null
};
console.error(JSON.stringify(sanitizedError));
throw err;
}
});
Implement secure error handling that removes sensitive context:
app.use(async (ctx, next) => {
try {
await next();
} catch (err) {
// Remove sensitive data from error context
const sanitizedError = {
message: err.message,
status: err.status || 500
};
ctx.status = err.status || 500;
ctx.body = sanitizedError;
// Log without sensitive data
console.error('Error:', {
message: err.message,
status: err.status,
timestamp: new Date().toISOString()
});
}
});
Use middleware to sanitize request bodies before logging:
const sanitize = (obj, keysToSanitize = ['api_key', 'password', 'token']) => {
if (Array.isArray(obj)) {
return obj.map(item => sanitize(item, keysToSanitize));
}
if (typeof obj === 'object' && obj !== null) {
return Object.keys(obj).reduce((acc, key) => {
if (keysToSanitize.includes(key.toLowerCase())) {
acc[key] = '[REDACTED]';
} else if (typeof obj[key] === 'object') {
acc[key] = sanitize(obj[key], keysToSanitize);
} else {
acc[key] = obj[key];
}
return acc;
}, {});
}
return obj;
};
app.use(async (ctx, next) => {
const originalBody = ctx.request.body;
// Sanitize body for logging
ctx.state.sanitizedBody = sanitize(originalBody);
await next();
});
Secure health check endpoints to prevent environment exposure:
app.use(async (ctx) => {
if (ctx.path === '/health') {
ctx.body = {
status: 'healthy',
timestamp: new Date().toISOString(),
uptime: process.uptime()
};
return;
}
await next();
});
Configure middleware to avoid logging sensitive headers:
const secureLogger = async (ctx, next) => {
const start = Date.now();
// Remove sensitive headers from context
const originalGet = ctx.get.bind(ctx);
ctx.get = (header) => {
const sensitiveHeaders = ['authorization', 'cookie', 'apikey'];
if (sensitiveHeaders.includes(header.toLowerCase())) {
return '[REDACTED]';
}
return originalGet(header);
};
await next();
// Log sanitized information
console.log(`${ctx.method} ${ctx.url} ${ctx.status} ${Date.now() - start}ms`);
};
app.use(secureLogger);
Implement API key validation middleware that doesn't log credentials:
const apiKeyValidator = async (ctx, next) => {
const authHeader = ctx.get('Authorization');
if (authHeader && authHeader.startsWith('Bearer ')) {
const apiKey = authHeader.substring(7);
// Validate without logging
const isValid = await validateApiKey(apiKey);
if (!isValid) {
ctx.status = 401;
ctx.body = { error: 'Invalid API key' };
return;
}
}
await next();
};
app.use(apiKeyValidator);
Use environment-specific logging configurations:
const isProduction = process.env.NODE_ENV === 'production';
app.use(async (ctx, next) => {
if (isProduction) {
// Minimal logging in production
console.log(`${ctx.method} ${ctx.url} ${ctx.status}`);
} else {
// More verbose logging in development
console.log(`${ctx.method} ${ctx.url} ${ctx.status} ${JSON.stringify(ctx.state.sanitizedBody)}`);
}
await next();
});