fix security issues

This commit is contained in:
Gerhard Scheikl
2026-05-09 22:19:25 +02:00
parent c45648832a
commit 3a77bed716
6 changed files with 391 additions and 36 deletions
@@ -6,6 +6,8 @@
* served from Shopify's CDN so re-fetching is cheap, but caching avoids
* hammering the network when regenerating an invoice multiple times.
*/
import { safeFetch, SafeFetchError, SHOPIFY_CDN_HOSTS } from "./safeFetch.server";
const MAX_BYTES = 2 * 1024 * 1024; // 2 MB cap per image
const CACHE_MAX_ENTRIES = 200;
@@ -40,25 +42,34 @@ export async function fetchProductImageDataUrl(url: string): Promise<string | un
? `${url}${url.includes("?") ? "&" : "?"}width=128`
: url;
let res: Response;
let res: Awaited<ReturnType<typeof safeFetch>>;
try {
res = await fetch(requestUrl);
res = await safeFetch(requestUrl, {
maxBytes: MAX_BYTES,
accept: "image/*",
// Lock product images to Shopify's CDN — line item image URLs come
// from the Admin API and should never point anywhere else.
allowedHosts: SHOPIFY_CDN_HOSTS,
});
} catch (err) {
console.warn(`Product image fetch failed for ${url}:`, err);
if (err instanceof SafeFetchError) {
console.warn(`Product image refused (${err.code}) for ${url}: ${err.message}`);
} else {
console.warn(`Product image fetch failed for ${url}:`, err);
}
return undefined;
}
if (!res.ok) {
if (res.status < 200 || res.status >= 300) {
console.warn(`Product image HTTP ${res.status} for ${url}`);
return undefined;
}
const buf = await res.arrayBuffer();
if (buf.byteLength === 0 || buf.byteLength > MAX_BYTES) return undefined;
if (res.bytesRead === 0) return undefined;
const contentType = guessContentType(url, res.headers.get("content-type"));
const contentType = guessContentType(url, res.contentType);
// @react-pdf supports png/jpeg natively; webp/gif are unreliable. Skip those.
if (contentType !== "image/png" && contentType !== "image/jpeg") return undefined;
const b64 = Buffer.from(buf).toString("base64");
const b64 = Buffer.from(res.bytes).toString("base64");
const dataUrl = `data:${contentType};base64,${b64}`;
rememberInCache(url, dataUrl);
return dataUrl;