fix(engine): validate remote download integrity (#2938)

This commit is contained in:
James Russo
2026-08-04 18:02:01 -07:00
committed by GitHub
parent 7bf425b7a9
commit bbdfee1166
11 changed files with 2219 additions and 133 deletions
@@ -85,6 +85,22 @@ describe("discoverMediaFromBrowser", () => {
});
});
function validTestMediaResponse(): Response {
const bytes = new Uint8Array([
0, 0, 0, 24, 0x66, 0x74, 0x79, 0x70, 0x69, 0x73, 0x6f, 0x6d, 0, 0, 0, 0, 0x69, 0x73, 0x6f, 0x6d,
0x6d, 0x70, 0x34, 0x32,
]);
return new Response(bytes, { status: 200 });
}
function validTestImageResponse(): Response {
const png = Buffer.from(
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=",
"base64",
);
return new Response(png, { status: 200 });
}
describe("injectSdkPositionEditsRenderScript", () => {
it("injects before </body> when SDK position-edit markers are present", () => {
const html =
@@ -1424,7 +1440,7 @@ describe("localizeRemoteMediaSources", () => {
it("rewrites remote <video> src to _remote_media path when download succeeds", async () => {
const orig = globalThis.fetch;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(globalThis as any).fetch = async () => new Response(new Uint8Array(100), { status: 200 });
(globalThis as any).fetch = async () => validTestMediaResponse();
try {
const dl = mkdtempSync(join(tmpdir(), "hf-dl-ok-"));
const html = `<video id="v1" src="https://media-ok.example.com/a/clip.mp4" data-start="0" data-end="10" muted></video>`;
@@ -1447,13 +1463,42 @@ describe("localizeRemoteMediaSources", () => {
expect(remoteMediaAssets.size).toBe(0);
});
it("logs only a safe fingerprint and host for a signed-URL media failure", async () => {
const originalFetch = globalThis.fetch;
const originalWarn = defaultLogger.warn;
const warnings: unknown[] = [];
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(globalThis as any).fetch = async () =>
new Response("<!doctype html><html><body>expired</body></html>", { status: 200 });
defaultLogger.warn = (message, meta) => warnings.push({ message, meta });
try {
const dl = mkdtempSync(join(tmpdir(), "hf-dl-safe-log-"));
const url = "https://cdn.example/private/customer.mp4?X-Amz-Signature=super-secret-signature";
const html = `<video id="v1" src="${url}" data-start="0" data-end="10"></video>`;
const { html: result, remoteMediaAssets } = await localizeRemoteMediaSources(html, dl);
expect(result).toContain(url);
expect(remoteMediaAssets.size).toBe(0);
expect(warnings).toHaveLength(1);
const serialized = JSON.stringify(warnings);
expect(serialized).toContain("cdn.example");
expect(serialized).toContain("urlFingerprint");
expect(serialized).not.toContain("customer.mp4");
expect(serialized).not.toContain("super-secret-signature");
} finally {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(globalThis as any).fetch = originalFetch;
defaultLogger.warn = originalWarn;
}
});
it("deduplicates: two tags with the same src URL → one download", async () => {
const orig = globalThis.fetch;
let fetchCount = 0;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(globalThis as any).fetch = async () => {
fetchCount++;
return new Response(new Uint8Array(100), { status: 200 });
return validTestMediaResponse();
};
try {
const dl = mkdtempSync(join(tmpdir(), "hf-dl-dedup-"));
@@ -1479,7 +1524,7 @@ describe("localizeRemoteMediaSources", () => {
it("rewrites src in both double-quoted and single-quoted attributes", async () => {
const orig = globalThis.fetch;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(globalThis as any).fetch = async () => new Response(new Uint8Array(100), { status: 200 });
(globalThis as any).fetch = async () => validTestMediaResponse();
try {
const dl = mkdtempSync(join(tmpdir(), "hf-dl-quotes-"));
const html = `<video id="v1" src="https://q.example.com/c/dq.mp4" data-start="0" data-end="10" muted></video>
@@ -1520,7 +1565,7 @@ describe("localizeRemoteImageSources", () => {
it("rewrites remote <img> src to _remote_media path when download succeeds", async () => {
const orig = globalThis.fetch;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(globalThis as any).fetch = async () => new Response(new Uint8Array(100), { status: 200 });
(globalThis as any).fetch = async () => validTestImageResponse();
try {
const dl = mkdtempSync(join(tmpdir(), "hf-img-ok-"));
const html = `<img class="hero" src="https://img-ok.example.com/photo.png" />`;
@@ -1549,7 +1594,7 @@ describe("localizeRemoteImageSources", () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(globalThis as any).fetch = async () => {
fetchCount++;
return new Response(new Uint8Array(100), { status: 200 });
return validTestImageResponse();
};
try {
const dl = mkdtempSync(join(tmpdir(), "hf-img-dedup-"));
@@ -1583,7 +1628,7 @@ describe("localizeRemoteImageSources", () => {
it("rewrites both double-quoted and single-quoted src attributes", async () => {
const orig = globalThis.fetch;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(globalThis as any).fetch = async () => new Response(new Uint8Array(100), { status: 200 });
(globalThis as any).fetch = async () => validTestImageResponse();
try {
const dl = mkdtempSync(join(tmpdir(), "hf-img-quotes-"));
const html = `<img src="https://q-img.example.com/dq.png" />
@@ -1607,7 +1652,7 @@ describe("localizeRemoteImageSources", () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(globalThis as any).fetch = async () => {
fetchCount++;
return new Response(new Uint8Array(100), { status: 200 });
return validTestImageResponse();
};
try {
const dl = mkdtempSync(join(tmpdir(), "hf-img-datasrc-"));
@@ -1628,7 +1673,7 @@ describe("localizeRemoteImageSources", () => {
// <img> tags with `class` before `src`. Regex must not assume src position.
const orig = globalThis.fetch;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(globalThis as any).fetch = async () => new Response(new Uint8Array(100), { status: 200 });
(globalThis as any).fetch = async () => validTestImageResponse();
try {
const dl = mkdtempSync(join(tmpdir(), "hf-img-attr-order-"));
const html = `<img class="kobe-cutout" alt="kobe" src="https://astral.example.com/d828bca.png" />`;
@@ -1798,6 +1843,7 @@ h1 { font-size: 2rem; }`;
const { html: result, remoteMediaAssets } = await localizeRemoteFontFaces(html, dl);
// The <link> tag should be replaced with an inline <style> containing the @font-face
expect(result).not.toContain(`href="${STYLESHEET_URL}"`);
expect(result).not.toContain(STYLESHEET_URL);
expect(result).not.toContain("<link");
expect(result).toContain("@font-face");
expect(result).toContain("CustomFont");
@@ -1828,6 +1874,57 @@ h1 { font-size: 2rem; }`;
}
});
it("rejects a stylesheet redirect to a private host before the second request", async () => {
const STYLESHEET_URL = "https://styles.example.com/fonts.css";
const orig = globalThis.fetch;
let fetchCount = 0;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(globalThis as any).fetch = async () => {
fetchCount++;
return new Response(null, {
status: 302,
headers: { location: "https://169.254.169.254/latest/meta-data/" },
});
};
try {
const dl = mkdtempSync(join(tmpdir(), "hf-ff-private-redirect-"));
const html = `<link rel="stylesheet" href="${STYLESHEET_URL}">`;
const { html: result, remoteMediaAssets } = await localizeRemoteFontFaces(html, dl);
expect(fetchCount).toBe(1);
expect(result).toBe(html);
expect(remoteMediaAssets.size).toBe(0);
} finally {
globalThis.fetch = orig;
}
});
it("does not log a signed stylesheet path or query on failure", async () => {
const STYLESHEET_URL =
"https://styles.example.com/private/customer.css?X-Amz-Signature=super-secret";
const originalFetch = globalThis.fetch;
const originalWarn = defaultLogger.warn;
const warnings: unknown[] = [];
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(globalThis as any).fetch = async () =>
new Response(null, { status: 503, statusText: STYLESHEET_URL });
defaultLogger.warn = (message, meta) => warnings.push({ message, meta });
try {
const dl = mkdtempSync(join(tmpdir(), "hf-ff-safe-style-log-"));
const html = `<link rel="stylesheet" href="${STYLESHEET_URL}">`;
await localizeRemoteFontFaces(html, dl);
const serialized = JSON.stringify(warnings);
expect(serialized).toContain("styles.example.com");
expect(serialized).toContain("urlFingerprint");
expect(serialized).not.toContain("customer.css");
expect(serialized).not.toContain("super-secret");
} finally {
globalThis.fetch = originalFetch;
defaultLogger.warn = originalWarn;
}
});
it("keeps <link> tag when external stylesheet has no @font-face rules", async () => {
const STYLESHEET_URL = "https://cdn.example.com/reset.css";
const orig = globalThis.fetch;
+43 -49
View File
@@ -53,7 +53,13 @@ import {
analyzeKeyframeIntervals,
probeMediaProfile,
} from "@hyperframes/engine";
import { assertPublicHttpsUrl, downloadToTemp, isHttpUrl } from "../utils/urlDownloader.js";
import {
downloadToTemp,
fetchPublicHttpsText,
isHttpUrl,
safeDownloadUrlIdentity,
type UrlDownloadTelemetry,
} from "../utils/urlDownloader.js";
import type { Page } from "puppeteer-core";
import {
injectDeterministicFontFaces,
@@ -66,6 +72,10 @@ import { defaultLogger, type ProducerLogger } from "../logger.js";
import { assertAssetMediaTypeProfile } from "./assetMediaType.js";
import { withMediaProbeSlot } from "../utils/mediaProbeConcurrency.js";
function logRemoteDownloadTelemetry(event: UrlDownloadTelemetry): void {
defaultLogger.info("[Compiler] Remote asset download integrity", { ...event });
}
export interface CompiledComposition {
html: string;
subCompositions: Map<string, string>;
@@ -419,7 +429,9 @@ async function resolveMediaDuration(
if (isHttpUrl(src)) {
if (!existsSync(downloadDir)) mkdirSync(downloadDir, { recursive: true });
try {
filePath = await downloadToTemp(src, downloadDir);
filePath = await downloadToTemp(src, downloadDir, undefined, undefined, undefined, {
onTelemetry: logRemoteDownloadTelemetry,
});
} catch {
// Download failed (e.g. 404 placeholder URL) — skip gracefully.
// The element will get duration 0 and be excluded from the render.
@@ -1306,14 +1318,17 @@ async function downloadAndRewriteUrls(
await Promise.all(
[...urlSet].map(async (url) => {
try {
const localPath = await downloadToTemp(url, remoteDir);
const localPath = await downloadToTemp(url, remoteDir, undefined, undefined, undefined, {
onTelemetry: logRemoteDownloadTelemetry,
});
urlToLocal.set(url, localPath);
} catch (err) {
defaultLogger.warn(
`[Compiler] ${warnLabel} ${url} — using original URL as fallback. ${
err instanceof Error ? err.message : String(err)
}`,
);
const identity = safeDownloadUrlIdentity(url);
defaultLogger.warn(`[Compiler] ${warnLabel} — using original URL as fallback.`, {
urlFingerprint: identity.urlFingerprint,
host: identity.host,
error: err instanceof Error ? err.message : String(err),
});
}
}),
);
@@ -1369,7 +1384,7 @@ export async function localizeRemoteMediaSources(
urlSet,
html,
join(downloadDir, REMOTE_MEDIA_SUBDIR),
"Remote media download failed for",
"Remote media download failed",
"Localized remote media source(s)",
);
}
@@ -1412,7 +1427,7 @@ export async function localizeRemoteImageSources(
urlSet,
html,
join(downloadDir, REMOTE_MEDIA_SUBDIR),
"Remote image download failed for",
"Remote image download failed",
"Localized remote image source(s)",
);
}
@@ -1450,7 +1465,7 @@ export async function localizeRemoteBackgroundImages(
urlSet,
html,
join(downloadDir, REMOTE_MEDIA_SUBDIR),
"Remote background-image download failed for",
"Remote background-image download failed",
"Localized remote background-image(s)",
// Quoted url('..')/url("..") are rewritten by downloadAndRewriteUrls' default
// replaceAll; this handles the unquoted url(https://..) form.
@@ -1492,42 +1507,18 @@ function isGoogleFontsUrl(href: string): boolean {
const MAX_STYLESHEET_BYTES = 2 * 1024 * 1024;
async function fetchExternalStylesheetCss(href: string): Promise<string | null> {
const identity = safeDownloadUrlIdentity(href);
try {
assertPublicHttpsUrl(href);
} catch {
return null;
}
try {
const response = await fetch(href, {
signal: AbortSignal.timeout(15_000),
return await fetchPublicHttpsText(href, {
maxBytes: MAX_STYLESHEET_BYTES,
timeoutMs: 15_000,
});
if (!response.ok) {
defaultLogger.warn(
`[Compiler] External stylesheet fetch failed for ${href} — HTTP ${response.status}`,
);
return null;
}
const contentLength = response.headers.get("content-length");
if (contentLength && parseInt(contentLength, 10) > MAX_STYLESHEET_BYTES) {
defaultLogger.warn(
`[Compiler] External stylesheet too large (${contentLength} bytes): ${href}`,
);
return null;
}
const text = await response.text();
if (text.length > MAX_STYLESHEET_BYTES) {
defaultLogger.warn(
`[Compiler] External stylesheet too large (${text.length} bytes): ${href}`,
);
return null;
}
return text;
} catch (err) {
defaultLogger.warn(
`[Compiler] External stylesheet fetch failed for ${href}${
err instanceof Error ? err.message : String(err)
}`,
);
defaultLogger.warn("[Compiler] External stylesheet fetch failed — preserving link tag.", {
urlFingerprint: identity.urlFingerprint,
host: identity.host,
error: err instanceof Error ? err.message : String(err),
});
return null;
}
}
@@ -1608,11 +1599,14 @@ async function inlineExternalFontStylesheets(html: string): Promise<string> {
if (css === null) continue;
const fontFaceBlocks = extractFontFaceBlocks(css);
if (fontFaceBlocks.length === 0) continue;
const inlineStyle = `<style>/* Inlined from ${href} */\n${fontFaceBlocks.join("\n")}\n</style>`;
const identity = safeDownloadUrlIdentity(href);
const inlineStyle = `<style>/* Inlined external font stylesheet */\n${fontFaceBlocks.join("\n")}\n</style>`;
result = result.replace(fullMatch, inlineStyle);
defaultLogger.info(
`[Compiler] Inlined ${fontFaceBlocks.length} @font-face rule(s) from external stylesheet: ${href}`,
);
defaultLogger.info("[Compiler] Inlined external @font-face rule(s)", {
count: fontFaceBlocks.length,
urlFingerprint: identity.urlFingerprint,
host: identity.host,
});
}
return result;
}
@@ -1672,7 +1666,7 @@ export async function localizeRemoteFontFaces(
urlSet,
processed,
join(downloadDir, REMOTE_MEDIA_SUBDIR),
"Remote font download failed for",
"Remote font download failed",
"Localized remote font face(s)",
(h, url, relPath) => h.replaceAll(`url(${url})`, `url("${relPath}")`),
);
+8 -1
View File
@@ -2,4 +2,11 @@
* Re-exported from @hyperframes/engine.
* @see engine/src/utils/urlDownloader.ts for implementation.
*/
export { assertPublicHttpsUrl, downloadToTemp, isHttpUrl } from "@hyperframes/engine";
export {
downloadToTemp,
fetchPublicHttpsText,
isHttpUrl,
safeDownloadUrlIdentity,
writeUrlDownloadTelemetry,
type UrlDownloadTelemetry,
} from "@hyperframes/engine";