CVE-2022-4683: Missing Encryption of Sensitive Data (CWE-311)
value: Sensitive Cookie in HTTPS Session Without 'Secure' Attribute in GitHub repository usememos/memos prior to 0.9.0.
CWE description: The product does not encrypt sensitive or critical information before storage or transmission.
CVSS v3 base score: 6.5
Published: 2022-12-23T12:15Z
Repository: https://github.com/usememos/memos
Language: Other
Files changed: api/system_setting.go, api/user_setting.go, go.mod, go.sum, server/acl.go, server/system.go, web/src/components/AppearanceSelect.tsx, web/src/components/LocaleSelect.tsx, web/src/components/Settings/PreferencesSection.tsx, web/src/components/UpdateCustomizedProfileDialog.tsx, web/src/pages/Auth.tsx, web/src/store/module/global.ts, web/src/store/reducer/global.ts, web/src/types/modules/system.d.ts
Commit date: 2022-12-22 19:48:44 +0800
Commit message: feat: customize system profile (#828)


Diff: diff --git a/api/system_setting.go b/api/system_setting.go
index 197ea22..ff67d11 100644
--- a/api/system_setting.go
+++ b/api/system_setting.go
@@ -1,10 +1,12 @@
 package api
 
 import (
 	"encoding/json"
 	"fmt"
+
+	"golang.org/x/exp/slices"
 )
 
 type SystemSettingName string
 
 const (
@@ -22,10 +24,16 @@ const (
 type CustomizedProfile struct {
 	// Name is the server name, default is `memos`
 	Name string `json:"name"`
 	// IconURL is the url of icon image.
 	IconURL string `json:"iconUrl"`
+	// Description is the server description.
+	Description string `json:"description"`
+	// Locale is the server default locale.
+	Locale string `json:"locale"`
+	// Appearance is the server default appearance.
+	Appearance string `json:"appearance"`
 	// ExternalURL is the external url of server. e.g. https://usermemos.com
 	ExternalURL string `json:"externalUrl"`
 }
 
 func (key SystemSettingName) String() string {
@@ -88,19 +96,28 @@ func (upsert SystemSettingUpsert) Validate() error {
 		err := json.Unmarshal([]byte(upsert.Value), &value)
 		if err != nil {
 			return fmt.Errorf("failed to unmarshal system setting additional script value")
 		}
 	} else if upsert.Name == SystemSettingCustomizedProfileName {
-		value := CustomizedProfile{
+		customizedProfile := CustomizedProfile{
 			Name:        "memos",
 			IconURL:     "",
+			Description: "",
+			Locale:      "en",
+			Appearance:  "system",
 			ExternalURL: "",
 		}
-		err := json.Unmarshal([]byte(upsert.Value), &value)
+		err := json.Unmarshal([]byte(upsert.Value), &customizedProfile)
 		if err != nil {
 			return fmt.Errorf("failed to unmarshal system setting customized profile value")
 		}
+		if !slices.Contains(UserSettingLocaleValue, customizedProfile.Locale) {
+			return fmt.Errorf("invalid locale value")
+		}
+		if !slices.Contains(UserSettingAppearanceValue, customizedProfile.Appearance) {
+			return fmt.Errorf("invalid appearance value")
+		}
 	} else {
 		return fmt.Errorf("invalid system setting name")
 	}
 
 	return nil
diff --git a/api/user_setting.go b/api/user_setting.go
index 23001f0..89ee852 100644
--- a/api/user_setting.go
+++ b/api/user_setting.go
@@ -1,10 +1,12 @@
 package api
 
 import (
 	"encoding/json"
 	"fmt"
+
+	"golang.org/x/exp/slices"
 )
 
 type UserSettingKey string
 
 const (
@@ -58,70 +60,38 @@ func (upsert UserSettingUpsert) Validate() error {
 		localeValue := "en"
 		err := json.Unmarshal([]byte(upsert.Value), &localeValue)
 		if err != nil {
 			return fmt.Errorf("failed to unmarshal user setting locale value")
 		}
-
-		invalid := true
-		for _, value := range UserSettingLocaleValue {
-			if localeValue == value {
-				invalid = false
-				break
-			}
-		}
-		if invalid {
+		if !slices.Contains(UserSettingLocaleValue, localeValue) {
 			return fmt.Errorf("invalid user setting locale value")
 		}
 	} else if upsert.Key == UserSettingAppearanceKey {
-		appearanceValue := "light"
+		appearanceValue := "system"
 		err := json.Unmarshal([]byte(upsert.Value), &appearanceValue)
 		if err != nil {
 			return fmt.Errorf("failed to unmarshal user setting appearance value")
 		}
-
-		invalid := true
-		for _, value := range UserSettingAppearanceValue {
-			if appearanceValue == value {
-				invalid = false
-				break
-			}
-		}
-		if invalid {
+		if !slices.Contains(UserSettingAppearanceValue, appearanceValue) {
 			return fmt.Errorf("invalid user setting appearance value")
 		}
 	} else if upsert.Key == UserSettingMemoVisibilityKey {
 		memoVisibilityValue := Private
 		err := json.Unmarshal([]byte(upsert.Value), &memoVisibilityValue)
 		if err != nil {
 			return fmt.Errorf("failed to unmarshal user setting memo visibility value")
 		}
-
-		invalid := true
-		for _, value := range UserSettingMemoVisibilityValue {
-			if memoVisibilityValue == value {
-				invalid = false
-				break
-			}
-		}
-		if invalid {
+		if !slices.Contains(UserSettingMemoVisibilityValue, memoVisibilityValue) {
 			return fmt.Errorf("invalid user setting memo visibility value")
 		}
 	} else if upsert.Key == UserSettingMemoDisplayTsOptionKey {
 		memoDisplayTsOption := "created_ts"
 		err := json.Unmarshal([]byte(upsert.Value), &memoDisplayTsOption)
 		if err != nil {
 			return fmt.Errorf("failed to unmarshal user setting memo display ts option")
 		}
-
-		invalid := true
-		for _, value := range UserSettingMemoDisplayTsOptionKeyValue {
-			if memoDisplayTsOption == value {
-				invalid = false
-				break
-			}
-		}
-		if invalid {
+		if !slices.Contains(UserSettingMemoDisplayTsOptionKeyValue, memoDisplayTsOption) {
 			return fmt.Errorf("invalid user setting memo display ts option value")
 		}
 	} else {
 		return fmt.Errorf("invalid user setting key")
 	}
diff --git a/go.mod b/go.mod
index 5085d86..9e2b69a 100644
--- a/go.mod
+++ b/go.mod
@@ -36,13 +36,16 @@ require (
 	github.com/pmezard/go-difflib v1.0.0 // indirect
 	github.com/segmentio/backo-go v1.0.1 // indirect
 	github.com/valyala/bytebufferpool v1.0.0 // indirect
 	github.com/valyala/fasttemplate v1.2.1 // indirect
 	github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c // indirect
-	golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10 // indirect
+	golang.org/x/sys v0.1.0 // indirect
 	golang.org/x/text v0.3.7 // indirect
 	golang.org/x/time v0.0.0-20220722155302-e5dcc9cfc0b9 // indirect
 	gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
 )
 
-require github.com/segmentio/analytics-go v3.1.0+incompatible
+require (
+	github.com/segmentio/analytics-go v3.1.0+incompatible
+	golang.org/x/exp v0.0.0-20221217163422-3c43f8badb15
+)
diff --git a/go.sum b/go.sum
index e03d5d8..3c318af 100644
--- a/go.sum
+++ b/go.sum
@@ -68,18 +68,20 @@ github.com/valyala/fasttemplate v1.2.1 h1:TVEnxayobAdVkhQfrfes2IzOB6o+z4roRkPF52
 github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
 github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c h1:3lbZUMbMiGUW/LMkfsEABsc5zNT9+b1CvsJx47JzJ8g=
 github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c/go.mod h1:UrdRz5enIKZ63MEE3IF9l2/ebyx59GyGgPi+tICQdmM=
 golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa h1:zuSxTR4o9y82ebqCUJYNGJbGPo6sKVl54f/TVDObg1c=
 golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
+golang.org/x/exp v0.0.0-20221217163422-3c43f8badb15 h1:5oN1Pz/eDhCpbMbLstvIPa0b/BEQo6g6nwV3pLjfM6w=
+golang.org/x/exp v0.0.0-20221217163422-3c43f8badb15/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
 golang.org/x/net v0.0.0-20220728030405-41545e8bf201 h1:bvOltf3SADAfG05iRml8lAB3qjoEX5RCyN4K6G5v3N0=
 golang.org/x/net v0.0.0-20220728030405-41545e8bf201/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
 golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220405052023-b1e9470b6e64/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10 h1:WIoqL4EROvwiPdUtaip4VcDdpZ4kha7wBWZrbVKCIZg=
-golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U=
+golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
 golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
 golang.org/x/time v0.0.0-20220722155302-e5dcc9cfc0b9 h1:ftMN5LMiBFjbzleLqtoBZk7KdJwhuybIU+FckUHgoyQ=
 golang.org/x/time v0.0.0-20220722155302-e5dcc9cfc0b9/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
diff --git a/server/acl.go b/server/acl.go
index ccb67e8..c2e3399 100644
--- a/server/acl.go
+++ b/server/acl.go
@@ -25,10 +25,11 @@ func setUserSession(ctx echo.Context, user *api.User) error {
 	sess, _ := session.Get("memos_session", ctx)
 	sess.Options = &sessions.Options{
 		Path:     "/",
 		MaxAge:   3600 * 24 * 30,
 		HttpOnly: true,
+		Secure:   true,
 	}
 	sess.Values[userIDContextKey] = user.ID
 	err := sess.Save(ctx.Request(), ctx.Response())
 	if err != nil {
 		return fmt.Errorf("failed to set session, err: %w", err)
diff --git a/server/system.go b/server/system.go
index c485e93..e3a2811 100644
--- a/server/system.go
+++ b/server/system.go
@@ -45,11 +45,16 @@ func (s *Server) registerSystemRoutes(g *echo.Group) {
 			DBSize:           0,
 			AllowSignUp:      false,
 			AdditionalStyle:  "",
 			AdditionalScript: "",
 			CustomizedProfile: api.CustomizedProfile{
-				Name: "memos",
+				Name:        "memos",
+				IconURL:     "",
+				Description: "",
+				Locale:      "en",
+				Appearance:  "system",
+				ExternalURL: "",
 			},
 		}
 
 		systemSettingList, err := s.Store.FindSystemSettingList(ctx, &api.SystemSettingFind{})
 		if err != nil {
@@ -71,10 +76,13 @@ func (s *Server) registerSystemRoutes(g *echo.Group) {
 			} else if systemSetting.Name == api.SystemSettingCustomizedProfileName {
 				valueMap := value.(map[string]interface{})
 				systemStatus.CustomizedProfile = api.CustomizedProfile{
 					Name:        valueMap["name"].(string),
 					IconURL:     valueMap["iconUrl"].(string),
+					Description: valueMap["description"].(string),
+					Locale:      valueMap["locale"].(string),
+					Appearance:  valueMap["appearance"].(string),
 					ExternalURL: valueMap["externalUrl"].(string),
 				}
 			}
 		}
 
diff --git a/web/src/components/AppearanceSelect.tsx b/web/src/components/AppearanceSelect.tsx
index 69a0094..2cddf1f 100644
--- a/web/src/components/AppearanceSelect.tsx
+++ b/web/src/components/AppearanceSelect.tsx
@@ -1,18 +1,21 @@
 import { Option, Select } from "@mui/joy";
+import { FC } from "react";
 import { useTranslation } from "react-i18next";
-import { useGlobalStore, useUserStore } from "../store/module";
 import Icon from "./Icon";
 
+interface Props {
+  value: Appearance;
+  onChange: (appearance: Appearance) => void;
+  className?: string;
+}
+
 const appearanceList = ["system", "light", "dark"];
 
-const AppearanceSelect = () => {
+const AppearanceSelect: FC<Props> = (props: Props) => {
+  const { onChange, value, className } = props;
   const { t } = useTranslation();
-  const globalStore = useGlobalStore();
-  const userStore = useUserStore();
-  const { appearance } = globalStore.state;
-  const user = userStore.state.user;
 
   const getPrefixIcon = (apperance: Appearance) => {
     const className = "w-4 h-auto";
     if (apperance === "light") {
       return <Icon.Sun className={className} />;
@@ -22,26 +25,23 @@ const AppearanceSelect = () => {
       return <Icon.Smile className={className} />;
     }
   };
 
   const handleSelectChange = async (appearance: Appearance) => {
-    if (user) {
-      await userStore.upsertUserSetting("appearance", appearance);
-    }
-    globalStore.setAppearance(appearance);
+    onChange(appearance);
   };
 
   return (
     <Select
-      className="!min-w-[10rem] w-auto text-sm"
-      value={appearance}
+      className={`!min-w-[10rem] w-auto whitespace-nowrap ${className ?? ""}`}
+      value={value}
       onChange={(_, appearance) => {
         if (appearance) {
           handleSelectChange(appearance);
         }
       }}
-      startDecorator={getPrefixIcon(appearance)}
+      startDecorator={getPrefixIcon(value)}
     >
       {appearanceList.map((item) => (
         <Option key={item} value={item} className="whitespace-nowrap">
           {t(`setting.apperance-option.${item}`)}
         </Option>
diff --git a/web/src/components/LocaleSelect.tsx b/web/src/components/LocaleSelect.tsx
new file mode 100644
index 0000000..790ee64
--- /dev/null
+++ b/web/src/components/LocaleSelect.tsx
@@ -0,0 +1,37 @@
+import { Option, Select } from "@mui/joy";
+import { FC } from "react";
+import Icon from "./Icon";
+
+interface Props {
+  value: Locale;
+  onChange: (locale: Locale) => void;
+  className?: string;
+}
+
+const LocaleSelect: FC<Props> = (props: Props) => {
+  const { onChange, value, className } = props;
+
+  const handleSelectChange = async (locale: Locale) => {
+    onChange(locale);
+  };
+
+  return (
+    <Select
+      className={`!min-w-[10rem] w-auto whitespace-nowrap ${className ?? ""}`}
+      startDecorator={<Icon.Globe className="w-4 h-auto" />}
+      value={value}
+      onChange={(_, value) => handleSelectChange(value as Locale)}
+    >
+      <Option value="en">English</Option>
+      <Option value="zh">中文</Option>
+      <Option value="vi">Tiếng Việt</Option>
+      <Option value="fr">French</Option>
+      <Option value="nl">Nederlands</Option>
+      <Option value="sv">Svenska</Option>
+      <Option value="de">German</Option>
+      <Option value="es">Español</Option>
+    </Select>
+  );
+};
+
+export default LocaleSelect;
diff --git a/web/src/components/Settings/PreferencesSection.tsx b/web/src/components/Settings/PreferencesSection.tsx
index 48cfbe7..bb67820 100644
--- a/web/src/components/Settings/PreferencesSection.tsx
+++ b/web/src/components/Settings/PreferencesSection.tsx
@@ -1,52 +1,18 @@
 import { Select, Switch, Option } from "@mui/joy";
 import { useTranslation } from "react-i18next";
 import { useGlobalStore, useUserStore } from "../../store/module";
 import { VISIBILITY_SELECTOR_ITEMS, MEMO_DISPLAY_TS_OPTION_SELECTOR_ITEMS } from "../../helpers/consts";
-import Icon from "../Icon";
 import AppearanceSelect from "../AppearanceSelect";
+import LocaleSelect from "../LocaleSelect";
 import "../../less/settings/preferences-section.less";
 
-const localeSelectorItems = [
-  {
-    text: "English",
-    value: "en",
-  },
-  {
-    text: "中文",
-    value: "zh",
-  },
-  {
-    text: "Tiếng Việt",
-    value: "vi",
-  },
-  {
-    text: "French",
-    value: "fr",
-  },
-  {
-    text: "Nederlands",
-    value: "nl",
-  },
-  {
-    text: "Svenska",
-    value: "sv",
-  },
-  {
-    text: "German",
-    value: "de",
-  },
-  {
-    text: "Español",
-    value: "es",
-  },
-];
-
 const PreferencesSection = () => {
   const { t } = useTranslation();
   const globalStore = useGlobalStore();
   const userStore = useUserStore();
+  const { appearance, locale } = globalStore.state;
   const { setting, localSetting } = userStore.state.user as User;
   const visibilitySelectorItems = VISIBILITY_SELECTOR_ITEMS.map((item) => {
     return {
       value: item.value,
       text: t(`memo.visibility.${item.text.toLowerCase()}`),
@@ -58,13 +24,18 @@ const PreferencesSection = () => {
       value: item.value,
       text: t(`setting.preference-section.${item.value}`),
     };
   });
 
-  const handleLocaleChanged = async (value: string) => {
-    await userStore.upsertUserSetting("locale", value);
-    globalStore.setLocale(value as Locale);
+  const handleLocaleSelectChange = async (locale: Locale) => {
+    await userStore.upsertUserSetting("locale", locale);
+    globalStore.setLocale(locale);
+  };
+
+  const handleAppearanceSelectChange = async (appearance: Appearance) => {
+    await userStore.upsertUserSetting("appearance", appearance);
+    globalStore.setAppearance(appearance);
   };
 
   const handleDefaultMemoVisibilityChanged = async (value: string) => {
     await userStore.upsertUserSetting("memoVisibility", value);
   };
@@ -80,30 +51,15 @@ const PreferencesSection = () => {
   return (
     <div className="section-container preferences-section-container">
       <p className="title-text">{t("common.basic")}</p>
       <div className="form-label selector">
         <span className="normal-text">{t("common.language")}</span>
-        <Select
-          className="!min-w-[10rem] w-auto text-sm"
-          value={setting.locale}
-          onChange={(_, locale) => {
-            if (locale) {
-              handleLocaleChanged(locale);
-            }
-          }}
-          startDecorator={<Icon.Globe className="w-4 h-auto" />}
-        >
-          {localeSelectorItems.map((item) => (
-            <Option key={item.value} value={item.value} className="whitespace-nowrap">
-              {item.text}
-            </Option>
-          ))}
-        </Select>
+        <LocaleSelect value={locale} onChange={handleLocaleSelectChange} />
       </div>
       <div className="form-label selector">
         <span className="normal-text">{t("setting.preference-section.theme")}</span>
-        <AppearanceSelect />
+        <AppearanceSelect value={appearance} onChange={handleAppearanceSelectChange} />
       </div>
       <p className="title-text">{t("setting.preference")}</p>
       <div className="form-label selector">
         <span className="normal-text">{t("setting.preference-section.default-memo-visibility")}</span>
         <Select
diff --git a/web/src/components/UpdateCustomizedProfileDialog.tsx b/web/src/components/UpdateCustomizedProfileDialog.tsx
index b189df4..8b30bf3 100644
--- a/web/src/components/UpdateCustomizedProfileDialog.tsx
+++ b/web/src/components/UpdateCustomizedProfileDialog.tsx
@@ -3,10 +3,12 @@ import { useTranslation } from "react-i18next";
 import { useGlobalStore } from "../store/module";
 import * as api from "../helpers/api";
 import Icon from "./Icon";
 import { generateDialog } from "./Dialog";
 import toastHelper from "./Toast";
+import LocaleSelect from "./LocaleSelect";
+import AppearanceSelect from "./AppearanceSelect";
 
 type Props = DialogProps;
 
 const UpdateCustomizedProfileDialog: React.FC<Props> = ({ destroy }: Props) => {
   const { t } = useTranslation();
@@ -37,10 +39,37 @@ const UpdateCustomizedProfileDialog: React.FC<Props> = ({ destroy }: Props) => {
         iconUrl: e.target.value as string,
       };
     });
   };
 
+  const handleDescriptionChanged = (e: React.ChangeEvent<HTMLInputElement>) => {
+    setState((state) => {
+      return {
+        ...state,
+        description: e.target.value as string,
+      };
+    });
+  };
+
+  const handleLocaleSelectChange = (locale: Locale) => {
+    setState((state) => {
+      return {
+        ...state,
+        locale: locale,
+      };
+    });
+  };
+
+  const handleAppearanceSelectChange = (appearance: Appearance) => {
+    setState((state) => {
+      return {
+        ...state,
+        appearance: appearance,
+      };
+    });
+  };
+
   const handleSaveBtnClick = async () => {
     if (state.name === "" || state.iconUrl === "") {
       toastHelper.error(t("message.fill-all"));
       return;
     }
@@ -59,24 +88,30 @@ const UpdateCustomizedProfileDialog: React.FC<Props> = ({ destroy }: Props) => {
     destroy();
   };
 
   return (
     <>
-      <div className="dialog-header-container !w-64">
+      <div className="dialog-header-container">
         <p className="title-text">{t("setting.system-section.customize-server.title")}</p>
         <button className="btn close-btn" onClick={handleCloseBtnClick}>
           <Icon.X />
         </button>
       </div>
-      <div className="dialog-content-container">
+      <div className="dialog-content-container !w-80">
         <p className="text-sm mb-1">
           {t("setting.system-section.server-name")}
           <span className="text-sm text-gray-400 ml-1">({t("setting.system-section.customize-server.default")})</span>
         </p>
         <input type="text" className="input-text" value={state.name} onChange={handleNameChanged} />
         <p className="text-sm mb-1 mt-2">{t("setting.system-section.customize-server.icon-url")}</p>
         <input type="text" className="input-text" value={state.iconUrl} onChange={handleIconUrlChanged} />
+        <p className="text-sm mb-1 mt-2">Description</p>
+        <input type="text" className="input-text" value={state.description} onChange={handleDescriptionChanged} />
+        <p className="text-sm mb-1 mt-2">Server locale</p>
+        <LocaleSelect className="w-full" value={state.locale} onChange={handleLocaleSelectChange} />
+        <p className="text-sm mb-1 mt-2">Server appearance</p>
+        <AppearanceSelect className="w-full" value={state.appearance} onChange={handleAppearanceSelectChange} />
         <div className="mt-4 w-full flex flex-row justify-end items-center space-x-2">
           <span className="btn-text" onClick={handleCloseBtnClick}>
             {t("common.cancel")}
           </span>
           <span className="btn-primary" onClick={handleSaveBtnClick}>
diff --git a/web/src/pages/Auth.tsx b/web/src/pages/Auth.tsx
index d79f9d2..10842f1 100644
--- a/web/src/pages/Auth.tsx
+++ b/web/src/pages/Auth.tsx
@@ -1,32 +1,32 @@
-import { Option, Select } from "@mui/joy";
 import { useEffect, useState } from "react";
 import { useTranslation } from "react-i18next";
 import { useNavigate } from "react-router-dom";
 import { useGlobalStore, useUserStore } from "../store/module";
 import * as api from "../helpers/api";
 import { validate, ValidatorConfig } from "../helpers/validator";
 import useLoading from "../hooks/useLoading";
 import Icon from "../components/Icon";
 import toastHelper from "../components/Toast";
 import AppearanceSelect from "../components/AppearanceSelect";
+import LocaleSelect from "../components/LocaleSelect";
 import "../less/auth.less";
 
 const validateConfig: ValidatorConfig = {
   minLength: 4,
   maxLength: 320,
   noSpace: true,
   noChinese: true,
 };
 
 const Auth = () => {
-  const { t, i18n } = useTranslation();
+  const { t } = useTranslation();
   const navigate = useNavigate();
   const globalStore = useGlobalStore();
   const userStore = useUserStore();
   const actionBtnLoadingState = useLoading(false);
-  const systemStatus = globalStore.state.systemStatus;
+  const { appearance, locale, systemStatus } = globalStore.state;
   const mode = systemStatus.profile.mode;
   const [username, setUsername] = useState(mode === "dev" ? "demohero" : "");
   const [password, setPassword] = useState(mode === "dev" ? "secret" : "");
 
   useEffect(() => {
@@ -41,10 +41,18 @@ const Auth = () => {
   const handlePasswordInputChanged = (e: React.ChangeEvent<HTMLInputElement>) => {
     const text = e.target.value as string;
     setPassword(text);
   };
 
+  const handleLocaleSelectChange = (locale: Locale) => {
+    globalStore.setLocale(locale);
+  };
+
+  const handleAppearanceSelectChange = (appearance: Appearance) => {
+    globalStore.setAppearance(appearance);
+  };
+
   const handleSigninBtnsClick = async () => {
     if (actionBtnLoadingState.isLoading) {
       return;
     }
 
@@ -107,24 +115,20 @@ const Auth = () => {
       toastHelper.error(error.response.data.error);
     }
     actionBtnLoadingState.setFinish();
   };
 
-  const handleLocaleItemClick = (locale: Locale) => {
-    globalStore.setLocale(locale);
-  };
-
   return (
     <div className="page-wrapper auth">
       <div className="page-container">
         <div className="auth-form-wrapper">
           <div className="page-header-container">
             <div className="title-container">
               <img className="logo-img" src={systemStatus.customizedProfile.iconUrl} alt="" />
               <p className="logo-text">{systemStatus.customizedProfile.name}</p>
             </div>
-            <p className="slogan-text">{t("slogan")}</p>
+            <p className="slogan-text">{systemStatus.customizedProfile.description || t("slogan")}</p>
           </div>
           <div className={`page-content-container ${actionBtnLoadingState.isLoading ? "requesting" : ""}`}>
             <div className="form-item-container input-form-container">
               <span className={`normal-text ${username ? "not-null" : ""}`}>{t("common.username")}</span>
               <input className="input-text" type="text" value={username} onChange={handleUsernameInputChanged} required />
@@ -165,26 +169,12 @@ const Auth = () => {
             )}
           </div>
           {!systemStatus?.host && <p className="tip-text">{t("auth.host-tip")}</p>}
         </div>
         <div className="flex flex-row items-center justify-center w-full gap-2">
-          <Select
-            className="!min-w-[9rem] w-auto whitespace-nowrap"
-            startDecorator={<Icon.Globe className="w-4 h-auto" />}
-            value={i18n.language}
-            onChange={(_, value) => handleLocaleItemClick(value as Locale)}
-          >
-            <Option value="en">English</Option>
-            <Option value="zh">中文</Option>
-            <Option value="vi">Tiếng Việt</Option>
-            <Option value="fr">French</Option>
-            <Option value="nl">Nederlands</Option>
-            <Option value="sv">Svenska</Option>
-            <Option value="de">German</Option>
-            <Option value="es">Español</Option>
-          </Select>
-          <AppearanceSelect />
+          <LocaleSelect value={locale} onChange={handleLocaleSelectChange} />
+          <AppearanceSelect value={appearance} onChange={handleAppearanceSelectChange} />
         </div>
       </div>
     </div>
   );
 };
diff --git a/web/src/store/module/global.ts b/web/src/store/module/global.ts
index 14c3b8f..687cc9e 100644
--- a/web/src/store/module/global.ts
+++ b/web/src/store/module/global.ts
@@ -11,11 +11,14 @@ export const initialGlobalState = async () => {
       allowSignUp: false,
       additionalStyle: "",
       additionalScript: "",
       customizedProfile: {
         name: "memos",
-        iconUrl: "/logo.webp",
+        iconUrl: "https://usememos.com/logo.webp",
+        description: "",
+        locale: "en",
+        appearance: "system",
         externalUrl: "",
       },
     } as SystemStatus,
   };
 
@@ -29,10 +32,12 @@ export const initialGlobalState = async () => {
 
   try {
     const { data } = (await api.getSystemStatus()).data;
     if (data) {
       defaultGlobalState.systemStatus = data;
+      defaultGlobalState.locale = data.customizedProfile.locale;
+      defaultGlobalState.appearance = data.customizedProfile.appearance;
     }
   } catch (error) {
     // do nth
   }
 
diff --git a/web/src/store/reducer/global.ts b/web/src/store/reducer/global.ts
index 9de1f80..089dca2 100644
--- a/web/src/store/reducer/global.ts
+++ b/web/src/store/reducer/global.ts
@@ -21,11 +21,14 @@ const globalSlice = createSlice({
       allowSignUp: false,
       additionalStyle: "",
       additionalScript: "",
       customizedProfile: {
         name: "memos",
-        iconUrl: "/logo.webp",
+        iconUrl: "https://usememos.com/logo.webp",
+        description: "",
+        locale: "en",
+        appearance: "system",
         externalUrl: "",
       },
     },
   } as State,
   reducers: {
diff --git a/web/src/types/modules/system.d.ts b/web/src/types/modules/system.d.ts
index 3187777..aad5bb7 100644
--- a/web/src/types/modules/system.d.ts
+++ b/web/src/types/modules/system.d.ts
@@ -4,10 +4,13 @@ interface Profile {
 }
 
 interface CustomizedProfile {
   name: string;
   iconUrl: string;
+  description: string;
+  locale: Locale;
+  appearance: Appearance;
   externalUrl: string;
 }
 
 interface SystemStatus {
   host?: User;

Vulnerable code: 		value := CustomizedProfile{
		err := json.Unmarshal([]byte(upsert.Value), &value)

		invalid := true
		for _, value := range UserSettingLocaleValue {
			if localeValue == value {
				invalid = false
				break
			}
		}
		if invalid {
		appearanceValue := "light"

		invalid := true
		for _, value := range UserSettingAppearanceValue {
			if appearanceValue == value {
				invalid = false
				break
			}
		}
		if invalid {

		invalid := true
		for _, value := range UserSettingMemoVisibilityValue {
			if memoVisibilityValue == value {
				invalid = false
				break
			}
		}
		if invalid {

		invalid := true
		for _, value := range UserSettingMemoDisplayTsOptionKeyValue {
			if memoDisplayTsOption == value {
				invalid = false
				break
			}
		}
		if invalid {
	golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10 // indirect
require github.com/segmentio/analytics-go v3.1.0+incompatible
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10 h1:WIoqL4EROvwiPdUtaip4VcDdpZ4kha7wBWZrbVKCIZg=
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
				Name: "memos",
import { useGlobalStore, useUserStore } from "../store/module";
const AppearanceSelect = () => {
  const globalStore = useGlobalStore();
  const userStore = useUserStore();
  const { appearance } = globalStore.state;
  const user = userStore.state.user;
    if (user) {
      await userStore.upsertUserSetting("appearance", appearance);
    }
    globalStore.setAppearance(appearance);
      className="!min-w-[10rem] w-auto text-sm"
      value={appearance}
      startDecorator={getPrefixIcon(appearance)}
import Icon from "../Icon";
const localeSelectorItems = [
  {
    text: "English",
    value: "en",
  },
  {
    text: "中文",
    value: "zh",
  },
  {
    text: "Tiếng Việt",
    value: "vi",
  },
  {
    text: "French",
    value: "fr",
  },
  {
    text: "Nederlands",
    value: "nl",
  },
  {
    text: "Svenska",
    value: "sv",
  },
  {
    text: "German",
    value: "de",
  },
  {
    text: "Español",
    value: "es",
  },
];

  const handleLocaleChanged = async (value: string) => {
    await userStore.upsertUserSetting("locale", value);
    globalStore.setLocale(value as Locale);
        <Select
          className="!min-w-[10rem] w-auto text-sm"
          value={setting.locale}
          onChange={(_, locale) => {
            if (locale) {
              handleLocaleChanged(locale);
            }
          }}
          startDecorator={<Icon.Globe className="w-4 h-auto" />}
        >
          {localeSelectorItems.map((item) => (
            <Option key={item.value} value={item.value} className="whitespace-nowrap">
              {item.text}
            </Option>
          ))}
        </Select>
        <AppearanceSelect />
      <div className="dialog-header-container !w-64">
      <div className="dialog-content-container">
import { Option, Select } from "@mui/joy";
  const { t, i18n } = useTranslation();
  const systemStatus = globalStore.state.systemStatus;
  const handleLocaleItemClick = (locale: Locale) => {
    globalStore.setLocale(locale);
  };

            <p className="slogan-text">{t("slogan")}</p>
          <Select
            className="!min-w-[9rem] w-auto whitespace-nowrap"
            startDecorator={<Icon.Globe className="w-4 h-auto" />}
            value={i18n.language}
            onChange={(_, value) => handleLocaleItemClick(value as Locale)}
          >
            <Option value="en">English</Option>
            <Option value="zh">中文</Option>
            <Option value="vi">Tiếng Việt</Option>
            <Option value="fr">French</Option>
            <Option value="nl">Nederlands</Option>
            <Option value="sv">Svenska</Option>
            <Option value="de">German</Option>
            <Option value="es">Español</Option>
          </Select>
          <AppearanceSelect />
        iconUrl: "/logo.webp",
        iconUrl: "/logo.webp",
Fixed code: 
	"golang.org/x/exp/slices"
	// Description is the server description.
	Description string `json:"description"`
	// Locale is the server default locale.
	Locale string `json:"locale"`
	// Appearance is the server default appearance.
	Appearance string `json:"appearance"`
		customizedProfile := CustomizedProfile{
			Description: "",
			Locale:      "en",
			Appearance:  "system",
		err := json.Unmarshal([]byte(upsert.Value), &customizedProfile)
		if !slices.Contains(UserSettingLocaleValue, customizedProfile.Locale) {
			return fmt.Errorf("invalid locale value")
		}
		if !slices.Contains(UserSettingAppearanceValue, customizedProfile.Appearance) {
			return fmt.Errorf("invalid appearance value")
		}

	"golang.org/x/exp/slices"
		if !slices.Contains(UserSettingLocaleValue, localeValue) {
		appearanceValue := "system"
		if !slices.Contains(UserSettingAppearanceValue, appearanceValue) {
		if !slices.Contains(UserSettingMemoVisibilityValue, memoVisibilityValue) {
		if !slices.Contains(UserSettingMemoDisplayTsOptionKeyValue, memoDisplayTsOption) {
	golang.org/x/sys v0.1.0 // indirect
require (
	github.com/segmentio/analytics-go v3.1.0+incompatible
	golang.org/x/exp v0.0.0-20221217163422-3c43f8badb15
)
golang.org/x/exp v0.0.0-20221217163422-3c43f8badb15 h1:5oN1Pz/eDhCpbMbLstvIPa0b/BEQo6g6nwV3pLjfM6w=
golang.org/x/exp v0.0.0-20221217163422-3c43f8badb15/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
		Secure:   true,
				Name:        "memos",
				IconURL:     "",
				Description: "",
				Locale:      "en",
				Appearance:  "system",
				ExternalURL: "",
					Description: valueMap["description"].(string),
					Locale:      valueMap["locale"].(string),
					Appearance:  valueMap["appearance"].(string),
import { FC } from "react";
interface Props {
  value: Appearance;
  onChange: (appearance: Appearance) => void;
  className?: string;
}

const AppearanceSelect: FC<Props> = (props: Props) => {
  const { onChange, value, className } = props;
    onChange(appearance);
      className={`!min-w-[10rem] w-auto whitespace-nowrap ${className ?? ""}`}
      value={value}
      startDecorator={getPrefixIcon(value)}
import { Option, Select } from "@mui/joy";
import { FC } from "react";
import Icon from "./Icon";

interface Props {
  value: Locale;
  onChange: (locale: Locale) => void;
  className?: string;
}

const LocaleSelect: FC<Props> = (props: Props) => {
  const { onChange, value, className } = props;

  const handleSelectChange = async (locale: Locale) => {
    onChange(locale);
  };

  return (
    <Select
      className={`!min-w-[10rem] w-auto whitespace-nowrap ${className ?? ""}`}
      startDecorator={<Icon.Globe className="w-4 h-auto" />}
      value={value}
      onChange={(_, value) => handleSelectChange(value as Locale)}
    >
      <Option value="en">English</Option>
      <Option value="zh">中文</Option>
      <Option value="vi">Tiếng Việt</Option>
      <Option value="fr">French</Option>
      <Option value="nl">Nederlands</Option>
      <Option value="sv">Svenska</Option>
      <Option value="de">German</Option>
      <Option value="es">Español</Option>
    </Select>
  );
};

export default LocaleSelect;
import LocaleSelect from "../LocaleSelect";
  const { appearance, locale } = globalStore.state;
  const handleLocaleSelectChange = async (locale: Locale) => {
    await userStore.upsertUserSetting("locale", locale);
    globalStore.setLocale(locale);
  };

  const handleAppearanceSelectChange = async (appearance: Appearance) => {
    await userStore.upsertUserSetting("appearance", appearance);
    globalStore.setAppearance(appearance);
        <LocaleSelect value={locale} onChange={handleLocaleSelectChange} />
        <AppearanceSelect value={appearance} onChange={handleAppearanceSelectChange} />
import LocaleSelect from "./LocaleSelect";
import AppearanceSelect from "./AppearanceSelect";
  const handleDescriptionChanged = (e: React.ChangeEvent<HTMLInputElement>) => {
    setState((state) => {
      return {
        ...state,
        description: e.target.value as string,
      };
    });
  };

  const handleLocaleSelectChange = (locale: Locale) => {
    setState((state) => {
      return {
        ...state,
        locale: locale,
      };
    });
  };

  const handleAppearanceSelectChange = (appearance: Appearance) => {
    setState((state) => {
      return {
        ...state,
        appearance: appearance,
      };
    });
  };

      <div className="dialog-header-container">
      <div className="dialog-content-container !w-80">
        <p className="text-sm mb-1 mt-2">Description</p>
        <input type="text" className="input-text" value={state.description} onChange={handleDescriptionChanged} />
        <p className="text-sm mb-1 mt-2">Server locale</p>
        <LocaleSelect className="w-full" value={state.locale} onChange={handleLocaleSelectChange} />
        <p className="text-sm mb-1 mt-2">Server appearance</p>
        <AppearanceSelect className="w-full" value={state.appearance} onChange={handleAppearanceSelectChange} />
import LocaleSelect from "../components/LocaleSelect";
  const { t } = useTranslation();
  const { appearance, locale, systemStatus } = globalStore.state;
  const handleLocaleSelectChange = (locale: Locale) => {
    globalStore.setLocale(locale);
  };

  const handleAppearanceSelectChange = (appearance: Appearance) => {
    globalStore.setAppearance(appearance);
  };

            <p className="slogan-text">{systemStatus.customizedProfile.description || t("slogan")}</p>
          <LocaleSelect value={locale} onChange={handleLocaleSelectChange} />
          <AppearanceSelect value={appearance} onChange={handleAppearanceSelectChange} />
        iconUrl: "https://usememos.com/logo.webp",
        description: "",
        locale: "en",
        appearance: "system",
      defaultGlobalState.locale = data.customizedProfile.locale;
      defaultGlobalState.appearance = data.customizedProfile.appearance;
        iconUrl: "https://usememos.com/logo.webp",
        description: "",
        locale: "en",
        appearance: "system",
  description: string;
  locale: Locale;
  appearance: Appearance;