Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
158 changes: 155 additions & 3 deletions library/helpers/extractStringsFromUserInput.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,12 +100,10 @@ t.test("it decodes JWTs", async () => {
}),
fromArr([
"token",
"iat",
"username",
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwidXNlcm5hbWUiOnsiJG5lIjpudWxsfSwiaWF0IjoxNTE2MjM5MDIyfQ._jhGJw9WzB6gHKPSozTFHDo9NOHs3CNOlvJ8rWy6VrQ",
"sub",
"1234567890",
"$ne",
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwidXNlcm5hbWUiOnsiJG5lIjpudWxsfSwiaWF0IjoxNTE2MjM5MDIyfQ._jhGJw9WzB6gHKPSozTFHDo9NOHs3CNOlvJ8rWy6VrQ",
"username",
"iat",
])
Expand Down Expand Up @@ -345,3 +343,157 @@ t.test("it works with objects containing constructor key", async () => {
fromArr(["test", "value", "constructor", "constructor value"])
);
});

t.test("it works with objects containing prototype key", async () => {
t.same(
extractStringsFromUserInput({
test: "value",
prototype: "prototype value",
}),
fromArr(["test", "value", "prototype", "prototype value"])
);

t.same(
extractStringsFromUserInput(
JSON.parse('{"test":"value","__proto__":{"protoKey":"protoValue"}}')
),
fromArr(["test", "value", "__proto__", "protoKey", "protoValue"])
);
});

t.test("it works with Map objects", async () => {
const map = new Map<string | number, string | object>();
map.set("key1", "value1");
map.set("key2", { nestedKey: "nestedValue" });
map.set(5, "value3");

t.same(
extractStringsFromUserInput(map),
fromArr(["key1", "value1", "key2", "nestedKey", "nestedValue", "value3"])
);
});

t.test("it works with Sets", async () => {
const set = new Set<string | object>();
set.add("value1");
set.add({ nestedKey: "nestedValue" });

t.same(
extractStringsFromUserInput(set),
fromArr(["value1", "nestedKey", "nestedValue"])
);
});

t.test("it works with URLSearchParams", async () => {
const params = new URLSearchParams();
params.append("key1", "value1");
params.append("key2", "value2");

t.same(
extractStringsFromUserInput(params),
fromArr(["key1", "value1", "key2", "value2"])
);
});

t.test(
"it works with FormData",
{
skip:
typeof globalThis.FormData === "undefined"
? "FormData is not supported in this environment"
: false,
},
async () => {
const formData = new globalThis.FormData();
formData.append("key1", "value1");
formData.append("key2", "value2");

t.same(
extractStringsFromUserInput(formData),
fromArr(["key1", "value1", "key2", "value2"])
);
}
);

t.test(
"it works with headers object",
{
skip:
typeof Headers === "undefined"
? "Headers is not supported in this environment"
: false,
},
async () => {
const headers = new Headers();
headers.append("Content-Type", "application/json");
headers.append("Authorization", "Bearer token");

t.same(
extractStringsFromUserInput(headers),
fromArr([
"content-type",
"application/json",
"authorization",
"Bearer token",
])
);
}
);

t.test("it works with class instances", async () => {
class Foo {
bar = "baz";
num = 42;
}
t.same(
extractStringsFromUserInput(new Foo()),
fromArr(["bar", "baz", "num"])
);

class Nested {
key = "value";
child = { nested: "data" };
}
t.same(
extractStringsFromUserInput(new Nested()),
fromArr(["key", "value", "child", "nested", "data"])
);
});

t.test("it works with wrongly used WeakMap", async () => {
const weakMap = new WeakMap();
// @ts-expect-error Ignore
weakMap["key1"] = "value1";
// @ts-expect-error Ignore
weakMap["key2"] = { nestedKey: "nestedValue" };

t.same(
extractStringsFromUserInput(weakMap),
fromArr(["key1", "value1", "key2", "nestedKey", "nestedValue"])
);
});

t.test("it does not call getters", async () => {
const obj = {
get secret() {
return "should not be called";
},
normalKey: "normalValue",
};

t.same(
extractStringsFromUserInput(obj),
fromArr(["normalKey", "normalValue"])
);
});

