From 852d1231d64270c7292dfdd0de8965e10a50d163 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Wed, 8 Jul 2026 11:49:41 +0000 Subject: [PATCH] =?UTF-8?q?=F0=9F=94=92=20Fix=20application=20crash=20on?= =?UTF-8?q?=20unhandled=20Symbol=20properties=20in=20proxies?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: johnstrand <11484777+johnstrand@users.noreply.github.com> --- src/Squawk.ts | 8 +++- src/__tests__/Squawk.proxy.test.ts | 60 ++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 2 deletions(-) create mode 100644 src/__tests__/Squawk.proxy.test.ts diff --git a/src/Squawk.ts b/src/Squawk.ts index 8dde883..01a0b5d 100644 --- a/src/Squawk.ts +++ b/src/Squawk.ts @@ -330,7 +330,9 @@ export default function createStore(initialState: Required, useReduxDevToo () => new Proxy(localPending, { get(_, prop) { - contexts.current.add(prop as StoreProp); + if (typeof prop === "string") { + contexts.current.add(prop as StoreProp); + } return localPending[prop as StoreProp]; } }), @@ -382,7 +384,9 @@ export default function createStore(initialState: Required, useReduxDevToo () => new Proxy(localState, { get(_, prop) { - contexts.current.add(prop as StoreProp); + if (typeof prop === "string") { + contexts.current.add(prop as StoreProp); + } return localState[prop as StoreProp]; } }), diff --git a/src/__tests__/Squawk.proxy.test.ts b/src/__tests__/Squawk.proxy.test.ts new file mode 100644 index 0000000..9841a78 --- /dev/null +++ b/src/__tests__/Squawk.proxy.test.ts @@ -0,0 +1,60 @@ +import createStore from "../Squawk"; +import * as React from "react"; + +// Mock React hooks to simulate hook environment +jest.mock("react", () => { + const actualReact = jest.requireActual("react"); + return { + ...actualReact, + useState: jest.fn((init) => [typeof init === 'function' ? init() : init, jest.fn()]), + useRef: jest.fn((init) => ({ current: init })), + useMemo: jest.fn((factory) => factory()), + useEffect: jest.fn(), + }; +}); + +describe("Squawk proxy vulnerability", () => { + it("does not crash or add Symbol properties to contexts in usePending", () => { + // Clear mocks between tests + jest.clearAllMocks(); + + const store = createStore({ foo: "bar", baz: 123 }); + const pending = store.usePending(); + + // Access a Symbol property on the returned proxy + const sym = Symbol("test"); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const value = pending[sym]; + expect(value).toBeUndefined(); + + // Since we mocked useRef, we can check its calls + const useRefCalls = (React.useRef as jest.Mock).mock.calls; + // useIfMounted has 2 useRef calls, usePending has 1 + const contextsRef = useRefCalls[2][0]; // explicitContexts set + + expect(contextsRef).toBeInstanceOf(Set); + expect(contextsRef.has(sym)).toBe(false); + }); + + it("does not crash or add Symbol properties to contexts in useSquawk", () => { + // Clear mocks between tests + jest.clearAllMocks(); + + const store = createStore({ foo: "bar", baz: 123 }); + const state = store.useSquawk(); + + // Access a Symbol property on the returned proxy + const sym = Symbol("test"); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const value = state[sym]; + expect(value).toBeUndefined(); + + const useRefCalls = (React.useRef as jest.Mock).mock.calls; + const contextsRef = useRefCalls[2][0]; // explicitContexts set + + expect(contextsRef).toBeInstanceOf(Set); + expect(contextsRef.has(sym)).toBe(false); + }); +});