fix(player): reject non-finite composition dimensions from attributes and stage-size (#1205)

width/height attributes went through parseInt with no validation, so a
typo like width="abc" reached scaleIframeToFit as NaN (invalid
scale(NaN) transform) and width="0" as a division by zero — both
blank the player with no signal. The stage-size message check had the
sibling gap: `> 0` alone lets Infinity through, which scales the
iframe to 0.

Reuse the composition probe's readPositiveDimension guard for the
attribute path (the probe path already rejected these) and add the
same finite-check the adjacent timeline branch uses for stage-size.
Mirrors the clampPlaybackRate hardening from #1120.

Co-authored-by: Carlos Alcaraz <193642530+calcarazgre646@users.noreply.github.com>
This commit is contained in:
Carlos Alcaraz Gregor
2026-06-16 23:43:00 -07:00
committed by GitHub
co-authored by Carlos Alcaraz
parent 937ba2cebe
commit 513819ee84
5 changed files with 147 additions and 4 deletions
@@ -0,0 +1,67 @@
import { describe, expect, it, vi } from "vitest";
import { handleRuntimeMessage, type MessageHandlerCallbacks } from "./runtime-message-handler.js";
import type { ParentMediaManager } from "./parent-media.js";
import type { ShaderLoaderState } from "./shader-loader-state.js";
// Only the stage-size branch is exercised here; the rest of the callback
// surface is satisfied with inert spies so the handler's type contract
// stays honest without pulling in the real player.
const makeCallbacks = (): MessageHandlerCallbacks => ({
updateControlsTime: vi.fn(),
updateControlsPlaying: vi.fn(),
dispatchEvent: vi.fn(),
seek: vi.fn(),
play: vi.fn(),
getLoop: vi.fn(() => false),
media: { mirrorTime: vi.fn(), promoteToParentProxy: vi.fn() } as unknown as ParentMediaManager,
getPlaybackState: vi.fn(() => ({ currentTime: 0, duration: 0, paused: true, lastUpdateMs: 0 })),
setPlaybackState: vi.fn(),
getShaderLoadingMode: vi.fn(() => "auto"),
shaderLoader: { update: vi.fn() } as unknown as ShaderLoaderState,
setCompositionSize: vi.fn(),
sendControl: vi.fn(),
getIframeDoc: vi.fn(() => null),
});
const stageSizeEvent = (width: unknown, height: unknown, source: object): MessageEvent =>
({
source,
data: { source: "hf-preview", type: "stage-size", width, height },
}) as unknown as MessageEvent;
describe("handleRuntimeMessage stage-size", () => {
it("applies a finite positive stage size", () => {
const frameWindow = {} as Window;
const callbacks = makeCallbacks();
handleRuntimeMessage(stageSizeEvent(1280, 720, frameWindow), frameWindow, callbacks);
expect(callbacks.setCompositionSize).toHaveBeenCalledWith(1280, 720);
});
it.each([
["Infinity width", Infinity, 720],
["Infinity height", 1280, Infinity],
["NaN width", NaN, 720],
["zero width", 0, 720],
["negative height", 1280, -720],
["string width", "1280", 720],
])("ignores stage-size with %s", (_label, width, height) => {
const frameWindow = {} as Window;
const callbacks = makeCallbacks();
handleRuntimeMessage(stageSizeEvent(width, height, frameWindow), frameWindow, callbacks);
expect(callbacks.setCompositionSize).not.toHaveBeenCalled();
});
it("ignores messages from a different source window", () => {
const frameWindow = {} as Window;
const callbacks = makeCallbacks();
handleRuntimeMessage(stageSizeEvent(1280, 720, {}), frameWindow, callbacks);
expect(callbacks.setCompositionSize).not.toHaveBeenCalled();
});
});