Pick
Build a new object holding only the keys you named. Assigning a request body onto a record takes every key the request happened to carry, which is how a form that edits a display name also sets isAdmin. Naming the keys inverts that: what was not named is not there.
The keys that move or read a prototype, __proto__, constructor and prototype, are dropped whether or not they were named, so an allow-list cannot be written that lets one through. The result has a null prototype internally and is copied onto a plain object, so nothing inherited comes with it.
Nesting is by [name, [keys]], and the same rules apply at every level. An array is filtered element by element, so a body an attacker sent as a list is no way around the allow-list, and the shape of the object does not matter: a class instance and an object from another realm are filtered the same way a literal is.
b.pick.registerPoisonedKeys(keys) #
Add key names to the set every caller treats as unsafe. A deployment whose object layer gives another name the power __proto__ has adds it here once, and every b.pick call and every assertSafeKey check refuses it from then on.
The set only grows: there is no removal, so a name registered as unsafe cannot be made safe again by later code.
var b = require("@blamejs/core");
b.pick.registerPoisonedKeys(["$where"]);
b.pick.isPoisonedKey("$where"); // → true
b.pick.isPoisonedKey(key) #
Is this key one of the names that must never be assigned from untrusted input? True for __proto__, constructor and prototype, and for anything b.pick.registerPoisonedKeys has added. False for a value that is not a string.
var b = require("@blamejs/core");
b.pick.isPoisonedKey("__proto__"); // → true
b.pick.isPoisonedKey("name"); // → false
b.pick.movesThePrototype(key) #
Is this the one key whose assignment replaces an object's prototype? True only for __proto__.
constructor and prototype are unsafe to copy and do not by themselves move a prototype, so a caller that has to tell the two apart, to report which happened, asks this rather than isPoisonedKey.
var b = require("@blamejs/core");
b.pick.movesThePrototype("__proto__"); // → true
b.pick.movesThePrototype("constructor"); // → false
b.pick.assertSafeKey(key, onPoisoned) #
Call onPoisoned(key) when the key is unsafe, and answer undefined when it is not. The caller decides what happens, because a parser building a value wants to skip the key while an options reader wants to throw.
onPoisoned must be a function; anything else throws a TypeError here, rather than leaving an unsafe key unhandled because the handler was missing.
var b = require("@blamejs/core");
var refused = [];
b.pick.assertSafeKey("__proto__", function (k) { refused.push(k); });
b.pick.assertSafeKey("name", function (k) { refused.push(k); });
refused;
// → ["__proto__"]
b.pick(input, allowList, opts?) #
{
onUnknown: string, // "drop" (default) or "throw"
}
Answer a new object carrying only the allowed keys of input. An entry in allowList is a key name, or [name, [keys]] to allow a nested object and name what may come out of it.
A key that is not on the list is dropped, or throws when onUnknown: "throw", which is the choice between accepting a request that carried more than it should and telling the caller it did.
__proto__, constructor and prototype never come through, and naming one on the allow-list does not change that.
An array is filtered element by element against the same list. Anything that is not an object comes back as it is, so a caller can hand the result of a parse straight in whatever it turned out to be.
Every object is filtered, whatever its prototype: a class instance keeps only the allowed own keys and brings none of its prototype, and an object built in another realm, by a node:vm context for instance, is treated as the object it is rather than passed through.
var b = require("@blamejs/core");
b.pick({ name: "ada", isAdmin: true }, ["name"]);
// → { name: "ada" }
b.pick({ name: "ada", profile: { bio: "x", role: "root" } },
["name", ["profile", ["bio"]]]);
// → { name: "ada", profile: { bio: "x" } }