mirror of
https://github.com/baptisteArno/typebot.io.git
synced 2026-06-05 21:04:43 +08:00
🐛 Make allowed origins stricter and prevent the bot being consumable from public URL as mentioned in the docs
Closes #1812
This commit is contained in:
parent
c9b57f58bf
commit
7f44ca4410
@ -1790,7 +1790,98 @@
|
||||
"$ref": "#/components/schemas/theme"
|
||||
},
|
||||
"settings": {
|
||||
"$ref": "#/components/schemas/settings"
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"general": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"isBrandingEnabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"isTypingEmulationEnabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"isInputPrefillEnabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"isHideQueryParamsEnabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"isNewResultOnRefreshEnabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"rememberUser": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"isEnabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"storage": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"session",
|
||||
"local"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"systemMessages": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"invalidMessage": {
|
||||
"type": "string"
|
||||
},
|
||||
"botClosed": {
|
||||
"type": "string"
|
||||
},
|
||||
"networkErrorTitle": {
|
||||
"type": "string"
|
||||
},
|
||||
"networkErrorMessage": {
|
||||
"type": "string"
|
||||
},
|
||||
"popupBlockedDescription": {
|
||||
"type": "string"
|
||||
},
|
||||
"popupBlockedButtonLabel": {
|
||||
"type": "string"
|
||||
},
|
||||
"fileUploadError": {
|
||||
"type": "string"
|
||||
},
|
||||
"fileUploadSizeError": {
|
||||
"type": "string"
|
||||
},
|
||||
"whatsAppPictureChoiceSelectLabel": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"typingEmulation": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"speed": {
|
||||
"type": "number"
|
||||
},
|
||||
"maxDelay": {
|
||||
"type": "number"
|
||||
},
|
||||
"delayBetweenBubbles": {
|
||||
"type": "number",
|
||||
"minimum": 0,
|
||||
"maximum": 5
|
||||
},
|
||||
"isDisabledOnFirstMessage": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"publishedAt": {
|
||||
"type": "string"
|
||||
@ -2958,7 +3049,98 @@
|
||||
"$ref": "#/components/schemas/theme"
|
||||
},
|
||||
"settings": {
|
||||
"$ref": "#/components/schemas/settings"
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"general": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"isBrandingEnabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"isTypingEmulationEnabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"isInputPrefillEnabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"isHideQueryParamsEnabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"isNewResultOnRefreshEnabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"rememberUser": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"isEnabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"storage": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"session",
|
||||
"local"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"systemMessages": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"invalidMessage": {
|
||||
"type": "string"
|
||||
},
|
||||
"botClosed": {
|
||||
"type": "string"
|
||||
},
|
||||
"networkErrorTitle": {
|
||||
"type": "string"
|
||||
},
|
||||
"networkErrorMessage": {
|
||||
"type": "string"
|
||||
},
|
||||
"popupBlockedDescription": {
|
||||
"type": "string"
|
||||
},
|
||||
"popupBlockedButtonLabel": {
|
||||
"type": "string"
|
||||
},
|
||||
"fileUploadError": {
|
||||
"type": "string"
|
||||
},
|
||||
"fileUploadSizeError": {
|
||||
"type": "string"
|
||||
},
|
||||
"whatsAppPictureChoiceSelectLabel": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"typingEmulation": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"speed": {
|
||||
"type": "number"
|
||||
},
|
||||
"maxDelay": {
|
||||
"type": "number"
|
||||
},
|
||||
"delayBetweenBubbles": {
|
||||
"type": "number",
|
||||
"minimum": 0,
|
||||
"maximum": 5
|
||||
},
|
||||
"isDisabledOnFirstMessage": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"publishedAt": {
|
||||
"type": "string"
|
||||
|
||||
@ -31,15 +31,13 @@ export const continueChat = publicProcedure
|
||||
.mutation(
|
||||
async ({
|
||||
input: { sessionId, message, textBubbleContentFormat },
|
||||
ctx: { origin, res },
|
||||
}) => {
|
||||
const { corsOrigin, ...response } = await continueChatFn({
|
||||
ctx: { origin, iframeReferrerOrigin },
|
||||
}) =>
|
||||
continueChatFn({
|
||||
origin,
|
||||
iframeReferrerOrigin,
|
||||
sessionId,
|
||||
message,
|
||||
textBubbleContentFormat,
|
||||
});
|
||||
if (corsOrigin) res.setHeader("Access-Control-Allow-Origin", corsOrigin);
|
||||
return response;
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
@ -2,6 +2,7 @@ import { publicProcedure } from "@/helpers/server/trpc";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { BubbleBlockType } from "@typebot.io/blocks-bubbles/constants";
|
||||
import { continueBotFlow } from "@typebot.io/bot-engine/continueBotFlow";
|
||||
import { assertOriginIsAllowed } from "@typebot.io/bot-engine/helpers/isOriginAllowed";
|
||||
import { parseDynamicTheme } from "@typebot.io/bot-engine/parseDynamicTheme";
|
||||
import { saveStateToDatabase } from "@typebot.io/bot-engine/saveStateToDatabase";
|
||||
import {
|
||||
@ -35,7 +36,7 @@ export const sendMessageV1 = publicProcedure
|
||||
.mutation(
|
||||
async ({
|
||||
input: { sessionId, message, startParams, clientLogs },
|
||||
ctx: { user, origin, res },
|
||||
ctx: { user, origin, iframeReferrerOrigin },
|
||||
}) => {
|
||||
const session = sessionId ? await getSession(sessionId) : null;
|
||||
const newSessionId = sessionId ?? createId();
|
||||
@ -120,18 +121,10 @@ export const sendMessageV1 = publicProcedure
|
||||
});
|
||||
|
||||
if (startParams.isPreview || typeof startParams.typebot !== "string") {
|
||||
if (
|
||||
newSessionState.allowedOrigins &&
|
||||
newSessionState.allowedOrigins.length > 0
|
||||
) {
|
||||
if (origin && newSessionState.allowedOrigins.includes(origin))
|
||||
res.setHeader("Access-Control-Allow-Origin", origin);
|
||||
else
|
||||
res.setHeader(
|
||||
"Access-Control-Allow-Origin",
|
||||
newSessionState.allowedOrigins[0],
|
||||
);
|
||||
}
|
||||
assertOriginIsAllowed(origin, {
|
||||
allowedOrigins: newSessionState.allowedOrigins,
|
||||
iframeReferrerOrigin,
|
||||
});
|
||||
}
|
||||
|
||||
const allLogs = clientLogs ? [...(logs ?? []), ...clientLogs] : logs;
|
||||
@ -180,18 +173,10 @@ export const sendMessageV1 = publicProcedure
|
||||
clientSideActions,
|
||||
};
|
||||
} else {
|
||||
if (
|
||||
session.state.allowedOrigins &&
|
||||
session.state.allowedOrigins.length > 0
|
||||
) {
|
||||
if (origin && session.state.allowedOrigins.includes(origin))
|
||||
res.setHeader("Access-Control-Allow-Origin", origin);
|
||||
else
|
||||
res.setHeader(
|
||||
"Access-Control-Allow-Origin",
|
||||
session.state.allowedOrigins[0],
|
||||
);
|
||||
}
|
||||
assertOriginIsAllowed(origin, {
|
||||
allowedOrigins: session.state.allowedOrigins,
|
||||
iframeReferrerOrigin,
|
||||
});
|
||||
|
||||
const {
|
||||
messages,
|
||||
|
||||
@ -2,6 +2,7 @@ import { publicProcedure } from "@/helpers/server/trpc";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { BubbleBlockType } from "@typebot.io/blocks-bubbles/constants";
|
||||
import { continueBotFlow } from "@typebot.io/bot-engine/continueBotFlow";
|
||||
import { assertOriginIsAllowed } from "@typebot.io/bot-engine/helpers/isOriginAllowed";
|
||||
import { parseDynamicTheme } from "@typebot.io/bot-engine/parseDynamicTheme";
|
||||
import { saveStateToDatabase } from "@typebot.io/bot-engine/saveStateToDatabase";
|
||||
import {
|
||||
@ -35,7 +36,7 @@ export const sendMessageV2 = publicProcedure
|
||||
.mutation(
|
||||
async ({
|
||||
input: { sessionId, message, startParams, clientLogs },
|
||||
ctx: { user, res, origin },
|
||||
ctx: { user, origin, iframeReferrerOrigin },
|
||||
}) => {
|
||||
const session = sessionId ? await getSession(sessionId) : null;
|
||||
const newSessionId = sessionId ?? createId();
|
||||
@ -120,18 +121,10 @@ export const sendMessageV2 = publicProcedure
|
||||
});
|
||||
|
||||
if (startParams.isPreview || typeof startParams.typebot !== "string") {
|
||||
if (
|
||||
newSessionState.allowedOrigins &&
|
||||
newSessionState.allowedOrigins.length > 0
|
||||
) {
|
||||
if (origin && newSessionState.allowedOrigins.includes(origin))
|
||||
res.setHeader("Access-Control-Allow-Origin", origin);
|
||||
else
|
||||
res.setHeader(
|
||||
"Access-Control-Allow-Origin",
|
||||
newSessionState.allowedOrigins[0],
|
||||
);
|
||||
}
|
||||
assertOriginIsAllowed(origin, {
|
||||
allowedOrigins: newSessionState.allowedOrigins,
|
||||
iframeReferrerOrigin,
|
||||
});
|
||||
}
|
||||
|
||||
const allLogs = clientLogs ? [...(logs ?? []), ...clientLogs] : logs;
|
||||
@ -179,18 +172,11 @@ export const sendMessageV2 = publicProcedure
|
||||
clientSideActions,
|
||||
};
|
||||
} else {
|
||||
if (
|
||||
session.state.allowedOrigins &&
|
||||
session.state.allowedOrigins.length > 0
|
||||
) {
|
||||
if (origin && session.state.allowedOrigins.includes(origin))
|
||||
res.setHeader("Access-Control-Allow-Origin", origin);
|
||||
else
|
||||
res.setHeader(
|
||||
"Access-Control-Allow-Origin",
|
||||
session.state.allowedOrigins[0],
|
||||
);
|
||||
}
|
||||
assertOriginIsAllowed(origin, {
|
||||
allowedOrigins: session.state.allowedOrigins,
|
||||
iframeReferrerOrigin,
|
||||
});
|
||||
|
||||
const {
|
||||
messages,
|
||||
input,
|
||||
|
||||
@ -15,11 +15,10 @@ export const startChat = publicProcedure
|
||||
})
|
||||
.input(startChatInputSchema)
|
||||
.output(startChatResponseSchema)
|
||||
.mutation(async ({ input, ctx: { origin, res } }) => {
|
||||
const { corsOrigin, ...response } = await startChatFn({
|
||||
.mutation(async ({ input, ctx: { origin, iframeReferrerOrigin } }) =>
|
||||
startChatFn({
|
||||
...input,
|
||||
origin,
|
||||
});
|
||||
if (corsOrigin) res.setHeader("Access-Control-Allow-Origin", corsOrigin);
|
||||
return response;
|
||||
});
|
||||
iframeReferrerOrigin,
|
||||
}),
|
||||
);
|
||||
|
||||
@ -11,11 +11,10 @@ export async function createContext(opts: trpcNext.CreateNextContextOptions) {
|
||||
|
||||
return {
|
||||
user,
|
||||
origin:
|
||||
(opts.req.headers["x-typebot-iframe-referrer-origin"] as
|
||||
| string
|
||||
| undefined) ?? opts.req.headers.origin,
|
||||
res: opts.res,
|
||||
origin: opts.req.headers.origin,
|
||||
iframeReferrerOrigin: opts.req.headers[
|
||||
"x-typebot-iframe-referrer-origin"
|
||||
] as string | undefined,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -8,6 +8,7 @@ import {
|
||||
} from "@typebot.io/runtime-session-store";
|
||||
import { computeCurrentProgress } from "../computeCurrentProgress";
|
||||
import { continueBotFlow } from "../continueBotFlow";
|
||||
import { assertOriginIsAllowed } from "../helpers/isOriginAllowed";
|
||||
import { filterPotentiallySensitiveLogs } from "../logs/filterPotentiallySensitiveLogs";
|
||||
import { parseDynamicTheme } from "../parseDynamicTheme";
|
||||
import { saveStateToDatabase } from "../saveStateToDatabase";
|
||||
@ -15,12 +16,14 @@ import type { Message } from "../schemas/api";
|
||||
|
||||
type Props = {
|
||||
origin: string | undefined;
|
||||
iframeReferrerOrigin: string | undefined;
|
||||
message?: Message;
|
||||
sessionId: string;
|
||||
textBubbleContentFormat: "richText" | "markdown";
|
||||
};
|
||||
export const continueChat = async ({
|
||||
origin,
|
||||
iframeReferrerOrigin,
|
||||
sessionId,
|
||||
message,
|
||||
textBubbleContentFormat,
|
||||
@ -34,6 +37,11 @@ export const continueChat = async ({
|
||||
});
|
||||
}
|
||||
|
||||
assertOriginIsAllowed(origin, {
|
||||
allowedOrigins: session.state.allowedOrigins,
|
||||
iframeReferrerOrigin,
|
||||
});
|
||||
|
||||
const isSessionExpired =
|
||||
session &&
|
||||
isDefined(session.state.expiryTimeout) &&
|
||||
@ -45,17 +53,6 @@ export const continueChat = async ({
|
||||
message: "Session expired. You need to start a new session.",
|
||||
});
|
||||
|
||||
let corsOrigin;
|
||||
|
||||
if (
|
||||
session?.state.allowedOrigins &&
|
||||
session.state.allowedOrigins.length > 0
|
||||
) {
|
||||
if (origin && session.state.allowedOrigins.includes(origin))
|
||||
corsOrigin = origin;
|
||||
else corsOrigin = session.state.allowedOrigins[0];
|
||||
}
|
||||
|
||||
const sessionStore = getSessionStore(sessionId);
|
||||
const {
|
||||
messages,
|
||||
@ -116,7 +113,6 @@ export const continueChat = async ({
|
||||
dynamicTheme,
|
||||
logs: isPreview ? logs : logs?.filter(filterPotentiallySensitiveLogs),
|
||||
lastMessageNewFormat,
|
||||
corsOrigin,
|
||||
progress: newSessionState.progressMetadata
|
||||
? isEnded
|
||||
? 100
|
||||
|
||||
@ -6,6 +6,7 @@ import {
|
||||
getSessionStore,
|
||||
} from "@typebot.io/runtime-session-store";
|
||||
import { computeCurrentProgress } from "../computeCurrentProgress";
|
||||
import { assertOriginIsAllowed } from "../helpers/isOriginAllowed";
|
||||
import { filterPotentiallySensitiveLogs } from "../logs/filterPotentiallySensitiveLogs";
|
||||
import { saveStateToDatabase } from "../saveStateToDatabase";
|
||||
import type { Message } from "../schemas/api";
|
||||
@ -13,6 +14,7 @@ import { startSession } from "../startSession";
|
||||
|
||||
type Props = {
|
||||
origin: string | undefined;
|
||||
iframeReferrerOrigin: string | undefined;
|
||||
message?: Message;
|
||||
isOnlyRegistering: boolean;
|
||||
publicId: string;
|
||||
@ -24,6 +26,7 @@ type Props = {
|
||||
|
||||
export const startChat = async ({
|
||||
origin,
|
||||
iframeReferrerOrigin,
|
||||
message,
|
||||
isOnlyRegistering,
|
||||
publicId,
|
||||
@ -61,16 +64,10 @@ export const startChat = async ({
|
||||
});
|
||||
deleteSessionStore(sessionId);
|
||||
|
||||
let corsOrigin;
|
||||
|
||||
if (
|
||||
newSessionState.allowedOrigins &&
|
||||
newSessionState.allowedOrigins.length > 0
|
||||
) {
|
||||
if (origin && newSessionState.allowedOrigins.includes(origin))
|
||||
corsOrigin = origin;
|
||||
else corsOrigin = newSessionState.allowedOrigins[0];
|
||||
}
|
||||
assertOriginIsAllowed(origin, {
|
||||
allowedOrigins: newSessionState.allowedOrigins,
|
||||
iframeReferrerOrigin,
|
||||
});
|
||||
|
||||
const session = isOnlyRegistering
|
||||
? await restartSession({
|
||||
@ -118,7 +115,6 @@ export const startChat = async ({
|
||||
dynamicTheme,
|
||||
logs: logs?.filter(filterPotentiallySensitiveLogs),
|
||||
clientSideActions,
|
||||
corsOrigin,
|
||||
progress: newSessionState.progressMetadata
|
||||
? isEnded
|
||||
? 100
|
||||
|
||||
29
packages/bot-engine/src/helpers/isOriginAllowed.ts
Normal file
29
packages/bot-engine/src/helpers/isOriginAllowed.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { env } from "@typebot.io/env";
|
||||
|
||||
const trustedOrigins = env.NEXT_PUBLIC_VIEWER_URL;
|
||||
|
||||
export const assertOriginIsAllowed = (
|
||||
origin: string | undefined,
|
||||
{
|
||||
allowedOrigins,
|
||||
iframeReferrerOrigin,
|
||||
}: {
|
||||
allowedOrigins: string[] | undefined;
|
||||
iframeReferrerOrigin: string | undefined;
|
||||
},
|
||||
) => {
|
||||
if (
|
||||
!origin ||
|
||||
!allowedOrigins ||
|
||||
allowedOrigins.includes(origin) ||
|
||||
(iframeReferrerOrigin &&
|
||||
trustedOrigins.includes(origin) &&
|
||||
allowedOrigins.includes(iframeReferrerOrigin))
|
||||
)
|
||||
return;
|
||||
throw new TRPCError({
|
||||
code: "FORBIDDEN",
|
||||
message: "Origin not allowed",
|
||||
});
|
||||
};
|
||||
@ -416,7 +416,10 @@ export const startChatResponseSchema = z
|
||||
typebotV6Schema.shape.version,
|
||||
]),
|
||||
theme: themeSchema,
|
||||
settings: settingsSchema,
|
||||
settings: settingsSchema.pick({
|
||||
general: true,
|
||||
typingEmulation: true,
|
||||
}),
|
||||
publishedAt: z.coerce.date().optional(),
|
||||
}),
|
||||
})
|
||||
|
||||
@ -3,7 +3,6 @@ import {
|
||||
removePaymentInProgressFromStorage,
|
||||
} from "@/features/blocks/inputs/payment/helpers/paymentInProgressStorage";
|
||||
import type { BotContext } from "@/types";
|
||||
import { CorsError } from "@/utils/CorsError";
|
||||
import { guessApiHost } from "@/utils/guessApiHost";
|
||||
import type {
|
||||
ContinueChatResponse,
|
||||
@ -16,7 +15,6 @@ import { isNotDefined, isNotEmpty } from "@typebot.io/lib/utils";
|
||||
import ky from "ky";
|
||||
|
||||
type Props = {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
typebot: string | any;
|
||||
stripeRedirectStatus?: string;
|
||||
apiHost?: string;
|
||||
@ -91,16 +89,6 @@ export async function startChatQuery({
|
||||
},
|
||||
);
|
||||
|
||||
const corsAllowOrigin = response.headers.get("access-control-allow-origin");
|
||||
|
||||
if (
|
||||
iframeReferrerOrigin &&
|
||||
corsAllowOrigin &&
|
||||
corsAllowOrigin !== "*" &&
|
||||
!iframeReferrerOrigin.includes(corsAllowOrigin)
|
||||
)
|
||||
throw new CorsError(corsAllowOrigin);
|
||||
|
||||
return { data: await response.json<StartChatResponse>() };
|
||||
} catch (error) {
|
||||
return { error };
|
||||
|
||||
Loading…
Reference in New Issue
Block a user