fix v2 hardening review findings

This commit is contained in:
ajnart committed 2026-07-28 15:28:58 +02:00
1 parent b82a6a7abc
commit a7eae3ff81
35 files changed
+1147 -277

No files matched your search

@@ -42,6 +42,14 @@ export function getCustomWidgetHttpUrlIssue(value: string): CustomWidgetHttpUrlI
const rawHostname = withoutTrailingRootDot(hostAndPort.hostname);
const parsedHostname = withoutTrailingRootDot(url.hostname);
if (!rawHostname || !hasCanonicalParsedHostname(parsedHostname)) return "invalid";
if (
rawHostname.startsWith("[") &&
parsedHostname.startsWith("[") &&
!rawHostname.includes(".") &&
rawHostname.toLowerCase() !== parsedHostname
) {
return "invalid";
}
if (looksNumeric(rawHostname) && !isStrictIpv4(rawHostname)) return "invalid";
if (isStrictIpv4(parsedHostname) && rawHostname !== parsedHostname) return "invalid";
@@ -1,6 +1,6 @@
import type { createPinnedAgent } from "./network-policy";
const DISPATCHER_CLOSE_GRACE_MS = 1_000;
export const DISPATCHER_CLOSE_GRACE_MS = 1_000;
type CustomWidgetDispatcher = ReturnType<typeof createPinnedAgent>;
@@ -31,7 +31,10 @@ function detachDispatcherDestroy(dispatcher: CustomWidgetDispatcher): void {
}
function abortable<T>(operation: Promise<T>, signal: AbortSignal): Promise<T> {
if (signal.aborted) return Promise.reject(signal.reason ?? createAbortError());
if (signal.aborted) {
void operation.catch(() => undefined);
return Promise.reject(signal.reason ?? createAbortError());
}
return new Promise<T>((resolve, reject) => {
const onAbort = () => {
@@ -15,6 +15,7 @@ vi.mock("../server/network-policy", async (importOriginal) => {
});
import { executeCustomWidgetRequest, MAX_REQUEST_DURATION_MS } from "../server/request-executor";
import { closeDispatcher, DISPATCHER_CLOSE_GRACE_MS } from "../server/request-dispatcher-lifecycle";
import { resolveAndValidateHost } from "../server/network-policy";
const query = {
@@ -83,7 +84,7 @@ describe("custom widget request deadline", () => {
logError,
});
await vi.advanceTimersByTimeAsync(1_000);
await vi.advanceTimersByTimeAsync(DISPATCHER_CLOSE_GRACE_MS);
await expect(request).resolves.toMatchObject({ ok: true, status: 200, data: {} });
expect(close).toHaveBeenCalledOnce();
@@ -133,6 +134,28 @@ describe("custom widget request deadline", () => {
);
});
it("consumes a late close rejection when cleanup starts after the deadline", async () => {
const abortReason = new Error("request deadline elapsed");
const closeFailure = new Error("late close failure");
const controller = new AbortController();
controller.abort(abortReason);
let rejectClose: ((reason: Error) => void) | undefined;
const close = vi.fn(
() =>
new Promise<void>((_resolve, reject) => {
rejectClose = reject;
}),
);
const destroy = vi.fn(async () => undefined);
await expect(closeDispatcher({ close, destroy } as never, controller.signal)).rejects.toBe(abortReason);
rejectClose?.(closeFailure);
await Promise.resolve();
expect(close).toHaveBeenCalledOnce();
expect(destroy).toHaveBeenCalledOnce();
});
it("redacts apiKeyQuery credentials from transport errors and structured log events", async () => {
const apiKey = "api-key-query-secret";
const request = vi.fn(async ({ path }: { path: string }) => {
@@ -128,6 +128,8 @@ describe("lean Custom Widget schema", () => {
"https://example.com:65536",
"https://[fe80::1%25eth0]",
"https://[::ffff:192.168.001.001]",
"https://[0:0:0:0:0:0:0:1]",
"https://[2001:0db8::1]",
])("rejects ambiguous persisted URL spelling %s for sources and icons", (url) => {
expect(
customWidgetDefinitionSchema.safeParse({