From e10d403e5e64d30774a45aea71e02f5808eca9a8 Mon Sep 17 00:00:00 2001 From: Dotta Date: Thu, 9 Jul 2026 19:39:17 +0000 Subject: [PATCH] Protect Clip catalog integrity Co-Authored-By: Paperclip --- server/src/__tests__/clips-routes.test.ts | 27 +++++++++++++++++------ server/src/routes/clips.ts | 9 +++++++- 2 files changed, 28 insertions(+), 8 deletions(-) diff --git a/server/src/__tests__/clips-routes.test.ts b/server/src/__tests__/clips-routes.test.ts index 3cc26e4d1bb..b25aaf8c0a9 100644 --- a/server/src/__tests__/clips-routes.test.ts +++ b/server/src/__tests__/clips-routes.test.ts @@ -147,15 +147,15 @@ function publicClipFixture(input: { clipId: string; slug?: string }) { }; } -function createApp() { +function createApp(actor: Record = { + type: "board", + source: "local_implicit", + userId: "board", +}) { const app = express(); app.use(express.json()); app.use((req, _res, next) => { - (req as any).actor = { - type: "board", - source: "local_implicit", - userId: "board", - }; + (req as any).actor = actor; next(); }); app.use("/api", clipRoutes({} as any)); @@ -212,7 +212,7 @@ describe("clip routes", () => { ); }); - it("rejects owner moderation and publish-status patches", async () => { + it("rejects owner moderation, publish-status, and public-visibility patches", async () => { const clipId = "33333333-3333-4333-8333-333333333333"; const sourceCompanyId = "22222222-2222-4222-8222-222222222222"; mockClipService.getClipById.mockResolvedValue({ @@ -228,12 +228,25 @@ describe("clip routes", () => { const publishRes = await request(createApp()) .patch("/api/clips/" + clipId) .send({ status: "published" }); + const visibilityRes = await request(createApp()) + .patch("/api/clips/" + clipId) + .send({ visibility: "public" }); expect(moderationRes.status).toBe(403); expect(publishRes.status).toBe(403); + expect(visibilityRes.status).toBe(403); expect(mockClipService.updateClip).not.toHaveBeenCalled(); }); + it("rejects anonymous applied import telemetry", async () => { + const res = await request(createApp({ type: "none", source: "none" })) + .post("/api/public/clips/support-triage/import-telemetry") + .send({ status: "applied" }); + + expect(res.status).toBe(403); + expect(mockClipService.recordImportTelemetry).not.toHaveBeenCalled(); + }); + it("uses the clip source company when building import previews", async () => { const destinationCompanyId = "11111111-1111-4111-8111-111111111111"; const sourceCompanyId = "22222222-2222-4222-8222-222222222222"; diff --git a/server/src/routes/clips.ts b/server/src/routes/clips.ts index 7f1ba2a3df4..3c85b6f58e9 100644 --- a/server/src/routes/clips.ts +++ b/server/src/routes/clips.ts @@ -643,6 +643,9 @@ export function clipRoutes(db: Db, storage?: StorageService) { if (req.body.status === "published" && clip.status !== "published") { throw forbidden("Publishing clips requires platform moderation access"); } + if (req.body.visibility === "public" && clip.visibility !== "public") { + throw forbidden("Public clip visibility requires platform moderation access"); + } const actorInfo = getActorInfo(req); const updated = await svc.updateClip(clip.id, req.body, { actorType: actorInfo.actorType, @@ -751,7 +754,11 @@ export function clipRoutes(db: Db, storage?: StorageService) { } if (!consumeClipRateLimit(req, res, "import")) return; const actor = actorForClip(req); - const clip = await svc.recordImportTelemetry(req.params.slug as string, req.body, actor); + const telemetryStatus = req.body.status ?? "previewed"; + if (actor.actorType === "anonymous" && telemetryStatus !== "previewed") { + throw forbidden("Authenticated actor required for applied import telemetry"); + } + const clip = await svc.recordImportTelemetry(req.params.slug as string, { ...req.body, status: telemetryStatus }, actor); await logActivity(db, { companyId: req.body.destinationCompanyId ?? clip.sourceCompanyId, actorType: actor.actorType === "agent" ? "agent" : actor.actorType === "user" ? "user" : "system",