VYPR
High severityOSV Advisory· Published Jan 22, 2026· Updated Jan 22, 2026

Typebot Vulnerable to Credential Theft via Client-Side Script Execution and API Authorization Bypass

CVE-2025-65098

Description

Typebot is an open-source chatbot builder. In versions prior to 3.13.2, client-side script execution in Typebot allows stealing all stored credentials from any user. When a victim previews a malicious typebot by clicking "Run", JavaScript executes in their browser and exfiltrates their OpenAI keys, Google Sheets tokens, and SMTP passwords. The /api/trpc/credentials.getCredentials endpoint returns plaintext API keys without verifying credential ownership. Version 3.13.2 fixes the issue.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
@typebot.io/jsnpm
< 0.9.150.9.15

Affected products

1

Patches

1
a68f0c91790a

🔒️ Restrict client code execution on imported bot

https://github.com/baptisteArno/typebot.ioBaptiste ArnaudNov 18, 2025via ghsa
27 files changed · +503 159
  • apps/builder/src/features/blocks/logic/script/components/ScriptSettings.tsx+6 0 modified
    @@ -5,6 +5,7 @@ import { MoreInfoTooltip } from "@typebot.io/ui/components/MoreInfoTooltip";
     import { Switch } from "@typebot.io/ui/components/Switch";
     import { CodeEditor } from "@/components/inputs/CodeEditor";
     import { DebouncedTextInput } from "@/components/inputs/DebouncedTextInput";
    +import { UnsafeScriptAlert } from "./UnsafeScriptAlert";
     
     type Props = {
       options: ScriptBlock["options"];
    @@ -21,6 +22,8 @@ export const ScriptSettings = ({ options, onOptionsChange }: Props) => {
       const updateClientExecution = (isExecutedOnClient: boolean) =>
         onOptionsChange({ ...options, isExecutedOnClient });
     
    +  const updateIsUnsafe = () => onOptionsChange({ ...options, isUnsafe: false });
    +
       return (
         <div className="flex flex-col gap-4">
           <Field.Root>
    @@ -46,6 +49,9 @@ export const ScriptSettings = ({ options, onOptionsChange }: Props) => {
               </MoreInfoTooltip>
             </Field.Label>
           </Field.Root>
    +      {options?.isUnsafe === true && options?.isExecutedOnClient !== false && (
    +        <UnsafeScriptAlert onTrustClick={updateIsUnsafe} />
    +      )}
           <CodeEditor
             defaultValue={options?.content}
             lang="javascript"
    
  • apps/builder/src/features/blocks/logic/script/components/UnsafeScriptAlert.tsx+24 0 added
    @@ -0,0 +1,24 @@
    +import { Alert } from "@typebot.io/ui/components/Alert";
    +import { Button } from "@typebot.io/ui/components/Button";
    +import { TriangleAlertIcon } from "@typebot.io/ui/icons/TriangleAlertIcon";
    +
    +export const UnsafeScriptAlert = ({
    +  onTrustClick,
    +}: {
    +  onTrustClick: () => void;
    +}) => (
    +  <Alert.Root variant="warning">
    +    <TriangleAlertIcon />
    +    <Alert.Description className="flex flex-col gap-2">
    +      <p>
    +        For security reasons, since this bot was imported from a potential
    +        untrusted source, we have disabled some access on bot preview. Only
    +        enable this option if you understand what the code is doing and you know
    +        what you are doing, otherwise it could be a security risk.
    +      </p>
    +      <Button variant="outline" onClick={onTrustClick}>
    +        I trust this code
    +      </Button>
    +    </Alert.Description>
    +  </Alert.Root>
    +);
    
  • apps/builder/src/features/blocks/logic/setVariable/components/SetVariableSettings.tsx+68 62 modified
    @@ -25,6 +25,7 @@ import { DebouncedTextInput } from "@/components/inputs/DebouncedTextInput";
     import { VariablesCombobox } from "@/components/inputs/VariablesCombobox";
     import { WhatsAppLogo } from "@/components/logos/WhatsAppLogo";
     import { useTypebot } from "@/features/editor/providers/TypebotProvider";
    +import { UnsafeScriptAlert } from "../../script/components/UnsafeScriptAlert";
     
     type Props = {
       options: SetVariableBlock["options"];
    @@ -217,74 +218,79 @@ const SetVariableValue = ({
         });
       };
     
    +  const updateIsUnsafe = () => onOptionsChange({ ...options, isUnsafe: false });
    +
       switch (options?.type) {
         case "Custom":
         case undefined:
           return (
    -        <>
    -          <Field.Root className="flex-row items-center">
    -            <Switch
    -              checked={
    -                options?.isExecutedOnClient ??
    -                defaultSetVariableOptions.isExecutedOnClient
    -              }
    -              onCheckedChange={updateClientExecution}
    -            />
    -            <Field.Label>
    -              Execute on client{" "}
    -              <MoreInfoTooltip>
    -                Check this if you need access to client-only variables like
    -                `window` or `document`.
    -              </MoreInfoTooltip>
    -            </Field.Label>
    -          </Field.Root>
    -          <div className="flex flex-col gap-2">
    -            <RadioGroup
    -              onValueChange={(value) => updateIsCode(value as "Text" | "Code")}
    -              defaultValue={
    -                (options?.isCode ?? defaultSetVariableOptions.isCode)
    -                  ? "Code"
    -                  : "Text"
    -              }
    -            >
    -              <Label className="hover:bg-gray-2/50 rounded-md p-2 border flex-1 flex justify-center">
    -                <Radio value="Text" className="hidden" />
    -                Text
    -              </Label>
    -              <Label className="hover:bg-gray-2/50 rounded-md p-2 border flex-1 flex justify-center">
    -                <Radio value="Code" className="hidden" />
    -                Code
    -              </Label>
    -            </RadioGroup>
    -            {options?.isCode ? (
    -              <div className="flex flex-col gap-2">
    -                <DebouncedTextInput
    -                  placeholder="Code description"
    -                  defaultValue={options?.expressionDescription}
    -                  onValueChange={updateExpressionDescription}
    -                />
    -                <CodeEditor
    -                  defaultValue={options?.expressionToEvaluate ?? ""}
    -                  onChange={updateExpression}
    -                  lang="javascript"
    -                  withLineNumbers={true}
    -                />
    -                <Field.Root>
    -                  <Field.Label>Save error</Field.Label>
    -                  <VariablesCombobox
    -                    initialVariableId={options.saveErrorInVariableId}
    -                    onSelectVariable={updateSaveErrorInVariableId}
    -                  />
    -                </Field.Root>
    -              </div>
    -            ) : (
    -              <DebouncedTextareaWithVariablesButton
    +        <div className="flex flex-col gap-2">
    +          <RadioGroup
    +            onValueChange={(value) => updateIsCode(value as "Text" | "Code")}
    +            defaultValue={
    +              (options?.isCode ?? defaultSetVariableOptions.isCode)
    +                ? "Code"
    +                : "Text"
    +            }
    +          >
    +            <Label className="hover:bg-gray-2/50 rounded-md p-2 border flex-1 flex justify-center">
    +              <Radio value="Text" className="hidden" />
    +              Text
    +            </Label>
    +            <Label className="hover:bg-gray-2/50 rounded-md p-2 border flex-1 flex justify-center">
    +              <Radio value="Code" className="hidden" />
    +              Code
    +            </Label>
    +          </RadioGroup>
    +          {options?.isCode ? (
    +            <div className="flex flex-col gap-2">
    +              <DebouncedTextInput
    +                placeholder="Code description"
    +                defaultValue={options?.expressionDescription}
    +                onValueChange={updateExpressionDescription}
    +              />
    +              <CodeEditor
                     defaultValue={options?.expressionToEvaluate ?? ""}
    -                onValueChange={updateExpression}
    +                onChange={updateExpression}
    +                lang="javascript"
    +                withLineNumbers={true}
                   />
    -            )}
    -          </div>
    -        </>
    +              <Field.Root className="flex-row items-center">
    +                <Switch
    +                  checked={
    +                    options?.isExecutedOnClient ??
    +                    defaultSetVariableOptions.isExecutedOnClient
    +                  }
    +                  onCheckedChange={updateClientExecution}
    +                />
    +                <Field.Label>
    +                  Execute on client{" "}
    +                  <MoreInfoTooltip>
    +                    Check this if you need access to client-only variables like
    +                    `window` or `document`.
    +                  </MoreInfoTooltip>
    +                </Field.Label>
    +              </Field.Root>
    +              {options?.isUnsafe === true &&
    +                options?.isExecutedOnClient === true &&
    +                options.isCode && (
    +                  <UnsafeScriptAlert onTrustClick={updateIsUnsafe} />
    +                )}
    +              <Field.Root>
    +                <Field.Label>Save error</Field.Label>
    +                <VariablesCombobox
    +                  initialVariableId={options.saveErrorInVariableId}
    +                  onSelectVariable={updateSaveErrorInVariableId}
    +                />
    +              </Field.Root>
    +            </div>
    +          ) : (
    +            <DebouncedTextareaWithVariablesButton
    +              defaultValue={options?.expressionToEvaluate ?? ""}
    +              onValueChange={updateExpression}
    +            />
    +          )}
    +        </div>
           );
         case "Pop":
         case "Shift":
    
  • apps/builder/src/features/templates/components/CreateNewTypebotButtons.tsx+3 2 modified
    @@ -57,7 +57,7 @@ export const CreateNewTypebotButtons = () => {
     
       const handleCreateSubmit = async (
         typebot?: Typebot,
    -    fromTemplate?: string,
    +    args?: { enableSafetyFlags?: boolean; fromTemplate?: string },
       ) => {
         if (!user || !workspace) return;
         const folderId = router.query.folderId?.toString() ?? null;
    @@ -68,7 +68,8 @@ export const CreateNewTypebotButtons = () => {
               ...typebot,
               folderId,
             },
    -        fromTemplate,
    +        fromTemplate: args?.fromTemplate,
    +        enableSafetyFlags: args?.enableSafetyFlags,
           });
         else
           createTypebot({
    
  • apps/builder/src/features/templates/components/ImportTypebotFromFileButton.tsx+10 7 modified
    @@ -9,7 +9,7 @@ import type { ChangeEvent } from "react";
     import { toast } from "@/lib/toast";
     
     type Props = {
    -  onNewTypebot: (typebot: Typebot) => void;
    +  onNewTypebot: (typebot: Typebot, args: { enableSafetyFlags: true }) => void;
     } & ButtonProps;
     
     export const ImportTypebotFromFileButton = ({
    @@ -24,12 +24,15 @@ export const ImportTypebotFromFileButton = ({
         const fileContent = await readFile(file);
         try {
           const typebot = JSON.parse(fileContent);
    -      onNewTypebot({
    -        ...typebot,
    -        events: typebot.events ?? null,
    -        icon: typebot.icon ?? null,
    -        name: typebot.name ?? "My typebot",
    -      } as Typebot);
    +      onNewTypebot(
    +        {
    +          ...typebot,
    +          events: typebot.events ?? null,
    +          icon: typebot.icon ?? null,
    +          name: typebot.name ?? "My typebot",
    +        } as Typebot,
    +        { enableSafetyFlags: true },
    +      );
         } catch (err) {
           console.error(err);
           toast(await parseUnknownClientError({ err }));
    
  • apps/builder/src/features/templates/components/TemplatesDialog.tsx+2 2 modified
    @@ -14,7 +14,7 @@ import type { TemplateProps } from "../types";
     type Props = {
       isOpen: boolean;
       onClose: () => void;
    -  onTypebotChoose: (typebot: Typebot, fromTemplate: string) => void;
    +  onTypebotChoose: (typebot: Typebot, args: { fromTemplate: string }) => void;
       isLoading: boolean;
     };
     
    @@ -60,7 +60,7 @@ export const TemplatesDialog = ({
     
       const onUseThisTemplateClick = async () => {
         if (!typebot) return;
    -    onTypebotChoose(typebot, selectedTemplate.name);
    +    onTypebotChoose(typebot, { fromTemplate: selectedTemplate.name });
       };
     
       return (
    
  • apps/builder/src/features/typebot/api/createTypebot.ts+1 1 modified
    @@ -84,7 +84,7 @@ export const createTypebot = authenticatedProcedure
         }
     
         const groups = (
    -      typebot.groups ? await sanitizeGroups(workspace)(typebot.groups) : []
    +      typebot.groups ? await sanitizeGroups(typebot.groups, { workspace }) : []
         ) as TypebotV6["groups"];
         const newTypebot = await prisma.typebot.create({
           data: {
    
  • apps/builder/src/features/typebot/api/importTypebot.ts+6 2 modified
    @@ -109,6 +109,7 @@ export const importTypebot = authenticatedProcedure
             ),
           typebot: importingTypebotSchema,
           fromTemplate: z.string().optional(),
    +      enableSafetyFlags: z.boolean().optional(),
         }),
       )
       .output(
    @@ -118,7 +119,7 @@ export const importTypebot = authenticatedProcedure
       )
       .mutation(
         async ({
    -      input: { typebot, workspaceId, fromTemplate },
    +      input: { typebot, workspaceId, fromTemplate, enableSafetyFlags },
           ctx: { user },
         }) => {
           const workspace = await prisma.workspace.findUnique({
    @@ -146,7 +147,10 @@ export const importTypebot = authenticatedProcedure
     
           const groups = (
             duplicatingBot.groups
    -          ? await sanitizeGroups(workspace)(duplicatingBot.groups)
    +          ? await sanitizeGroups(duplicatingBot.groups, {
    +              workspace,
    +              enableSafetyFlags,
    +            })
               : []
           ) as TypebotV6["groups"];
     
    
  • apps/builder/src/features/typebot/api/updateTypebot.ts+3 1 modified
    @@ -166,7 +166,9 @@ export const updateTypebot = authenticatedProcedure
           }
     
           const groups = typebot.groups
    -        ? await sanitizeGroups(existingTypebot.workspace)(typebot.groups)
    +        ? await sanitizeGroups(typebot.groups, {
    +            workspace: existingTypebot.workspace,
    +          })
             : undefined;
     
           const newTypebot = await prisma.typebot.update({
    
  • apps/builder/src/features/typebot/helpers/sanitizers.ts+71 39 modified
    @@ -39,48 +39,80 @@ export const sanitizeSettings = (
         : undefined,
     });
     
    -export const sanitizeGroups =
    -  (workspace: Pick<Workspace, "id" | "plan">) =>
    -  async (groups: Typebot["groups"]): Promise<Typebot["groups"]> =>
    -    Promise.all(
    -      groups.map(async (group) => ({
    -        ...group,
    -        blocks: await Promise.all(group.blocks.map(sanitizeBlock(workspace))),
    -      })),
    -    ) as Promise<Typebot["groups"]>;
    +export const sanitizeGroups = async (
    +  groups: Typebot["groups"],
    +  {
    +    enableSafetyFlags,
    +    workspace,
    +  }: {
    +    enableSafetyFlags?: boolean;
    +    workspace: Pick<Workspace, "id" | "plan">;
    +  },
    +): Promise<Typebot["groups"]> =>
    +  Promise.all(
    +    groups.map(async (group) => ({
    +      ...group,
    +      blocks: await Promise.all(
    +        group.blocks.map((block) =>
    +          sanitizeBlock(block, { enableSafetyFlags, workspace }),
    +        ),
    +      ),
    +    })),
    +  ) as Promise<Typebot["groups"]>;
    +
    +const sanitizeBlock = async (
    +  block: Block,
    +  {
    +    enableSafetyFlags,
    +    workspace,
    +  }: { enableSafetyFlags?: boolean; workspace: Pick<Workspace, "id" | "plan"> },
    +): Promise<Block> => {
    +  if (!("options" in block) || !block.options) return block;
     
    -const sanitizeBlock =
    -  (workspace: Pick<Workspace, "id" | "plan">) =>
    -  async (block: Block): Promise<Block> => {
    -    if (!("options" in block) || !block.options) return block;
    +  if (
    +    enableSafetyFlags &&
    +    (block.type === LogicBlockType.SCRIPT ||
    +      block.type === LogicBlockType.SET_VARIABLE)
    +  ) {
    +    return {
    +      ...block,
    +      options: {
    +        ...block.options,
    +        isUnsafe:
    +          block.options.isExecutedOnClient === true ||
    +          (block.type === LogicBlockType.SCRIPT &&
    +            block.options.isExecutedOnClient === undefined),
    +      },
    +    };
    +  }
     
    -    switch (block.type) {
    -      case IntegrationBlockType.EMAIL:
    -        return {
    -          ...block,
    -          options: {
    -            ...block.options,
    -            credentialsId:
    -              (await sanitizeCredentialsId(workspace.id)(
    -                block.options?.credentialsId,
    -              )) ?? getDefaultEmailCredentialsId(workspace.plan),
    -          },
    -        };
    -      default:
    -        return {
    -          ...block,
    -          options: {
    -            ...block.options,
    -            proxyCredentialsId: await sanitizeCredentialsId(workspace.id)(
    -              block.options?.proxyCredentialsId,
    -            ),
    -            credentialsId: await sanitizeCredentialsId(workspace.id)(
    +  switch (block.type) {
    +    case IntegrationBlockType.EMAIL:
    +      return {
    +        ...block,
    +        options: {
    +          ...block.options,
    +          credentialsId:
    +            (await sanitizeCredentialsId(workspace.id)(
                   block.options?.credentialsId,
    -            ),
    -          },
    -        };
    -    }
    -  };
    +            )) ?? getDefaultEmailCredentialsId(workspace.plan),
    +        },
    +      };
    +    default:
    +      return {
    +        ...block,
    +        options: {
    +          ...block.options,
    +          proxyCredentialsId: await sanitizeCredentialsId(workspace.id)(
    +            block.options?.proxyCredentialsId,
    +          ),
    +          credentialsId: await sanitizeCredentialsId(workspace.id)(
    +            block.options?.credentialsId,
    +          ),
    +        },
    +      };
    +  }
    +};
     
     const sanitizeCredentialsId =
       (workspaceId: string) =>
    
  • apps/docs/openapi/builder.json+35 0 modified
    @@ -9097,6 +9097,9 @@
                       },
                       "fromTemplate": {
                         "type": "string"
    +                  },
    +                  "enableSafetyFlags": {
    +                    "type": "boolean"
                       }
                     },
                     "required": [
    @@ -15142,6 +15145,10 @@
                   "isExecutedOnClient": {
                     "type": "boolean"
                   },
    +              "isUnsafe": {
    +                "type": "boolean",
    +                "description": "Enabled by default for imported bots to prevent code to be executed in preview with priviledged access"
    +              },
                   "shouldExecuteInParentContext": {
                     "type": "boolean"
                   }
    @@ -15213,6 +15220,10 @@
                       "isExecutedOnClient": {
                         "type": "boolean"
                       },
    +                  "isUnsafe": {
    +                    "type": "boolean",
    +                    "description": "Enabled by default for imported bots to prevent code to be executed in preview with priviledged access"
    +                  },
                       "expressionToEvaluate": {
                         "type": "string"
                       },
    @@ -15236,6 +15247,10 @@
                       "isExecutedOnClient": {
                         "type": "boolean"
                       },
    +                  "isUnsafe": {
    +                    "type": "boolean",
    +                    "description": "Enabled by default for imported bots to prevent code to be executed in preview with priviledged access"
    +                  },
                       "expressionToEvaluate": {
                         "type": "string"
                       },
    @@ -15268,6 +15283,10 @@
                       "isExecutedOnClient": {
                         "type": "boolean"
                       },
    +                  "isUnsafe": {
    +                    "type": "boolean",
    +                    "description": "Enabled by default for imported bots to prevent code to be executed in preview with priviledged access"
    +                  },
                       "type": {
                         "type": "string",
                         "enum": [
    @@ -15293,6 +15312,10 @@
                       "isExecutedOnClient": {
                         "type": "boolean"
                       },
    +                  "isUnsafe": {
    +                    "type": "boolean",
    +                    "description": "Enabled by default for imported bots to prevent code to be executed in preview with priviledged access"
    +                  },
                       "type": {
                         "type": "string",
                         "enum": [
    @@ -15325,6 +15348,10 @@
                       "isExecutedOnClient": {
                         "type": "boolean"
                       },
    +                  "isUnsafe": {
    +                    "type": "boolean",
    +                    "description": "Enabled by default for imported bots to prevent code to be executed in preview with priviledged access"
    +                  },
                       "type": {
                         "type": "string",
                         "enum": [
    @@ -15359,6 +15386,10 @@
                       "isExecutedOnClient": {
                         "type": "boolean"
                       },
    +                  "isUnsafe": {
    +                    "type": "boolean",
    +                    "description": "Enabled by default for imported bots to prevent code to be executed in preview with priviledged access"
    +                  },
                       "type": {
                         "type": "string",
                         "enum": [
    @@ -15382,6 +15413,10 @@
                       "isExecutedOnClient": {
                         "type": "boolean"
                       },
    +                  "isUnsafe": {
    +                    "type": "boolean",
    +                    "description": "Enabled by default for imported bots to prevent code to be executed in preview with priviledged access"
    +                  },
                       "type": {
                         "type": "string",
                         "enum": [
    
  • apps/docs/openapi/viewer.json+41 0 modified
    @@ -7206,6 +7206,10 @@
                   "isExecutedOnClient": {
                     "type": "boolean"
                   },
    +              "isUnsafe": {
    +                "type": "boolean",
    +                "description": "Enabled by default for imported bots to prevent code to be executed in preview with priviledged access"
    +              },
                   "shouldExecuteInParentContext": {
                     "type": "boolean"
                   }
    @@ -7277,6 +7281,10 @@
                       "isExecutedOnClient": {
                         "type": "boolean"
                       },
    +                  "isUnsafe": {
    +                    "type": "boolean",
    +                    "description": "Enabled by default for imported bots to prevent code to be executed in preview with priviledged access"
    +                  },
                       "expressionToEvaluate": {
                         "type": "string"
                       },
    @@ -7300,6 +7308,10 @@
                       "isExecutedOnClient": {
                         "type": "boolean"
                       },
    +                  "isUnsafe": {
    +                    "type": "boolean",
    +                    "description": "Enabled by default for imported bots to prevent code to be executed in preview with priviledged access"
    +                  },
                       "expressionToEvaluate": {
                         "type": "string"
                       },
    @@ -7332,6 +7344,10 @@
                       "isExecutedOnClient": {
                         "type": "boolean"
                       },
    +                  "isUnsafe": {
    +                    "type": "boolean",
    +                    "description": "Enabled by default for imported bots to prevent code to be executed in preview with priviledged access"
    +                  },
                       "type": {
                         "type": "string",
                         "enum": [
    @@ -7357,6 +7373,10 @@
                       "isExecutedOnClient": {
                         "type": "boolean"
                       },
    +                  "isUnsafe": {
    +                    "type": "boolean",
    +                    "description": "Enabled by default for imported bots to prevent code to be executed in preview with priviledged access"
    +                  },
                       "type": {
                         "type": "string",
                         "enum": [
    @@ -7389,6 +7409,10 @@
                       "isExecutedOnClient": {
                         "type": "boolean"
                       },
    +                  "isUnsafe": {
    +                    "type": "boolean",
    +                    "description": "Enabled by default for imported bots to prevent code to be executed in preview with priviledged access"
    +                  },
                       "type": {
                         "type": "string",
                         "enum": [
    @@ -7423,6 +7447,10 @@
                       "isExecutedOnClient": {
                         "type": "boolean"
                       },
    +                  "isUnsafe": {
    +                    "type": "boolean",
    +                    "description": "Enabled by default for imported bots to prevent code to be executed in preview with priviledged access"
    +                  },
                       "type": {
                         "type": "string",
                         "enum": [
    @@ -7446,6 +7474,10 @@
                       "isExecutedOnClient": {
                         "type": "boolean"
                       },
    +                  "isUnsafe": {
    +                    "type": "boolean",
    +                    "description": "Enabled by default for imported bots to prevent code to be executed in preview with priviledged access"
    +                  },
                       "type": {
                         "type": "string",
                         "enum": [
    @@ -16806,6 +16838,9 @@
                   "content": {
                     "type": "string"
                   },
    +              "isUnsafe": {
    +                "type": "boolean"
    +              },
                   "isCode": {
                     "type": "boolean"
                   },
    @@ -16922,6 +16957,9 @@
                       "content": {
                         "type": "string"
                       },
    +                  "isUnsafe": {
    +                    "type": "boolean"
    +                  },
                       "isCode": {
                         "type": "boolean"
                       },
    @@ -17095,6 +17133,9 @@
                       "content": {
                         "type": "string"
                       },
    +                  "isUnsafe": {
    +                    "type": "boolean"
    +                  },
                       "isCode": {
                         "type": "boolean"
                       },
    
  • packages/blocks/logic/src/script/schema.ts+6 0 modified
    @@ -6,6 +6,12 @@ export const scriptOptionsSchema = z.object({
       name: z.string().optional(),
       content: z.string().optional(),
       isExecutedOnClient: z.boolean().optional(),
    +  isUnsafe: z
    +    .boolean()
    +    .optional()
    +    .describe(
    +      "Enabled by default for imported bots to prevent code to be executed in preview with priviledged access",
    +    ),
       shouldExecuteInParentContext: z.boolean().optional(),
     });
     
    
  • packages/blocks/logic/src/setVariable/schema.ts+6 0 modified
    @@ -6,6 +6,12 @@ import { valueTypesWithNoOptions } from "./constants";
     const baseOptions = z.object({
       variableId: z.string().optional(),
       isExecutedOnClient: z.boolean().optional(),
    +  isUnsafe: z
    +    .boolean()
    +    .optional()
    +    .describe(
    +      "Enabled by default for imported bots to prevent code to be executed in preview with priviledged access",
    +    ),
     });
     
     const basicSetVariableOptionsSchema = baseOptions.extend({
    
  • packages/bot-engine/src/blocks/logic/script/executeScript.ts+4 1 modified
    @@ -66,7 +66,10 @@ export const executeScript = async (
         clientSideActions: [
           {
             type: "scriptToExecute",
    -        scriptToExecute: scriptToExecute,
    +        scriptToExecute: {
    +          ...scriptToExecute,
    +          isUnsafe: block.options.isUnsafe,
    +        },
           },
         ],
       };
    
  • packages/bot-engine/src/blocks/logic/setVariable/executeSetVariable.ts+1 0 modified
    @@ -78,6 +78,7 @@ export const executeSetVariable = async (
                 scriptToExecute: {
                   ...scriptToExecute,
                   isCode,
    +              isUnsafe: block.options.isUnsafe,
                 },
               },
               expectsDedicatedReply: true,
    
  • packages/chat-api/src/clientSideAction.ts+1 0 modified
    @@ -16,6 +16,7 @@ export type StartPropsToInject = z.infer<typeof startPropsToInjectSchema>;
     
     const scriptToExecuteSchema = z.object({
       content: z.string(),
    +  isUnsafe: z.boolean().optional(),
       isCode: z.boolean().optional(),
       args: z.array(
         z.object({
    
  • packages/embeds/js/package.json+1 1 modified
    @@ -1,6 +1,6 @@
     {
       "name": "@typebot.io/js",
    -  "version": "0.9.14",
    +  "version": "0.9.15",
       "description": "Javascript library to display typebots on your website",
       "license": "FSL-1.1-ALv2",
       "type": "module",
    
  • packages/embeds/js/src/components/ConversationContainer/ChatContainer.tsx+1 0 modified
    @@ -345,6 +345,7 @@ export const ChatContainer = (props: Props) => {
           const response = await executeClientSideAction({
             clientSideAction: action,
             context: {
    +          isPreview: props.context.isPreview,
               apiHost: props.context.apiHost,
               wsHost: props.context.wsHost,
               sessionId: props.initialChatReply.sessionId,
    
  • packages/embeds/js/src/features/blocks/integrations/chatwoot/utils/executeChatwoot.ts+8 4 modified
    @@ -1,11 +1,15 @@
     import type { ScriptToExecute } from "@typebot.io/chat-api/clientSideAction";
     import { executeScript } from "@/features/blocks/logic/script/executeScript";
    +import type { ClientSideActionContext } from "@/types";
     import { chatwootWebWidgetOpenedMessage } from "../constants";
     
    -export const executeChatwoot = (chatwoot: {
    -  scriptToExecute: ScriptToExecute;
    -}) => {
    -  executeScript(chatwoot.scriptToExecute);
    +export const executeChatwoot = (
    +  chatwoot: {
    +    scriptToExecute: ScriptToExecute;
    +  },
    +  { isPreview }: Pick<ClientSideActionContext, "isPreview">,
    +) => {
    +  executeScript(chatwoot.scriptToExecute, { isPreview });
       return {
         scriptCallbackMessage: chatwootWebWidgetOpenedMessage,
       };
    
  • packages/embeds/js/src/features/blocks/integrations/httpRequest/executeHttpRequest.ts+2 0 modified
    @@ -2,13 +2,15 @@ import type { ExecutableHttpRequest } from "@typebot.io/blocks-integrations/http
     
     export const executeHttpRequest = async (
       httpRequestToExecute: ExecutableHttpRequest,
    +  isPreview: boolean,
     ): Promise<string> => {
       const { url, method, body, headers } = httpRequestToExecute;
       try {
         const response = await fetch(url, {
           method,
           body: method !== "GET" && body ? JSON.stringify(body) : undefined,
           headers,
    +      credentials: isPreview ? "omit" : undefined,
         });
         const statusCode = response.status;
         const data = await response.json();
    
  • packages/embeds/js/src/features/blocks/logic/script/executeScript.ts+23 17 modified
    @@ -1,19 +1,32 @@
     import type { ScriptToExecute } from "@typebot.io/chat-api/clientSideAction";
     import { parseUnknownClientError } from "@typebot.io/lib/parseUnknownClientError";
    +import type { ClientSideActionContext } from "@/types";
    +import { runUserCodeInWorker } from "./scriptRunner";
     
     const AsyncFunction = Object.getPrototypeOf(async () => {}).constructor;
     
    -export const executeScript = async ({ content, args }: ScriptToExecute) => {
    +export const executeScript = async (
    +  { content, args, isUnsafe }: ScriptToExecute,
    +  { isPreview }: Pick<ClientSideActionContext, "isPreview">,
    +) => {
       try {
    -    const func = AsyncFunction(
    -      ...args.map((arg) => arg.id),
    -      parseContent(content),
    -    );
    -    const result = await func(...args.map((arg) => arg.value));
    -    if (result && typeof result === "string")
    -      return {
    -        scriptCallbackMessage: result,
    -      };
    +    const code = content.replace(/<script>/g, "").replace(/<\/script>/g, "");
    +    if (isPreview && isUnsafe) {
    +      const argsRecord = Object.fromEntries(args.map((a) => [a.id, a.value]));
    +
    +      const result = await runUserCodeInWorker(code, argsRecord);
    +
    +      if (result && typeof result === "string") {
    +        return { scriptCallbackMessage: result };
    +      }
    +    } else {
    +      const func = AsyncFunction(...args.map((arg) => arg.id), code);
    +      const result = await func(...args.map((arg) => arg.value));
    +      if (result && typeof result === "string")
    +        return {
    +          scriptCallbackMessage: result,
    +        };
    +    }
       } catch (err) {
         console.log(err);
         return {
    @@ -27,13 +40,6 @@ export const executeScript = async ({ content, args }: ScriptToExecute) => {
       }
     };
     
    -const parseContent = (content: string) => {
    -  const contentWithoutScriptTags = content
    -    .replace(/<script>/g, "")
    -    .replace(/<\/script>/g, "");
    -  return contentWithoutScriptTags;
    -};
    -
     export const executeCode = async ({
       args,
       content,
    
  • packages/embeds/js/src/features/blocks/logic/script/scriptRunner.ts+143 0 added
    @@ -0,0 +1,143 @@
    +// Inline the worker code as a string to avoid bundler issues
    +const workerCode = `
    +const AsyncFunction = Object.getPrototypeOf(async () => {})
    +  .constructor;
    +
    +const originalFetch = self.fetch.bind(self);
    +
    +// Wrap fetch to force credentials: "omit" and strip sensitive headers
    +async function safeFetch(
    +  input,
    +  init,
    +) {
    +  const safeInit = {
    +    ...(init || {}),
    +    credentials: "omit",
    +  };
    +
    +  if (input instanceof Request) {
    +    const safeRequest = new Request(input, safeInit);
    +    return originalFetch(safeRequest);
    +  }
    +
    +  return originalFetch(input, safeInit);
    +}
    +
    +// Override global fetch BEFORE any user code runs
    +self.fetch = safeFetch;
    +
    +// Disable other network APIs that could carry cookies automatically
    +self.XMLHttpRequest = () => {
    +  console.warn("XMLHttpRequest is disabled in preview mode.");
    +};
    +
    +self.WebSocket = () => {
    +  console.warn("WebSocket is disabled in preview mode.");
    +};
    +
    +self.EventSource = () => {
    +  console.warn("EventSource is disabled in preview mode.");
    +};
    +
    +self.Worker = () => {
    +  console.warn("Creating nested workers is disabled in preview mode.");
    +};
    +
    +self.SharedWorker = () => {
    +  console.warn("Shared workers are disabled in preview mode.");
    +};
    +
    +self.onmessage = async (event) => {
    +  const { id, code, args = {} } = event.data;
    +
    +  try {
    +    const argNames = Object.keys(args);
    +    const argValues = Object.values(args);
    +
    +    // Create an async function with the given args
    +    const userFunc = new AsyncFunction(...argNames, code);
    +
    +    const result = await userFunc(...argValues);
    +
    +    const message = { id, ok: true, result };
    +    self.postMessage(message);
    +  } catch (err) {
    +    const message = {
    +      id,
    +      ok: false,
    +      error:
    +        err instanceof Error
    +          ? err.message
    +          : typeof err === "string"
    +            ? err
    +            : JSON.stringify(err),
    +    };
    +    self.postMessage(message);
    +  }
    +};
    +`;
    +
    +let workerPromise: Promise<Worker> | null = null;
    +
    +function getScriptRunnerWorker(): Promise<Worker> {
    +  if (!workerPromise) {
    +    workerPromise = new Promise((resolve, reject) => {
    +      try {
    +        if (typeof window === "undefined") {
    +          throw new Error("Script runner worker cannot be used on the server.");
    +        }
    +
    +        const blob = new Blob([workerCode], { type: "application/javascript" });
    +        const workerUrl = URL.createObjectURL(blob);
    +        const worker = new Worker(workerUrl);
    +
    +        worker.addEventListener("error", (e) => {
    +          console.error("[ScriptRunnerWorker] error", e);
    +        });
    +
    +        resolve(worker);
    +      } catch (err) {
    +        reject(err);
    +      }
    +    });
    +  }
    +  return workerPromise;
    +}
    +
    +type WorkerRequest = {
    +  id: number;
    +  code: string;
    +  args: Record<string, unknown>;
    +};
    +
    +type WorkerResponse =
    +  | { id: number; ok: true; result: unknown }
    +  | { id: number; ok: false; error: string };
    +
    +let nextId = 0;
    +
    +export const runUserCodeInWorker = async (
    +  code: string,
    +  args: Record<string, unknown>,
    +): Promise<unknown> => {
    +  const worker = await getScriptRunnerWorker();
    +
    +  return new Promise((resolve, reject) => {
    +    const id = nextId++;
    +
    +    const listener = (event: MessageEvent<WorkerResponse>) => {
    +      const msg = event.data;
    +      if (!msg || msg.id !== id) return;
    +
    +      worker.removeEventListener("message", listener);
    +
    +      if (msg.ok) resolve(msg.result);
    +      else reject(new Error(msg.error));
    +    };
    +
    +    worker.addEventListener("message", listener);
    +
    +    const payload: WorkerRequest = { id, code, args };
    +    worker.postMessage(payload);
    +  });
    +};
    
  • packages/embeds/js/src/features/blocks/logic/setVariable/executeSetVariable.ts+25 16 modified
    @@ -2,31 +2,40 @@ import type { ScriptToExecute } from "@typebot.io/chat-api/clientSideAction";
     import { parseUnknownClientError } from "@typebot.io/lib/parseUnknownClientError";
     import { safeStringify } from "@typebot.io/lib/safeStringify";
     import type { LogInSession } from "@typebot.io/logs/schemas";
    +import type { ClientSideActionContext } from "@/types";
    +import { runUserCodeInWorker } from "../script/scriptRunner";
     
     const AsyncFunction = Object.getPrototypeOf(async () => {}).constructor;
     
    -export const executeSetVariable = async ({
    -  content,
    -  args,
    -  isCode,
    -}: ScriptToExecute): Promise<{
    +export const executeSetVariable = async (
    +  { content, args, isCode, isUnsafe }: ScriptToExecute,
    +  { isPreview }: Pick<ClientSideActionContext, "isPreview">,
    +): Promise<{
       replyToSend: string | undefined;
       logs?: LogInSession[];
     }> => {
       try {
    -    // To avoid octal number evaluation
    -    if (!isNaN(content as unknown as number) && /0[^.].+/.test(content))
    +    if (isPreview && isUnsafe) {
    +      const argsRecord = Object.fromEntries(args.map((a) => [a.id, a.value]));
    +      const result = await runUserCodeInWorker(content, argsRecord);
           return {
    -        replyToSend: content,
    +        replyToSend: safeStringify(result) ?? undefined,
           };
    -    const func = AsyncFunction(
    -      ...args.map((arg) => arg.id),
    -      content.includes("return ") ? content : `return ${content}`,
    -    );
    -    const replyToSend = await func(...args.map((arg) => arg.value));
    -    return {
    -      replyToSend: safeStringify(replyToSend) ?? undefined,
    -    };
    +    } else {
    +      // To avoid octal number evaluation
    +      if (!isNaN(content as unknown as number) && /0[^.].+/.test(content))
    +        return {
    +          replyToSend: content,
    +        };
    +      const func = AsyncFunction(
    +        ...args.map((arg) => arg.id),
    +        content.includes("return ") ? content : `return ${content}`,
    +      );
    +      const replyToSend = await func(...args.map((arg) => arg.value));
    +      return {
    +        replyToSend: safeStringify(replyToSend) ?? undefined,
    +      };
    +    }
       } catch (err) {
         console.error(err);
         return {
    
  • packages/embeds/js/src/types.ts+1 0 modified
    @@ -18,6 +18,7 @@ export type ClientSideActionContext = {
       wsHost?: string;
       sessionId: string;
       resultId?: string;
    +  isPreview: boolean;
     };
     
     export type ChatChunk = Pick<
    
  • packages/embeds/js/src/utils/executeClientSideActions.ts+10 3 modified
    @@ -37,13 +37,17 @@ export const executeClientSideAction = async ({
       onStreamError,
     }: Props): Promise<ClientSideActionResponse> => {
       if ("chatwoot" in clientSideAction) {
    -    return executeChatwoot(clientSideAction.chatwoot);
    +    return executeChatwoot(clientSideAction.chatwoot, {
    +      isPreview: context.isPreview,
    +    });
       }
       if ("googleAnalytics" in clientSideAction) {
         return executeGoogleAnalyticsBlock(clientSideAction.googleAnalytics);
       }
       if ("scriptToExecute" in clientSideAction) {
    -    return executeScript(clientSideAction.scriptToExecute);
    +    return executeScript(clientSideAction.scriptToExecute, {
    +      isPreview: context.isPreview,
    +    });
       }
       if ("redirect" in clientSideAction) {
         return executeRedirect(clientSideAction.redirect);
    @@ -55,7 +59,9 @@ export const executeClientSideAction = async ({
           : undefined;
       }
       if ("setVariable" in clientSideAction) {
    -    return executeSetVariable(clientSideAction.setVariable.scriptToExecute);
    +    return executeSetVariable(clientSideAction.setVariable.scriptToExecute, {
    +      isPreview: context.isPreview,
    +    });
       }
       if (
         "streamOpenAiChatCompletion" in clientSideAction ||
    @@ -85,6 +91,7 @@ export const executeClientSideAction = async ({
       if ("httpRequestToExecute" in clientSideAction) {
         const response = await executeHttpRequest(
           clientSideAction.httpRequestToExecute,
    +      context.isPreview,
         );
         return { replyToSend: response };
       }
    
  • packages/embeds/react/package.json+1 1 modified
    @@ -1,6 +1,6 @@
     {
       "name": "@typebot.io/react",
    -  "version": "0.9.14",
    +  "version": "0.9.15",
       "description": "Convenient library to display typebots on your React app",
       "license": "FSL-1.1-ALv2",
       "type": "module",
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

4

News mentions

0

No linked articles in our index yet.