Skip to content

Commit 1365a27

Browse files
committed
feat: hardened input validation
1 parent f72f819 commit 1365a27

File tree

6 files changed

+536
-402
lines changed

6 files changed

+536
-402
lines changed

.vscode/settings.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,9 @@
88
"typescript.tsdk": "node_modules/typescript/lib",
99
"eslint.packageManager": "yarn",
1010
"prettier.useEditorConfig": true,
11-
"prettier.configPath": "prettier-config/.prettierrc.js"
11+
"prettier.configPath": "prettier-config/.prettierrc.js",
12+
"sonarlint.connectedMode.project": {
13+
"connectionId": "kleros",
14+
"projectKey": "kleros_kleros-v2"
15+
}
1216
}

web/netlify/functions/update-settings.ts

Lines changed: 74 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,40 +3,108 @@ import { verifyTypedData } from "viem";
33
import { createClient } from "@supabase/supabase-js";
44
import { Database } from "../../src/types/supabase-notification";
55
import messages from "../../src/consts/eip712-messages";
6+
import { EMAIL_REGEX, TELEGRAM_REGEX, ETH_ADDRESS_REGEX, ETH_SIGNATURE_REGEX } from "../../src/consts/index";
67

7-
const supabase = createClient<Database>(process.env.SUPABASE_URL!, process.env.SUPABASE_CLIENT_API_KEY!);
8+
type NotificationSettings = {
9+
email?: string;
10+
telegram?: string;
11+
nonce: `${number}`;
12+
address: `0x${string}`;
13+
signature: string;
14+
};
15+
16+
const parse = (inputString: string): NotificationSettings => {
17+
let input;
18+
try {
19+
input = JSON.parse(inputString);
20+
} catch (err) {
21+
throw new Error("Invalid JSON format");
22+
}
23+
24+
const requiredKeys: (keyof NotificationSettings)[] = ["nonce", "address", "signature"];
25+
const optionalKeys: (keyof NotificationSettings)[] = ["email", "telegram"];
26+
const receivedKeys = Object.keys(input);
27+
28+
for (const key of requiredKeys) {
29+
if (!receivedKeys.includes(key)) {
30+
throw new Error(`Missing key: ${key}`);
31+
}
32+
}
33+
34+
const allExpectedKeys = [...requiredKeys, ...optionalKeys];
35+
for (const key of receivedKeys) {
36+
if (!allExpectedKeys.includes(key as keyof NotificationSettings)) {
37+
throw new Error(`Unexpected key: ${key}`);
38+
}
39+
}
40+
41+
const email = input.email ? input.email.trim() : "";
42+
if (email && !EMAIL_REGEX.test(email)) {
43+
throw new Error("Invalid email format");
44+
}
45+
46+
const telegram = input.telegram ? input.telegram.trim() : "";
47+
if (telegram && !TELEGRAM_REGEX.test(telegram)) {
48+
throw new Error("Invalid Telegram username format");
49+
}
50+
51+
if (!/^\d+$/.test(input.nonce)) {
52+
throw new Error("Invalid nonce format. Expected an integer as a string.");
53+
}
54+
55+
if (!ETH_ADDRESS_REGEX.test(input.address)) {
56+
throw new Error("Invalid Ethereum address format");
57+
}
58+
59+
if (!ETH_SIGNATURE_REGEX.test(input.signature)) {
60+
throw new Error("Invalid signature format");
61+
}
62+
63+
return {
64+
email: input.email.trim(),
65+
telegram: input.telegram.trim(),
66+
nonce: input.nonce,
67+
address: input.address.trim().toLowerCase(),
68+
signature: input.signature.trim(),
69+
};
70+
};
871

