fix(browser): reject inexact storage values

This commit is contained in:
morluto committed 2026-10-27 09:45:11 +08:00
1 parent f3a301eca5
commit 15a8f6847c
3 files changed
+69 -18

No files matched your search

+9 -5
View File
@@ -30,6 +30,14 @@ const pointerTokens = (pointer: string): readonly string[] =>
.split("/")
.map((token) => token.replaceAll("~1", "/").replaceAll("~0", "~"));
/** Keep unrelated fields usable while marking numbers that cannot be selected exactly. */
export const parseScenarioValueJson = (text: string) =>
safeParseJson(text, (_key, value, context) =>
typeof value === "number" && !isExactJsonNumber(value, context?.source)
? Number.NaN
: value,
);
/** Exact own-property lookup: an absent pointer remains distinct from a non-scalar value. */
export const responsePointerValue = (
value: unknown,
@@ -82,11 +90,7 @@ export const readResponseValues = (
}
if (text.length > MAX_SAVED_RESPONSE_CHARACTERS)
return err(issue("the response is too large to save values from."));
const parsed = safeParseJson(text, (_key, value, context) =>
typeof value === "number" && !isExactJsonNumber(value, context?.source)
? Number.NaN
: value,
);
const parsed = parseScenarioValueJson(text);
if (!parsed.ok)
return err(
issue("the response is not JSON, so no value can be saved from it."),
@@ -584,6 +584,54 @@ describe("request step confinement", () => {
expect(calls.at(-1)!.url, path).toBe(`${ORIGIN}${sent}`);
}
});
});
describe("request page value admission", () => {
afterEach(() => {
vi.unstubAllGlobals();
});
it("preserves exact local storage pointer values and refuses lossy numbers before dispatch", async () => {
const calls = stubFetch(200);
const parsed = scenario([
requestStep({
headers: [
{
name: "X-Account",
value: {
source: "local_storage",
key: "auth",
json_pointer: "/id",
},
},
],
}),
]);
for (const literal of ["9007199254740993", "1e400", "-0"]) {
await expect(
perform(
fakePage({ storage: { auth: `{"id":${literal}}` } }),
parsed.actions[0]!,
resolved(parsed),
),
).rejects.toThrow("cannot be read exactly");
}
expect(calls).toEqual([]);
for (const [literal, expected] of [
["9007199254740991", "9007199254740991"],
['"9007199254740993"', "9007199254740993"],
["42", "42"],
]) {
await perform(
fakePage({
storage: { auth: `{"id":${literal},"unselected":9007199254740993}` },
}),
parsed.actions[0]!,
resolved(parsed),
);
expect(calls.at(-1)!.init.headers).toEqual([["X-Account", expected]]);
}
});
it("refuses page values a header cannot carry, and stored values it cannot read", async () => {
const calls = stubFetch(200);
+12 -13
View File
@@ -7,6 +7,7 @@ import {
MAX_SAVED_RESPONSE_CHARACTERS,
MAX_SAVED_RESPONSE_BYTES,
responsePointerValue,
parseScenarioValueJson,
readResponseValues,
readBoundedResponseBody,
} from "./BrowserScenarioResponseValues.js";
@@ -46,14 +47,6 @@ const originOf = (url: string): string | undefined => {
}
};
/** The string or number at a JSON pointer, as text. */
const pointerText = (value: unknown, pointer: string): string | undefined => {
const found = responsePointerValue(value, pointer);
return typeof found === "string" || typeof found === "number"
? String(found)
: undefined;
};
interface PageValueStep {
readonly page: Page;
readonly url: URL;
@@ -105,15 +98,21 @@ const readPageValue = async (
);
if (value.json_pointer === undefined) read = stored;
else {
let parsed: unknown;
try {
parsed = JSON.parse(stored);
} catch {
const parsed = parseScenarioValueJson(stored);
if (!parsed.ok) {
throw new ScenarioRequestError(
`Request step ${stepId}: local storage ${value.key} is not JSON.`,
);
}
read = pointerText(parsed, value.json_pointer);
const selected = responsePointerValue(parsed.value, value.json_pointer);
if (typeof selected === "number" && !Number.isFinite(selected))
throw new ScenarioRequestError(
`Request step ${stepId}: local storage ${value.key} has a number at ${value.json_pointer} that cannot be read exactly.`,
);
read =
typeof selected === "string" || typeof selected === "number"
? String(selected)
: undefined;
if (read === undefined)
throw new ScenarioRequestError(
`Request step ${stepId}: local storage ${value.key} has no string or number at ${value.json_pointer}.`,