Critical severity9.8NVD Advisory· Published Aug 12, 2024· Updated Apr 15, 2026
CVE-2024-38989
CVE-2024-38989
Description
izatop bunt v0.29.19 was discovered to contain a prototype pollution via the component /esm/qs.js. This vulnerability allows attackers to execute arbitrary code or cause a Denial of Service (DoS) via injecting arbitrary properties.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
@bunt/appnpm | < 0.29.26 | 0.29.26 |
Patches
13 files changed · +50 −23
packages/app/src/TransformRequest/MultipartFormDataTransform.ts+4 −5 modified@@ -17,10 +17,9 @@ export const MultipartFormDataTransform = async <T = unknown>(request: IRequest) const rs = await request.createReadableStream(); const defer = new Defer<void>(); - const result: Record<string, any> = {}; const pending: Defer<void>[] = []; - const {parseFieldName, inject} = QueryString; + const qs = new QueryString(); bb .on("file", (name, file, info) => { @@ -37,7 +36,7 @@ export const MultipartFormDataTransform = async <T = unknown>(request: IRequest) tmpname, }; - inject(parseFieldName(name), value, result); + qs.push(name, value); const def = new Defer<void>(); pending.push(def); @@ -48,7 +47,7 @@ export const MultipartFormDataTransform = async <T = unknown>(request: IRequest) }) .on("field", (name, value) => { try { - inject(parseFieldName(name), JSON.parse(value), result); + qs.push(name, JSON.parse(value)); } catch { // skip } @@ -60,5 +59,5 @@ export const MultipartFormDataTransform = async <T = unknown>(request: IRequest) await defer; await Promise.all(pending); - return result as any; + return qs.toObject(); };
packages/util/src/qs.ts+23 −4 modified@@ -1,15 +1,34 @@ +import {Rec} from "@bunt/type"; + const isNumeric = (key: string): boolean => !isNaN(+key); export class QueryString { - public static parseFieldName = (name: string): string[] => { + readonly #value: Rec; + + constructor(entries: [field: string, value: unknown][] = []) { + this.#value = Object.create(null); + for (const [field, value] of entries) { + this.push(field, value); + } + } + + public parseField(name: string): string[] { const base = name.replace(/\[.+/, ""); return [base, ...[...name.matchAll(/\[([^\]]*)\]/ig)].map(([, key]) => key)]; - }; + } + + public push(name: string, value: unknown): Rec { + return this.#inject(this.parseField(name), value, this.#value); + } + + public toObject(): Rec { + return this.#value; + } - public static inject = ([key, ...paths]: string[], value: unknown, fields: any = {}): any => { + #inject = ([key, ...paths]: string[], value: unknown, fields: Rec = Object.create(null)): Rec => { if (paths.length > 0) { - fields[key] = this.inject(paths, value, fields[key]); + fields[key] = this.#inject(paths, value, fields[key]); } else { fields[key] = value; }
packages/util/test/src/qs/QueryStirng.test.ts+23 −14 modified@@ -1,57 +1,66 @@ import {QueryString} from "../../../src"; describe("QueryString", () => { + test("Prevent pollution", () => { + const injectTest = {}; + const injectKey = "__proto__[polluted]"; + + const qs = new QueryString(); + qs.push(injectKey, true); + + expect(Reflect.has(Reflect.get(injectTest, "__proto__"), "polluted")).toBeFalsy(); + }); + test("Base", () => { + const qs = new QueryString(); const field = "foo[bar][baz][0]"; - const parsed = QueryString.parseFieldName(field); + const parsed = qs.parseField(field); expect(parsed) .toEqual(["foo", "bar", "baz", "0"]); - expect(QueryString.inject(parsed, 1)) + expect(qs.push(field, 1)) .toEqual({foo: {bar: {baz: [1]}}}); }); test("Array", () => { + const qs = new QueryString(); const map: [string, any][] = [ ["foo[0]", 1], ["foo[1]", 2], ["foo[2]", 3], ]; - const result = {}; for (const [field, value] of map) { - const paths = QueryString.parseFieldName(field); - QueryString.inject(paths, value, result); + qs.push(field, value); } - expect(result).toEqual({foo: [1, 2, 3]}); + expect(qs.toObject()).toEqual({foo: [1, 2, 3]}); }); test("Nested array", () => { + const qs = new QueryString(); const map: [string, any][] = [ ["foo[0][a]", 1], ["foo[0][b]", 2], ["foo[1][c]", 3], ["foo[1][d]", 4], ]; - const result = {}; for (const [field, value] of map) { - const paths = QueryString.parseFieldName(field); - QueryString.inject(paths, value, result); + qs.push(field, value); } - expect(result).toEqual({foo: [{a: 1, b: 2}, {c: 3, d: 4}]}); + expect(qs.toObject()).toEqual({foo: [{a: 1, b: 2}, {c: 3, d: 4}]}); }); test.each([ ["foo", 1, {foo: 1}], ["foo[bar]", 1, {foo: {bar: 1}}], ["foo[bar][0]", 1, {foo: {bar: [1]}}], ["foo[0][bar]", 1, {foo: [{bar: 1}]}], - ])("Variants", (field, value, res) => ( - expect(QueryString.inject(QueryString.parseFieldName(field), value)) - .toEqual(res) - )); + ])("Variants", (field, value, res) => { + const qs = new QueryString([[field, value]]); + expect(qs.toObject()).toEqual(res); + }); });
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
5News mentions
0No linked articles in our index yet.