Out Of Bounds Read in Hapi
How Out Of Bounds Read Manifests in Hapi
In a Hapi application, an out‑of‑bounds read typically occurs when user‑supplied data is used as an index or length value without proper validation, causing the code to read memory (or a Buffer) beyond its allocated size. Because Hapi handlers receive raw request data through request.params, request.query, or request.payload, a missing validation step can let an attacker influence buffer operations.
Consider a route that extracts a numeric ID from the path and uses it to read a specific byte from a pre‑allocated Buffer that holds a shared secret:
const Hapi = require('@hapi/hapi');
const init = async () => {
const server = Hapi.server({ port: 3000 });
// 32‑byte secret buffer
const secret = Buffer.from('0123456789abcdef0123456789abcdef', 'hex');
server.route({
method: 'GET',
path: '/secret/{index}',
handler: (request, h) => {
// BUG: index is taken directly from the path, no bounds check
const idx = Number(request.params.index);
// If idx is negative or >= secret.length, Buffer.readUInt8 throws
const byte = secret.readUInt8(idx);
return { value: byte };
}
});
await server.start();
console.log('Server running at', server.info.uri);
};
init();
If an attacker supplies /secret/-1 or /secret/100, readUInt8 throws a RangeError, which Hapi treats as an unhandled rejection and returns a 500 error. While this may seem like a simple crash, the error message can leak stack traces, and repeated out‑of‑bounds reads can be used to probe the size of the buffer (information disclosure) or to cause a denial‑of‑service by triggering exceptions at high volume.
Another common pattern appears when handling binary payloads. Suppose a route expects a Buffer where the first two bytes indicate a length field that dictates how many subsequent bytes to slice:
server.route({
method: 'POST',
path: '/parse',
handler: (request, h) => {
const buf = request.payload; // assumed to be a Buffer
const len = buf.readUInt16BE(0); // attacker‑controlled length
// BUG: no validation that len <= buf.length - 2
const slice = buf.slice(2, 2 + len);
return slice.toString('utf8');
}
});
Here, a malicious client can set len to a value larger than the actual payload, causing slice to read past the end of the Buffer. In Node.js, Buffer.slice does not throw; it returns a Buffer that includes zero‑filled bytes beyond the original data, potentially exposing adjacent memory contents if the underlying SlowBuffer was not zero‑filled. This can lead to inadvertent disclosure of other process memory or application data.
Both examples illustrate how missing input validation in Hapi handlers can turn a routine data‑access operation into an out‑of‑bounds read vulnerability.
Hapi-Specific Detection
Detecting out‑of‑bounds reads in a Hapi service requires observing how user‑controlled values influence buffer or array accesses. Because the vulnerability is rooted in missing validation, static analysis alone is insufficient; dynamic probing is needed to confirm that a handler reacts abnormally when given extreme numeric inputs.
middleBrick performs unauthenticated, black‑box scanning of the API surface. When it encounters a Hapi endpoint, it:
- Extracts all path parameters, query strings, and body fields that are numeric or can be coerced to a number.
- Generates a series of probes: negative numbers, zero, very large integers (e.g., 2^32‑1), and values that exceed typical buffer sizes.
- Sends each probe to the endpoint and monitors the response:
- HTTP 500 with a
RangeErroror stack trace indicates an unhandled out‑of‑bounds read. - A successful response that returns unexpected data (e.g., extra null bytes, repeated patterns) can signal a silent over‑read via
Buffer.sliceor similar. - Changes in response length or timing that correlate with the probe value suggest a length‑driven read.
- Correlates findings with the 12 security checks, tagging the issue under Input Validation and, when applicable, Data Exposure.
For the first example (/secret/{index}), middleBrick would detect that supplying -1 or 100 triggers a 500 error with a RangeError: Offset is out of range message. For the binary payload example, it would notice that increasing the declared length field beyond the actual payload size returns a response padded with zeros, and that the response length grows linearly with the injected length—behaviour inconsistent with a properly bounded slice.
Because middleBrick does not require agents, credentials, or configuration, a security engineer can simply point the scanner at the base URL of the Hapi service (e.g., https://api.example.com) and receive a prioritized finding that includes:
- The exact URL and HTTP method probed.
- The parameter name (
indexorlen) that caused the over‑read. - Severity rating (typically Medium) based on the potential for information disclosure or denial‑of‑service.
- Remediation guidance that references Hapi‑specific validation techniques.
This automated check complements manual code review and ensures that any Hapi endpoint exposing buffer or array accesses is continuously validated throughout the development lifecycle.
Hapi-Specific Remediation
Fixing out‑of‑bounds reads in Hapi centers on validating all user‑supplied numeric values before they are used to index or slice buffers. Hapi’s built‑in validation (via Joi) makes this straightforward and keeps the validation logic close to the route definition.
**1. Validate path parameters with Joi**
Replace the raw Number(request.params.index) conversion with a Joi schema that enforces a valid range:
const Joi = require('@hapi/joi');
server.route({
method: 'GET',
path: '/secret/{index}',
options: {
validate: {
params: Joi.object({
index: Joi.number().integer().min(0).max(31).required()
})
}
},
handler: (request, h) => {
const idx = request.params.index; // already validated and cast to number
const byte = secret.readUInt8(idx);
return { value: byte };
}
});
If the supplied index falls outside 0‑31, Hapi returns a 400 Bad Request before the handler runs, preventing the RangeError.
**2. Validate payload‑driven lengths**
For binary payloads, validate the length field against the actual buffer size:
server.route({
method: 'POST',
path: '/parse',
options: {
validate: {
payload: Joi.binary().max(1024).required() // limit overall size
}
},
handler: (request, h) => {
const buf = request.payload;
if (buf.length < 2) {
return h.response('Payload too short').code(400);
}
const len = buf.readUInt16BE(0);
// Ensure length does not exceed available bytes
if (len > buf.length - 2) {
return h.response('Invalid length field').code(400);
}
const slice = buf.slice(2, 2 + len);
return slice.toString('utf8');
}
});
Here, the handler explicitly checks that len is not greater than buf.length - 2. If the check fails, a 400 response is returned, blocking the over‑read.
**3. Use safe alternatives when possible**
When the goal is to extract a substring from a Buffer, consider using buf.toString('utf8', start, end) which internally validates the bounds and throws a clear error if they are invalid. Wrapping the call in a try/catch allows you to convert the error into a user‑friendly 400 response.
try {
const text = buf.toString('utf8', 2, 2 + len);
return h.response(text);
} catch (err) {
if (err instanceof RangeError) {
return h.response('Invalid range').code(400);
}
throw err;
}
**4. Leverage Hapi’s lifecycle extensions for centralized validation**
If many routes share similar validation rules, you can register a onPreHandler extension that inspects request.params or request.query for numeric fields and applies a common schema. This reduces duplication and ensures consistent protection across the API.
By applying these Hapi‑native validation techniques—Joi schemas for path/query/payload, explicit bounds checks before Buffer operations, and safe API usage—you eliminate the root cause of out‑of‑bounds reads. The fixes are straightforward, do not require external libraries, and align with the principle that middleBrick only detects and reports; it is the developer’s responsibility to remediate using the platform’s built‑in security features.