Skip to content

Commit a68f0c9

Browse files
committed
🔒️ Restrict client code execution on imported bot
1 parent a5cf298 commit a68f0c9

File tree

27 files changed

+503
-159
lines changed

27 files changed

+503
-159
lines changed

apps/builder/src/features/blocks/logic/script/components/ScriptSettings.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { MoreInfoTooltip } from "@typebot.io/ui/components/MoreInfoTooltip";
55
import { Switch } from "@typebot.io/ui/components/Switch";
66
import { CodeEditor } from "@/components/inputs/CodeEditor";
77
import { DebouncedTextInput } from "@/components/inputs/DebouncedTextInput";
8+
import { UnsafeScriptAlert } from "./UnsafeScriptAlert";
89

910
type Props = {
1011
options: ScriptBlock["options"];
@@ -21,6 +22,8 @@ export const ScriptSettings = ({ options, onOptionsChange }: Props) => {
2122
const updateClientExecution = (isExecutedOnClient: boolean) =>
2223
onOptionsChange({ ...options, isExecutedOnClient });
2324

25+
const updateIsUnsafe = () => onOptionsChange({ ...options, isUnsafe: false });
26+
2427
return (
2528
<div className="flex flex-col gap-4">
2629
<Field.Root>
@@ -46,6 +49,9 @@ export const ScriptSettings = ({ options, onOptionsChange }: Props) => {
4649
</MoreInfoTooltip>
4750
</Field.Label>
4851
</Field.Root>
52+
{options?.isUnsafe === true && options?.isExecutedOnClient !== false && (
53+
<UnsafeScriptAlert onTrustClick={updateIsUnsafe} />
54+
)}
4955
<CodeEditor
5056
defaultValue={options?.content}
5157
lang="javascript"
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { Alert } from "@typebot.io/ui/components/Alert";
2+
import { Button } from "@typebot.io/ui/components/Button";
3+
import { TriangleAlertIcon } from "@typebot.io/ui/icons/TriangleAlertIcon";
4+
5+
export const UnsafeScriptAlert = ({
6+
onTrustClick,
7+
}: {
8+
onTrustClick: () => void;
9+
}) => (
10+
<Alert.Root variant="warning">
11+
<TriangleAlertIcon />
12+
<Alert.Description className="flex flex-col gap-2">
13+
<p>
14+
For security reasons, since this bot was imported from a potential
15+
untrusted source, we have disabled some access on bot preview. Only
16+
enable this option if you understand what the code is doing and you know
17+
what you are doing, otherwise it could be a security risk.
18+
</p>
19+
<Button variant="outline" onClick={onTrustClick}>
20+
I trust this code
21+
</Button>
22+
</Alert.Description>
23+
</Alert.Root>
24+
);

apps/builder/src/features/blocks/logic/setVariable/components/SetVariableSettings.tsx

Lines changed: 68 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import { DebouncedTextInput } from "@/components/inputs/DebouncedTextInput";
2525
import { VariablesCombobox } from "@/components/inputs/VariablesCombobox";
2626
import { WhatsAppLogo } from "@/components/logos/WhatsAppLogo";
2727
import { useTypebot } from "@/features/editor/providers/TypebotProvider";
28+
import { UnsafeScriptAlert } from "../../script/components/UnsafeScriptAlert";
2829

2930
type Props = {
3031
options: SetVariableBlock["options"];
@@ -217,74 +218,79 @@ const SetVariableValue = ({
217218
});
218219
};
219220

221+
const updateIsUnsafe = () => onOptionsChange({ ...options, isUnsafe: false });
222+
220223
switch (options?.type) {
221224
case "Custom":
222225
case undefined:
223226
return (
224-
<>
225-
<Field.Root className="flex-row items-center">
226-
<Switch
227-
checked={
228-
options?.isExecutedOnClient ??
229-
defaultSetVariableOptions.isExecutedOnClient
230-
}
231-
onCheckedChange={updateClientExecution}
232-
/>
233-
<Field.Label>
234-
Execute on client{" "}
235-
<MoreInfoTooltip>
236-
Check this if you need access to client-only variables like
237-
`window` or `document`.
238-
</MoreInfoTooltip>
239-
</Field.Label>
240-
</Field.Root>
241-
<div className="flex flex-col gap-2">
242-
<RadioGroup
243-
onValueChange={(value) => updateIsCode(value as "Text" | "Code")}
244-
defaultValue={
245-
(options?.isCode ?? defaultSetVariableOptions.isCode)
246-
? "Code"
247-
: "Text"
248-
}
249-
>
250-
<Label className="hover:bg-gray-2/50 rounded-md p-2 border flex-1 flex justify-center">
251-
<Radio value="Text" className="hidden" />
252-
Text
253-
</Label>
254-
<Label className="hover:bg-gray-2/50 rounded-md p-2 border flex-1 flex justify-center">
255-
<Radio value="Code" className="hidden" />
256-
Code
257-
</Label>
258-
</RadioGroup>
259-
{options?.isCode ? (
260-
<div className="flex flex-col gap-2">
261-
<DebouncedTextInput
262-
placeholder="Code description"
263-
defaultValue={options?.expressionDescription}
264-
onValueChange={updateExpressionDescription}
265-
/>
266-
<CodeEditor
267-
defaultValue={options?.expressionToEvaluate ?? ""}
268-
onChange={updateExpression}
269-
lang="javascript"
270-
withLineNumbers={true}
271-
/>
272-
<Field.Root>
273-
<Field.Label>Save error</Field.Label>
274-
<VariablesCombobox
275-
initialVariableId={options.saveErrorInVariableId}
276-
onSelectVariable={updateSaveErrorInVariableId}
277-
/>
278-
</Field.Root>
279-
</div>
280-
) : (
281-
<DebouncedTextareaWithVariablesButton
227+
<div className="flex flex-col gap-2">
228+
<RadioGroup
229+
onValueChange={(value) => updateIsCode(value as "Text" | "Code")}
230+
defaultValue={
231+
(options?.isCode ?? defaultSetVariableOptions.isCode)
232+
? "Code"
233+
: "Text"
234+
}
235+
>
236+
<Label className="hover:bg-gray-2/50 rounded-md p-2 border flex-1 flex justify-center">
237+
<Radio value="Text" className="hidden" />
238+
Text
239+
</Label>
240+
<Label className="hover:bg-gray-2/50 rounded-md p-2 border flex-1 flex justify-center">
241+
<Radio value="Code" className="hidden" />
242+
Code
243+
</Label>
244+
</RadioGroup>
245+
{options?.isCode ? (
246+
<div className="flex flex-col gap-2">
247+
<DebouncedTextInput
248+
placeholder="Code description"
249+
defaultValue={options?.expressionDescription}
250+
onValueChange={updateExpressionDescription}
251+
/>
252+
<CodeEditor
282253
defaultValue={options?.expressionToEvaluate ?? ""}
283-
onValueChange={updateExpression}
254+
onChange={updateExpression}
255+
lang="javascript"
256+
withLineNumbers={true}
284257
/>
285-
)}
286-
</div>
287-
</>
258+
<Field.Root className="flex-row items-center">
259+
<Switch
260+
checked={
261+
options?.isExecutedOnClient ??
262+
defaultSetVariableOptions.isExecutedOnClient
263+
}
264+
onCheckedChange={updateClientExecution}
265+
/>
266+
<Field.Label>
267+
Execute on client{" "}
268+
<MoreInfoTooltip>
269+
Check this if you need access to client-only variables like
270+
`window` or `document`.
271+
</MoreInfoTooltip>
272+
</Field.Label>
273+
</Field.Root>
274+
{options?.isUnsafe === true &&
275+
options?.isExecutedOnClient === true &&
276+
options.isCode && (
277+
<UnsafeScriptAlert onTrustClick={updateIsUnsafe} />
278+
)}
279+
<Field.Root>
280+
<Field.Label>Save error</Field.Label>
281+
<VariablesCombobox
282+
initialVariableId={options.saveErrorInVariableId}
283+
onSelectVariable={updateSaveErrorInVariableId}
284+
/>
285+
</Field.Root>
286+
</div>
287+
) : (
288+
<DebouncedTextareaWithVariablesButton
289+
defaultValue={options?.expressionToEvaluate ?? ""}
290+
onValueChange={updateExpression}
291+
/>
292+
)}
293+
</div>
288294
);
289295
case "Pop":
290296
case "Shift":

apps/builder/src/features/templates/components/CreateNewTypebotButtons.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ export const CreateNewTypebotButtons = () => {
5757

5858
const handleCreateSubmit = async (
5959
typebot?: Typebot,
60-
fromTemplate?: string,
60+
args?: { enableSafetyFlags?: boolean; fromTemplate?: string },
6161
) => {
6262
if (!user || !workspace) return;
6363
const folderId = router.query.folderId?.toString() ?? null;
@@ -68,7 +68,8 @@ export const CreateNewTypebotButtons = () => {
6868
...typebot,
6969
folderId,
7070
},
71-
fromTemplate,
71+
fromTemplate: args?.fromTemplate,
72+
enableSafetyFlags: args?.enableSafetyFlags,
7273
});
7374
else
7475
createTypebot({

apps/builder/src/features/templates/components/ImportTypebotFromFileButton.tsx

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import type { ChangeEvent } from "react";
99
import { toast } from "@/lib/toast";
1010

1111
type Props = {
12-
onNewTypebot: (typebot: Typebot) => void;
12+
onNewTypebot: (typebot: Typebot, args: { enableSafetyFlags: true }) => void;
1313
} & ButtonProps;
1414

1515
export const ImportTypebotFromFileButton = ({
@@ -24,12 +24,15 @@ export const ImportTypebotFromFileButton = ({
2424
const fileContent = await readFile(file);
2525
try {
2626
const typebot = JSON.parse(fileContent);
27-
onNewTypebot({
28-
...typebot,
29-
events: typebot.events ?? null,
30-
icon: typebot.icon ?? null,
31-
name: typebot.name ?? "My typebot",
32-
} as Typebot);
27+
onNewTypebot(
28+
{
29+
...typebot,
30+
events: typebot.events ?? null,
31+
icon: typebot.icon ?? null,
32+
name: typebot.name ?? "My typebot",
33+
} as Typebot,
34+
{ enableSafetyFlags: true },
35+
);
3336
} catch (err) {
3437
console.error(err);
3538
toast(await parseUnknownClientError({ err }));

apps/builder/src/features/templates/components/TemplatesDialog.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import type { TemplateProps } from "../types";
1414
type Props = {
1515
isOpen: boolean;
1616
onClose: () => void;
17-
onTypebotChoose: (typebot: Typebot, fromTemplate: string) => void;
17+
onTypebotChoose: (typebot: Typebot, args: { fromTemplate: string }) => void;
1818
isLoading: boolean;
1919
};
2020

@@ -60,7 +60,7 @@ export const TemplatesDialog = ({
6060

6161
const onUseThisTemplateClick = async () => {
6262
if (!typebot) return;
63-
onTypebotChoose(typebot, selectedTemplate.name);
63+
onTypebotChoose(typebot, { fromTemplate: selectedTemplate.name });
6464
};
6565

6666
return (

apps/builder/src/features/typebot/api/createTypebot.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ export const createTypebot = authenticatedProcedure
8484
}
8585

8686
const groups = (
87-
typebot.groups ? await sanitizeGroups(workspace)(typebot.groups) : []
87+
typebot.groups ? await sanitizeGroups(typebot.groups, { workspace }) : []
8888
) as TypebotV6["groups"];
8989
const newTypebot = await prisma.typebot.create({
9090
data: {

apps/builder/src/features/typebot/api/importTypebot.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ export const importTypebot = authenticatedProcedure
109109
),
110110
typebot: importingTypebotSchema,
111111
fromTemplate: z.string().optional(),
112+
enableSafetyFlags: z.boolean().optional(),
112113
}),
113114
)
114115
.output(
@@ -118,7 +119,7 @@ export const importTypebot = authenticatedProcedure
118119
)
119120
.mutation(
120121
async ({
121-
input: { typebot, workspaceId, fromTemplate },
122+
input: { typebot, workspaceId, fromTemplate, enableSafetyFlags },
122123
ctx: { user },
123124
}) => {
124125
const workspace = await prisma.workspace.findUnique({
@@ -146,7 +147,10 @@ export const importTypebot = authenticatedProcedure
146147

147148
const groups = (
148149
duplicatingBot.groups
149-
? await sanitizeGroups(workspace)(duplicatingBot.groups)
150+
? await sanitizeGroups(duplicatingBot.groups, {
151+
workspace,
152+
enableSafetyFlags,
153+
})
150154
: []
151155
) as TypebotV6["groups"];
152156

apps/builder/src/features/typebot/api/updateTypebot.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,9 @@ export const updateTypebot = authenticatedProcedure
166166
}
167167

168168
const groups = typebot.groups
169-
? await sanitizeGroups(existingTypebot.workspace)(typebot.groups)
169+
? await sanitizeGroups(typebot.groups, {
170+
workspace: existingTypebot.workspace,
171+
})
170172
: undefined;
171173

172174
const newTypebot = await prisma.typebot.update({

0 commit comments

Comments
 (0)