Crlf Injection in Hapi
How Crlf Injection Manifests in Hapi
CRLF injection in Hapi applications occurs when untrusted input is incorporated into HTTP headers or response bodies without proper sanitization. Hapi's request/response abstraction can mask these vulnerabilities if developers aren't vigilant about where user data flows.
The most common Hapi-specific manifestation involves dynamic header generation. Consider this vulnerable pattern:
const Hapi = require('@hapi/hapi');
const init = async () => {
const server = Hapi.server({ port: 3000 });
server.route({
method: 'GET',
path: '/set-header',
handler: async (request, h) => {
const headerName = request.query.name;
const headerValue = request.query.value;
// Vulnerable: headerName comes from user input
h.response('Header set').header(headerName, headerValue);
return h;
}
});
await server.start();
};
An attacker could exploit this with /set-header?name=Location%0D%0AX-XSS-Protection%3A%200&value=http://evil.com, injecting additional headers. The %0D%0A sequence represents CRLF, allowing header smuggling.
Another Hapi-specific scenario involves response splitting in dynamic content types:
server.route({
method: 'GET',
path: '/download',
handler: (request, h) => {
const filename = request.query.filename;
const content = request.query.content;
// Vulnerable: filename directly in Content-Disposition
h.response(content)
.type('text/plain')
.header('Content-Disposition', `attachment; filename="${filename}"`);
return h;
}
});
If filename contains %0D%0A sequences, an attacker can inject arbitrary headers like filename=test.txt%0D%0AContent-Type%3A%20application/pdf.
Hapi's plugin system introduces additional complexity. Plugins often chain header modifications:
// Plugin code - vulnerable
exports.plugin = {
name: 'header-plugin',
register: (server) => {
server.ext('onPreResponse', (request, h) => {
const response = request.response;
const customHeader = request.query.x;
// No validation of customHeader
response.headers['X-Custom'] = customHeader;
return h.continue;
});
}
};
This plugin blindly accepts any header value, including those with embedded CRLF sequences.
Hapi-Specific Detection
Detecting CRLF injection in Hapi requires understanding both the framework's request lifecycle and common injection points. middleBrick's black-box scanning approach is particularly effective for Hapi applications since it tests the actual runtime behavior without needing source code.
When scanning a Hapi API endpoint, middleBrick tests for CRLF injection by submitting payloads containing %0D%0A sequences to parameters that might influence headers. For example:
middlebrick scan https://api.example.com/set-header?name=test&value=test
The scanner attempts to inject header splits and observes whether the server responds with unexpected headers or status codes. Hapi's specific response handling makes certain patterns detectable:
- Response splitting attempts that result in HTTP 2xx status codes when they should fail
- Unexpected headers appearing in responses
- Content-Type mismatches indicating header injection
For Hapi applications using plugins, middleBrick's OpenAPI analysis is particularly valuable. If your Hapi server exposes a Swagger/OpenAPI spec, middleBrick cross-references the documented parameters with its runtime findings:
{
"endpoint": "/set-header",
"test_case": "CRLF injection in header name",
"status": "vulnerable",
"severity": "high",
"remediation": "Validate and sanitize all header names and values using Hoek.escapeHeader()",
"evidence": "Response contained unexpected X-Injected-Header: test-value"
}
Hapi's built-in validation using joi can help detect some injection attempts, but it doesn't automatically prevent CRLF injection. middleBrick tests these validation boundaries specifically:
// This validation doesn't prevent CRLF injection
const schema = Joi.object({
filename: Joi.string().required()
});
// Need additional sanitization
middleBrick's continuous monitoring plan for Hapi APIs would periodically rescan endpoints to detect if CRLF injection vulnerabilities are introduced through new plugin versions or code changes.
Hapi-Specific Remediation
Remediating CRLF injection in Hapi requires a defense-in-depth approach using the framework's built-in capabilities. The primary defense is input validation and sanitization using Hapi's native utilities.
For header manipulation, always use Hoek.escapeHeader() from Hapi's utility library:
const Hapi = require('@hapi/hapi');
const Hoek = require('@hapi/hoek');
const init = async () => {
const server = Hapi.server({ port: 3000 });
server.route({
method: 'GET',
path: '/set-header',
handler: (request, h) => {
const headerName = Hoek.escapeHeader(request.query.name);
const headerValue = Hoek.escapeHeader(request.query.value);
// Safe: header values are sanitized
h.response('Header set').header(headerName, headerValue);
return h;
}
});
await server.start();
};
Hoek.escapeHeader() specifically targets CRLF sequences and other dangerous characters in HTTP headers, making it the correct tool for this vulnerability.
For content disposition headers, use a whitelist approach with validation:
const Hapi = require('@hapi/hapi');
const Hoek = require('@hapi/hoek');
const sanitizeFilename = (filename) => {
// Remove any CRLF characters
const sanitized = filename.replace(/[
]/g, '');
// Only allow alphanumeric, hyphen, underscore, dot
if (!/^[a-zA-Z0-9._-]+$/.test(sanitized)) {
throw new Error('Invalid filename');
}
return sanitized;
};
const init = async () => {
const server = Hapi.server({ port: 3000 });
server.route({
method: 'GET',
path: '/download',
handler: (request, h) => {
const filename = sanitizeFilename(request.query.filename);
const content = request.query.content;
h.response(content)
.type('text/plain')
.header('Content-Disposition', `attachment; filename="${filename}"`);
return h;
}
});
await server.start();
};
For plugin developers, implement strict input validation in onPreResponse extensions:
exports.plugin = {
name: 'secure-headers',
register: (server) => {
server.ext('onPreResponse', (request, h) => {
const response = request.response;
// Sanitize all headers before sending
Object.keys(response.headers).forEach(key => {
response.headers[key] = Hoek.escapeHeader(response.headers[key]);
});
return h.continue;
});
}
};
When using Hapi's response toolkit, prefer the built-in methods over manual header manipulation:
// Safer approach
h.response('Content')
.type('application/json')
.header('X-Content-Type-Options', 'nosniff')
.header('X-Frame-Options', 'DENY');
This ensures headers are properly formatted and validated by Hapi's internal mechanisms.
Frequently Asked Questions
How does CRLF injection differ in Hapi compared to Express?
onPreResponse extensions and dynamic header manipulation that aren't present in Express's simpler middleware chain.