Openstatus www.openstatus.dev

fix: secure custom domain (#1101)

authored by

Maximilian Kaske and committed by
GitHub
399d7911 d3407d8d

+44 -8
+12
apps/server/src/v1/pages/post.ts
··· 45 45 const limits = c.get("limits"); 46 46 const input = c.req.valid("json"); 47 47 48 + if (input.customDomain && !limits["custom-domain"]) { 49 + throw new HTTPException(403, { 50 + message: "Upgrade for custom domains", 51 + }); 52 + } 53 + 54 + if (input.customDomain?.toLowerCase().includes("openstatus")) { 55 + throw new HTTPException(400, { 56 + message: "Domain cannot contain 'openstatus'", 57 + }); 58 + } 59 + 48 60 const count = ( 49 61 await db 50 62 .select({ count: sql<number>`count(*)` })
+12
apps/server/src/v1/pages/put.ts
··· 46 46 const { id } = c.req.valid("param"); 47 47 const input = c.req.valid("json"); 48 48 49 + if (input.customDomain && !limits["custom-domain"]) { 50 + throw new HTTPException(403, { 51 + message: "Upgrade for custom domains", 52 + }); 53 + } 54 + 55 + if (input.customDomain?.toLowerCase().includes("openstatus")) { 56 + throw new HTTPException(400, { 57 + message: "Domain cannot contain 'openstatus'", 58 + }); 59 + } 60 + 49 61 if ( 50 62 limits["password-protection"] === false && 51 63 input?.passwordProtected === true
+12 -8
apps/web/src/components/forms/custom-domain-form.tsx
··· 6 6 import { useForm } from "react-hook-form"; 7 7 import type * as z from "zod"; 8 8 9 - import { insertPageSchema } from "@openstatus/db/src/schema"; 9 + import { selectPageSchema } from "@openstatus/db/src/schema"; 10 10 import { 11 11 Button, 12 12 Form, ··· 20 20 } from "@openstatus/ui"; 21 21 22 22 import { useDomainStatus } from "@/hooks/use-domain-status"; 23 - import { toastAction } from "@/lib/toast"; 23 + import { toast, toastAction } from "@/lib/toast"; 24 24 import { api } from "@/trpc/client"; 25 25 import DomainConfiguration from "../domains/domain-configuration"; 26 26 import DomainStatusIcon from "../domains/domain-status-icon"; 27 27 import { LoadingAnimation } from "../loading-animation"; 28 28 29 - const customDomain = insertPageSchema.pick({ 29 + const customDomain = selectPageSchema.pick({ 30 30 customDomain: true, 31 31 id: true, 32 32 }); ··· 48 48 async function onSubmit(data: Schema) { 49 49 startTransition(async () => { 50 50 try { 51 - if (defaultValues.id) { 52 - await api.page.addCustomDomain.mutate({ 53 - customDomain: data.customDomain, 54 - pageId: defaultValues?.id, 55 - }); 51 + if (data.customDomain.toLowerCase().includes("openstatus")) { 52 + toast.error("Domain cannot contain 'openstatus'"); 53 + return; 56 54 } 55 + 56 + await api.page.addCustomDomain.mutate({ 57 + customDomain: data.customDomain, 58 + pageId: defaultValues?.id, 59 + }); 60 + 57 61 if (data.customDomain && !defaultValues.customDomain) { 58 62 await api.domain.addDomainToVercel.mutate({ 59 63 domain: data.customDomain,
+8
packages/api/src/router/domain.ts
··· 1 1 import { z } from "zod"; 2 2 3 + import { TRPCError } from "@trpc/server"; 3 4 import { env } from "../env"; 4 5 import { createTRPCRouter, protectedProcedure } from "../trpc"; 5 6 ··· 54 55 addDomainToVercel: protectedProcedure 55 56 .input(z.object({ domain: z.string() })) 56 57 .mutation(async (opts) => { 58 + if (opts.input.domain.toLowerCase().includes("openstatus")) { 59 + throw new TRPCError({ 60 + code: "BAD_REQUEST", 61 + message: "Domain cannot contain 'openstatus'", 62 + }); 63 + } 64 + 57 65 const data = await fetch( 58 66 `https://api.vercel.com/v9/projects/${env.PROJECT_ID_VERCEL}/domains?teamId=${env.TEAM_ID_VERCEL}`, 59 67 {