mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-25 20:05:46 -06:00
fix: reject malformed UTF-8 in APNs relay response bodies (#111769)
* fix: reject malformed UTF-8 in APNs relay response bodies
Add { fatal: true } to TextDecoder in push-apns.relay.ts so invalid
UTF-8 bytes throw a TypeError before JSON.parse, instead of silently
becoming U+FFFD inside the parsed JSON payload.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(relay): add invalid UTF-8 relay body regression test
Verify that a relay response containing a malformed UTF-8 byte is
rejected at decode time and treated as absent, falling back to the
HTTP status-derived result instead of leaking corrupted field values.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(apns): exercise malformed relay metadata
Co-authored-by: chenyangjun-xy <chen.yangjun@xydigit.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
@@ -456,5 +456,34 @@ describe("push-apns.relay", () => {
|
||||
expect(result.reason).toBe("RelayResponseTooLarge");
|
||||
expect(result.status).toBe(202);
|
||||
});
|
||||
|
||||
it("rejects relay body with malformed UTF-8 bytes instead of parsing corrupted metadata", async () => {
|
||||
// Regression guard: with { fatal: true } on the TextDecoder, a relay body
|
||||
// containing invalid UTF-8 sequences must be rejected at decode time and
|
||||
// treated as absent (status-derived fallback). Corrupted field values must
|
||||
// never reach the caller.
|
||||
const encoder = new TextEncoder();
|
||||
const prefix = encoder.encode('{"ok":true,"status":200,"apnsId":"test-');
|
||||
const suffix = encoder.encode('1234"}');
|
||||
const body = new Uint8Array(prefix.length + 1 + suffix.length);
|
||||
body.set(prefix, 0);
|
||||
body[prefix.length] = 0xff; // bare invalid byte inside the apns-id string
|
||||
body.set(suffix, prefix.length + 1);
|
||||
|
||||
const fetchMock = vi
|
||||
.fn()
|
||||
.mockResolvedValue(
|
||||
new Response(body, { status: 202, headers: { "content-type": "application/json" } }),
|
||||
);
|
||||
vi.stubGlobal("fetch", fetchMock as unknown as typeof fetch);
|
||||
|
||||
await expect(sendApnsRelayPush(createRelayPushParams())).resolves.toEqual({
|
||||
ok: true,
|
||||
status: 202,
|
||||
apnsId: undefined,
|
||||
reason: undefined,
|
||||
tokenSuffix: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -309,7 +309,7 @@ async function sendApnsRelayRequest(params: {
|
||||
const buffer = await readResponseWithLimit(response, APNS_RELAY_MAX_RESPONSE_BYTES, {
|
||||
onOverflow: ({ size, maxBytes }) => new ApnsRelayResponseTooLargeError(size, maxBytes),
|
||||
});
|
||||
json = JSON.parse(new TextDecoder("utf-8").decode(buffer)) as unknown;
|
||||
json = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(buffer)) as unknown;
|
||||
} catch (err) {
|
||||
if (err instanceof ApnsRelayResponseTooLargeError) {
|
||||
// Fail closed: an oversized relay body must never be reported as a delivered push.
|
||||
|
||||
Reference in New Issue
Block a user