From b4d501e71403c2ba1bd9fa3def6bb0ccc30b4843 Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Wed, 11 Nov 2026 18:18:15 +0800 Subject: [PATCH] fix(frida): preserve partial facts when redacted keys collide (#10071) fix(frida): retain partial facts on redacted key collisions --- src/frida/FridaInstrumentationManager.test.ts | 73 +++++++++++++++++++ src/frida/FridaInstrumentationManager.ts | 5 ++ 2 files changed, 78 insertions(+) diff --git a/src/frida/FridaInstrumentationManager.test.ts b/src/frida/FridaInstrumentationManager.test.ts index 28549e9745..71af2b800a 100644 --- a/src/frida/FridaInstrumentationManager.test.ts +++ b/src/frida/FridaInstrumentationManager.test.ts @@ -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 }, { diff --git a/src/frida/FridaInstrumentationManager.ts b/src/frida/FridaInstrumentationManager.ts index 0d0bbae941..f453f6c479 100644 --- a/src/frida/FridaInstrumentationManager.ts +++ b/src/frida/FridaInstrumentationManager.ts @@ -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) {