Summary
api.slothlet.api.add(path, fn, …) splits its dotted path argument into segments and writes each into a nested object without guarding the prototype-chain segment names. A path containing __proto__ or constructor.prototype therefore mutates Object.prototype (and Function.prototype) globally, not the api tree — classic prototype pollution.
This is distinct from the documented __-prefixed export name convention (module-private members): here __proto__ is a mount-path segment, and the effect is global object-graph contamination, not an api member named __proto__.
Reproduce
Verified against @cldmv/slothlet@3.14.0 (repo v3.13.3-9-g0dd03b7), eager mode, empty api dir:
const api = await slothlet({ base: emptyDirUrl, silent: true, mode: "eager" });
await api.slothlet.api.add("__proto__.x", () => "polluted", { moduleID: "p" });
// no throw
({}).x; // → [Function (anonymous)] ← a fresh, unrelated object is contaminated
Object.create(Object.prototype).x; // → the function too
await api.slothlet.api.add("constructor.prototype.pwn", () => "x", { moduleID: "p2" });
({}).pwn; // → function
Expected
A mount-path segment of __proto__, constructor, or prototype should be refused with a named error (the same treatment reserved names already get — MODULE_RESERVED_EXPORT / a new MODULE_UNSAFE_PATH), never written through to the real prototype chain. slothlet already ships sanitizePropertyName and guards slothlet/shutdown/destroy; the dotted-path split in add() appears to bypass that guard for these three segments.
Impact
Any consumer that mounts from a path string it doesn't fully control is exposed to global prototype pollution. It surfaced while building @cldmv/slothlet-vine, which mounts forwarding leaves at paths taken from a remote peer — vine guards these segments on its own side, but the underlying add() allowing them is a latent footgun for every consumer.
Severity
Security — prototype pollution. Low exploitability where mount paths are always literal/trusted, but the fix is a cheap segment allowlist and the failure is silent + global.
Summary
api.slothlet.api.add(path, fn, …)splits its dottedpathargument into segments and writes each into a nested object without guarding the prototype-chain segment names. A path containing__proto__orconstructor.prototypetherefore mutatesObject.prototype(andFunction.prototype) globally, not the api tree — classic prototype pollution.This is distinct from the documented
__-prefixed export name convention (module-private members): here__proto__is a mount-path segment, and the effect is global object-graph contamination, not an api member named__proto__.Reproduce
Verified against
@cldmv/slothlet@3.14.0(repov3.13.3-9-g0dd03b7), eager mode, empty api dir:Expected
A mount-path segment of
__proto__,constructor, orprototypeshould be refused with a named error (the same treatment reserved names already get —MODULE_RESERVED_EXPORT/ a newMODULE_UNSAFE_PATH), never written through to the real prototype chain. slothlet already shipssanitizePropertyNameand guardsslothlet/shutdown/destroy; the dotted-path split inadd()appears to bypass that guard for these three segments.Impact
Any consumer that mounts from a path string it doesn't fully control is exposed to global prototype pollution. It surfaced while building
@cldmv/slothlet-vine, which mounts forwarding leaves at paths taken from a remote peer — vine guards these segments on its own side, but the underlyingadd()allowing them is a latent footgun for every consumer.Severity
Security — prototype pollution. Low exploitability where mount paths are always literal/trusted, but the fix is a cheap segment allowlist and the failure is silent + global.