Skip to content

Commit d5be702

Browse files
committed
fix(gateway): guard assistant media ticket clocks
1 parent 3d66d20 commit d5be702

2 files changed

Lines changed: 45 additions & 3 deletions

File tree

src/gateway/control-ui.http.test.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -463,6 +463,36 @@ describe("handleControlUiHttpRequest", () => {
463463
});
464464
});
465465

466+
it("reports assistant media metadata when the process clock is outside the Date range", async () => {
467+
const dateNowSpy = vi.spyOn(Date, "now").mockReturnValue(8_640_000_000_000_001);
468+
try {
469+
await withAllowedAssistantMediaRoot({
470+
prefix: "ui-media-bad-clock-",
471+
fn: async (tmpRoot) => {
472+
const filePath = path.join(tmpRoot, "photo.png");
473+
await fs.writeFile(filePath, Buffer.from("not-a-real-png"));
474+
const { res, handled, end } = await runAssistantMediaRequest({
475+
url: `/__openclaw__/assistant-media?meta=1&source=${encodeURIComponent(filePath)}&token=test-token`,
476+
method: "GET",
477+
auth: { mode: "token", token: "test-token", allowTailscale: false },
478+
});
479+
expect(handled).toBe(true);
480+
expect(res.statusCode).toBe(200);
481+
const payload = responseJson(end) as {
482+
available?: boolean;
483+
mediaTicket?: string;
484+
mediaTicketExpiresAt?: string;
485+
};
486+
expect(payload.available).toBe(true);
487+
expect(payload.mediaTicket).toBeUndefined();
488+
expect(payload.mediaTicketExpiresAt).toBeUndefined();
489+
},
490+
});
491+
} finally {
492+
dateNowSpy.mockRestore();
493+
}
494+
});
495+
466496
it("serves assistant local media with a scoped media ticket after metadata auth", async () => {
467497
await withAllowedAssistantMediaRoot({
468498
prefix: "ui-media-ticket-",

src/gateway/control-ui.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { getAgentScopedMediaLocalRoots } from "../media/local-roots.js";
1919
import { resolveMediaReferenceLocalPath } from "../media/media-reference.js";
2020
import { detectMime } from "../media/mime.js";
2121
import { AVATAR_MAX_BYTES } from "../shared/avatar-policy.js";
22+
import { asDateTimestampMs, resolveTimestampMsToIsoString } from "../shared/number-coercion.js";
2223
import { resolveUserPath } from "../utils.js";
2324
import { resolveRuntimeServiceVersion } from "../version.js";
2425
import { DEFAULT_ASSISTANT_IDENTITY, resolveAssistantIdentity } from "./assistant-identity.js";
@@ -408,7 +409,14 @@ function signAssistantMediaTicketPayload(encodedPayload: string): string {
408409
}
409410

410411
function createAssistantMediaTicket(source: string, nowMs = Date.now()) {
411-
const exp = nowMs + CONTROL_UI_ASSISTANT_MEDIA_TICKET_TTL_MS;
412+
const now = asDateTimestampMs(nowMs);
413+
if (now === undefined) {
414+
return {};
415+
}
416+
const exp = asDateTimestampMs(now + CONTROL_UI_ASSISTANT_MEDIA_TICKET_TTL_MS);
417+
if (exp === undefined) {
418+
return {};
419+
}
412420
const payload: AssistantMediaTicketPayload = {
413421
scope: CONTROL_UI_ASSISTANT_MEDIA_TICKET_SCOPE,
414422
source,
@@ -418,11 +426,15 @@ function createAssistantMediaTicket(source: string, nowMs = Date.now()) {
418426
const sig = signAssistantMediaTicketPayload(encodedPayload);
419427
return {
420428
mediaTicket: `v1.${encodedPayload}.${sig}`,
421-
mediaTicketExpiresAt: new Date(exp).toISOString(),
429+
mediaTicketExpiresAt: resolveTimestampMsToIsoString(exp),
422430
};
423431
}
424432

425433
function verifyAssistantMediaTicket(ticket: string | null, source: string, nowMs = Date.now()) {
434+
const now = asDateTimestampMs(nowMs);
435+
if (now === undefined) {
436+
return false;
437+
}
426438
const parts = ticket?.split(".");
427439
if (!parts || parts.length !== 3 || parts[0] !== "v1") {
428440
return false;
@@ -446,7 +458,7 @@ function verifyAssistantMediaTicket(ticket: string | null, source: string, nowMs
446458
payload.source === source &&
447459
typeof payload.exp === "number" &&
448460
Number.isFinite(payload.exp) &&
449-
payload.exp >= nowMs
461+
payload.exp >= now
450462
);
451463
} catch {
452464
return false;

0 commit comments

Comments
 (0)