fix(frida): preserve partial facts when redacted keys collide (#10071)

fix(frida): retain partial facts on redacted key collisions
This commit is contained in:
morluto authored and GitHub committed 2026-11-11 18:18:15 +08:00
1 parent 50740bb05e
commit b4d501e714
2 files changed
+78

No files matched your search

@@ -1457,6 +1457,79 @@ it("redacts remote credentials echoed by a target script", async () => {
await manager.closeAll();
});
it.each([
{
label: "colliding redacted names",
declared: true,
secondName: "[redacted]",
truncated: true,
},
{
label: "distinct redacted names",
declared: true,
secondName: "other",
truncated: false,
},
{
label: "same marker spelling without a declared secret",
declared: false,
secondName: "[redacted]",
truncated: false,
},
])(
"preserves captured object facts with $label",
async ({ declared, secondName, truncated }) => {
const token = "synthetic-key-token";
const bindings = new FakeDeviceManager();
bindings.device.session.nextPayload = {
[token]: "first observation",
[secondName]: "second observation",
ordinary: "third observation",
};
const manager = new FridaInstrumentationManager({
deviceManager: bindings,
});
onTestFinished(() => manager.closeAll());
const started = await manager.startSession({
mode: "attach",
remote: { address: "127.0.0.1:27042", ...(declared ? { token } : {}) },
pid: 512,
});
if (!started.ok) throw started.error;
const loaded = await new FridaInstrumentationService(manager).loadScript(
started.value.sessionId,
{ sourceKind: "inline", source: "send(payload)" },
);
if (!loaded.ok) throw loaded.error;
const result = jsonObjectSchema.parse(loaded.value.normalized_result);
if (!Array.isArray(result.messages))
throw new Error("Expected captured messages");
const message = jsonObjectSchema.parse(result.messages[0]);
expect(message.payload).toEqual(
truncated
? { "[redacted]": "first observation" }
: {
[declared ? "[redacted]" : token]: "first observation",
[secondName]: "second observation",
ordinary: "third observation",
},
);
expect(message.type).toBe("send");
expect(message.script_id).toBe(result.script_id);
expect(message.payload_truncated ?? false).toBe(truncated);
if (truncated)
expect(message.payload_capture_limit).toBe("unrepresentable");
else expect(message).not.toHaveProperty("payload_capture_limit");
expect(
loaded.value.limitations.some((text) =>
text.includes("could not be represented as JSON"),
),
).toBe(truncated);
if (declared) expect(JSON.stringify(loaded.value)).not.toContain(token);
else expect(JSON.stringify(loaded.value)).toContain(token);
},
);
it.each([
{ captureLimits: undefined, retained: 32_768, truncated: true },
{
+5
View File
@@ -1537,6 +1537,11 @@ const boundedJsonValue = (
break;
}
const key = boundedKey.value;
// Redaction can collapse source names; never replace a retained fact.
if (Object.hasOwn(output, key)) {
limit ??= "unrepresentable";
break;
}
const keyBytes = jsonStringByteLength(key) + 1;
const remaining = budget - usedBytes - separatorBytes - keyBytes - 1;
if (remaining < 8) {