Why toString turned into an array in two Node.js HTTP adapters
Inversify is a dependency injection library for TypeScript. Its HTTP packages have adapters for Express, Fastify, Hono and uWebSockets. I found this bug while comparing how the adapters parse query strings and form bodies.
The bug
The Hono and uWebSockets adapters collected form fields in a plain object, and the uWebSockets adapter did the same for query parameters. To support repeated keys such as tag=a&tag=b, the parser checked whether a key already had a value.
if (parsedBody[key] === undefined) {
parsedBody[key] = value;
} else if (Array.isArray(parsedBody[key])) {
parsedBody[key].push(value);
} else {
parsedBody[key] = [parsedBody[key], value];
}A plain object inherits from Object.prototype. For a key like toString or constructor, parsedBody[key] is not undefined but the inherited function. The first value was treated as a repeat, and the result was an array with the function and the value. The Express adapter returned toString: 'x' for the same request.
The fix
The uWebSockets query is now collected in an object made with Object.create(null). It has no prototype, like the query objects of the other adapters. The body parsers keep a plain object, check keys with Object.hasOwn and add new keys with Object.defineProperty.
const existingValue = Object.hasOwn(parsedBody, key) ? parsedBody[key] : undefined;
if (existingValue === undefined) {
Object.defineProperty(parsedBody, key, {
configurable: true,
enumerable: true,
value,
writable: true,
});
} else if (Array.isArray(existingValue)) {
existingValue.push(value);
} else {
parsedBody[key] = [existingValue, value];
}The defineProperty call matters for one key. A plain assignment such as parsedBody['__proto__'] = 'z' does not create a field. It calls the __proto__ setter, which ignores a string, so the field disappears without an error. defineProperty always creates an own property.
The maintainer asked for tests with a repeated prototype key and __proto__. The final tests send toString=x&toString=y&__proto__=z and expect toString to be ["x", "y"] and __proto__ to be "z". Without defineProperty the __proto__ test fails.
What to take from it
When you build an object from user input, check keys with Object.hasOwn instead of obj[key] === undefined. If the object never needs prototype methods, start from Object.create(null).
The fix was merged in inversify/monorepo#2166.