972
export const handler: Handler = async (event) => {
1073
try {
1174
if (!event.body) {
1275
throw new Error("No body provided");
1376
}
14-
// TODO: sanitize event.body
15-
const { email, telegram, nonce, address, signature } = JSON.parse(event.body);
77+
const { email, telegram, nonce, address, signature } = parse(event.body);
1678
const lowerCaseAddress = address.toLowerCase() as `0x${string}`;
1779
// Note: this does NOT work for smart contract wallets, but viem's publicClient.verifyMessage() fails to verify atm.
1880
// https://viem.sh/docs/utilities/verifyTypedData.html
81+
const data = messages.contactDetails(address, nonce, telegram, email);
1982
const isValid = await verifyTypedData({
20-
...messages.contactDetails(address, nonce, telegram, email),
83+
...data,
2184
signature,
2285
});
2386
if (!isValid) {
2487
// If the recovered address does not match the provided address, return an error
2588
throw new Error("Signature verification failed");
2689
}
27-
// TODO: use typed supabase client
90+
91+
const supabase = createClient<Database>(process.env.SUPABASE_URL!, process.env.SUPABASE_CLIENT_API_KEY!);
92+
2893
// If the message is empty, delete the user record
2994
if (email === "" && telegram === "") {
3095
const { error } = await supabase.from("users").delete().match({ address: lowerCaseAddress });
3196
if (error) throw error;
3297
return { statusCode: 200, body: JSON.stringify({ message: "Record deleted successfully." }) };
3398
}
99+
34100
// For a user matching this address, upsert the user record
35101
const { error } = await supabase
36102
.from("user-settings")
37103
.upsert({ address: lowerCaseAddress, email: email, telegram: telegram })
38104
.match({ address: lowerCaseAddress });
39-
if (error) throw error;
105+
if (error) {
106+
throw error;
107+
}
40108
return { statusCode: 200, body: JSON.stringify({ message: "Record updated successfully." }) };
41109
} catch (err) {
42110
return { statusCode: 500, body: JSON.stringify({ message: `Error: ${err}` }) };

web/package.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,15 +55,15 @@
5555
"@typescript-eslint/eslint-plugin": "^5.58.0",
5656
"@typescript-eslint/parser": "^5.61.0",
5757
"@typescript-eslint/utils": "^5.58.0",
58-
"@wagmi/cli": "^1.3.0",
58+
"@wagmi/cli": "^1.5.2",
5959
"eslint": "^8.38.0",
6060
"eslint-config-prettier": "^8.8.0",
6161
"eslint-import-resolver-parcel": "^1.10.6",
6262
"eslint-plugin-react": "^7.33.0",
6363
"eslint-plugin-react-hooks": "^4.6.0",
6464
"lru-cache": "^7.18.3",
6565
"parcel": "2.8.3",
66-
"supabase": "^1.88.0",
66+
"supabase": "^1.102.2",
6767
"typescript": "^4.9.5"
6868
},
6969
"dependencies": {
@@ -99,8 +99,8 @@
9999
"react-toastify": "^9.1.3",
100100
"react-use": "^17.4.0",
101101
"styled-components": "^5.3.9",
102-
"viem": "^0.3.48",
103-
"wagmi": "^1.1.0"
102+
"viem": "^1.15.4",
103+
"wagmi": "^1.4.3"
104104
},
105105
"volta": {
106106
"node": "16.20.1",

web/src/consts/index.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,10 @@ export const GIT_HASH = gitCommitShortHash;
1010
export const GIT_DIRTY = clean ? "" : "-dirty";
1111
export const GIT_URL = `https://github.com/kleros/kleros-v2/tree/${gitCommitHash}/web`;
1212
export const RELEASE_VERSION = version;
13+
14+
// https://www.w3.org/TR/2012/WD-html-markup-20120329/input.email.html#input.email.attrs.value.single
15+
// eslint-disable-next-line security/detect-unsafe-regex
16+
export const EMAIL_REGEX = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/;
17+
export const TELEGRAM_REGEX = /^@\w{5,32}$/;
18+
export const ETH_ADDRESS_REGEX = /^0x[a-fA-F0-9]{40}$/;
19+
export const ETH_SIGNATURE_REGEX = /^0x[a-fA-F0-9]{130}$/;

web/src/layout/Header/navbar/Menu/Settings/Notifications/FormContactDetails/index.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { Button } from "@kleros/ui-components-library";
55
import { uploadSettingsToSupabase } from "utils/uploadSettingsToSupabase";
66
import FormContact from "./FormContact";
77
import messages from "../../../../../../../consts/eip712-messages";
8+
import { EMAIL_REGEX, TELEGRAM_REGEX } from "../../../../../../../consts/index";
89
import { ISettings } from "../../types";
910

1011
const FormContainer = styled.form`
@@ -70,7 +71,7 @@ const FormContactDetails: React.FC<ISettings> = ({ setIsSettingsOpen }) => {
7071
contactIsValid={telegramIsValid}
7172
setContactInput={setTelegramInput}
7273
setContactIsValid={setTelegramIsValid}
73-
validator={/^@[a-zA-Z0-9_]{5,32}$/}
74+
validator={TELEGRAM_REGEX}
7475
/>
7576
</FormContactContainer>
7677
<FormContactContainer>
@@ -81,7 +82,7 @@ const FormContactDetails: React.FC<ISettings> = ({ setIsSettingsOpen }) => {
8182
contactIsValid={emailIsValid}
8283
setContactInput={setEmailInput}
8384
setContactIsValid={setEmailIsValid}
84-
validator={/^([a-zA-Z0-9._%-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})$/}
85+
validator={EMAIL_REGEX}
8586
/>
8687
</FormContactContainer>
8788

0 commit comments

Comments
 (0)