t.test("it does not call toString methods", async () => {
const obj = {
key: "value",
toString() {
throw new Error("toString should not be called");
},
};

t.same(extractStringsFromUserInput(obj), fromArr(["key", "value"]));
});
79 changes: 73 additions & 6 deletions library/helpers/extractStringsFromUserInput.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { isPlainObject } from "./isPlainObject";
import { safeDecodeURIComponent } from "./safeDecodeURIComponent";
import { tryDecodeAsJWT } from "./tryDecodeAsJWT";
import { tryParseURL } from "./tryParseURL";
Expand All @@ -21,13 +20,51 @@ export function extractStringsFromUserInput(
return results;
}

if (isPlainObject(obj)) {
for (const key in obj) {
if (obj instanceof Map) {
for (const [key, value] of obj.entries()) {
if (typeof key === "string") {
results.add(key);
}
extractStringsFromUserInput(value, depth + 1).forEach((v) =>
results.add(v)
);
}
return results;
}

if (obj instanceof Set) {
for (const value of obj.values()) {
extractStringsFromUserInput(value, depth + 1).forEach((v) =>
results.add(v)
);
}
return results;
}

if (obj instanceof URLSearchParams) {
for (const [key, value] of obj.entries()) {
results.add(key);
extractStringsFromUserInput(obj[key], depth + 1).forEach((value) => {
results.add(value);
});
results.add(value);
}
return results;
}

if (globalThis.FormData && obj instanceof globalThis.FormData) {
obj.forEach((value, key) => {
results.add(key);
if (typeof value === "string") {
results.add(value);
}
});
return results;
}

if (globalThis.Headers && obj instanceof globalThis.Headers) {
obj.forEach((value, key) => {
results.add(key);
results.add(value);
});
return results;
}
Comment thread
hansott marked this conversation as resolved.

if (Array.isArray(obj)) {
Expand All @@ -46,6 +83,7 @@ export function extractStringsFromUserInput(
// Ignore deeply nested/cyclic arrays that can overflow during native join recursion.
// We still keep strings gathered from traversed elements above.
}
return results;
}

if (typeof obj === "string" && obj.length > 0) {
Expand Down Expand Up @@ -75,6 +113,35 @@ export function extractStringsFromUserInput(
results.add(value);
});
}
return results;
}

if (typeof obj === "object" && obj !== null && !Array.isArray(obj)) {
try {
for (const key of Reflect.ownKeys(obj)) {
const descriptor = Object.getOwnPropertyDescriptor(obj, key);
if (!descriptor || !("value" in descriptor)) {
continue;
}

// Ignore function names
if (typeof descriptor.value === "function") {
continue;
}

if (typeof key === "string" && key.length > 0) {
results.add(key);
}

extractStringsFromUserInput(descriptor.value, depth + 1).forEach(
(value) => {
results.add(value);
}
);
}
} catch {
// Ignore errors
Comment on lines +142 to +143
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Silent catch in extractStringsFromUserInput swallows errors from the Reflect.ownKeys/Object.getOwnPropertyDescriptor loop; add logging or handle specific error cases instead of ignoring.

Show fix
Suggested change
} catch {
// Ignore errors
} catch (error) {
// Ignore errors from exotic objects/proxies that throw during reflection
console.warn("Failed to extract strings from object via reflection:", error);
Details

✨ AI Reasoning
​A new try/catch was added around reflective property iteration in extractStringsFromUserInput. The catch clause is a bare, silent catch that only contains a comment saying errors are ignored. Silent catches can mask unexpected failures and make debugging harder. The try block uses Reflect.ownKeys/Object.getOwnPropertyDescriptor to inspect arbitrary objects, which can throw for some proxies or exotic objects; swallowing those silently hides the cause.

This finding points to the empty/silent catch introduced for the reflective iteration. Consider adding targeted error handling, logging, or restricting which objects are inspected to avoid silent failures.

Reply @AikidoSec feedback: [FEEDBACK] to get better review comments in the future.
Reply @AikidoSec ignore: [REASON] to ignore this issue.
More info

}
}

return results;
Expand Down
Loading
Loading