CVE-2023-40580 (published 2023-08-25T20:15Z)

Freighter is a Stellar chrome extension. It may be possible for a malicious website to access the recovery mnemonic phrase when the Freighter wallet is unlocked. This vulnerability impacts access control to the mnemonic recovery phrase. This issue was patched in version 5.3.1.

CVSS v3 base score: 6.5, MEDIUM (CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:N/A:N)

Weakness information:
cwe_id: NVD-CWE-noinfo
cwe_name: Insufficient Information
description: There is insufficient information about the issue to classify it; details are unkown or unspecified.
extended_description: Insufficient Information

Fix information:
repo_url: https://github.com/stellar/freighter
commit_details: repo_url: https://github.com/stellar/freighter
author: Piyal Basu
author_date: 2023-08-23 12:10:20-04:00
msg: Prevent Javascript from accessing the mnemonic phrase through a private API (#948)
repository: repo_url: https://github.com/stellar/freighter
repo_name: stellar/freighter
description: Stellar chrome extension
repo_language: TypeScript
owner: stellar
file_changes: filename: internal.ts
change_type: ModificationType.MODIFY
diff: @@ -670,8 +670,8 @@ export const signOut = async (): Promise<{
 
 export const showBackupPhrase = async (
   password: string,
-): Promise<{ error: string }> => {
-  let response = { error: "" };
+): Promise<{ mnemonicPhrase: string; error: string }> => {
+  let response = { mnemonicPhrase: "", error: "" };
   try {
     response = await sendMessageToBackground({
       password,

programming_language: TypeScript
filename: package.json
change_type: ModificationType.MODIFY
diff: @@ -1,6 +1,6 @@
 {
   "name": "extension",
-  "version": "5.3.0",
+  "version": "5.3.1",
   "license": "Apache-2.0",
   "prettier": "@stellar/prettier-config",
   "scripts": {

programming_language: JavaScript
filename: v2.json
change_type: ModificationType.MODIFY
diff: @@ -1,7 +1,7 @@
 {
   "name": "Freighter",
-  "version": "5.3.0",
-  "version_name": "5.3.0",
+  "version": "5.3.1",
+  "version_name": "5.3.1",
   "description": "Freighter is a non-custodial wallet extension that enables you to sign Stellar transactions via your browser.",
   "browser_specific_settings": {
     "gecko": {

programming_language: JavaScript
filename: v3.json
change_type: ModificationType.MODIFY
diff: @@ -1,7 +1,7 @@
 {
   "name": "Freighter",
-  "version": "5.3.0",
-  "version_name": "5.3.0",
+  "version": "5.3.1",
+  "version_name": "5.3.1",
   "description": "Freighter is a non-custodial wallet extension that enables you to sign Stellar transactions via your browser.",
   "background": {
     "service_worker": "background.min.js"

programming_language: JavaScript
filename: popupMessageListener.ts
change_type: ModificationType.MODIFY
diff: @@ -583,9 +583,23 @@ export const popupMessageListener = (request: Request, sessionStore: Store) => {
     };
   };
 
-  const getMnemonicPhrase = () => ({
-    mnemonicPhrase: mnemonicPhraseSelector(sessionStore.getState()),
-  });
+  const getMnemonicPhrase = async () => {
+    const { password } = request;
+
+    const keyID = (await getIsHardwareWalletActive())
+      ? await _getNonHwKeyID()
+      : (await localStore.getItem(KEY_ID)) || "";
+
+    try {
+      await _unlockKeystore({ keyID, password });
+    } catch (e) {
+      console.error(e);
+      return { error: "Incorrect password" };
+    }
+    return {
+      mnemonicPhrase: mnemonicPhraseSelector(sessionStore.getState()),
+    };
+  };
 
   const confirmMnemonicPhrase = async () => {
     const isCorrectPhrase =
@@ -698,10 +712,13 @@ export const popupMessageListener = (request: Request, sessionStore: Store) => {
         keyID: (await localStore.getItem(KEY_ID)) || "",
         password,
       });
-      return {};
     } catch (e) {
       return { error: "Incorrect Password" };
     }
+
+    return {
+      mnemonicPhrase: mnemonicPhraseSelector(sessionStore.getState()),
+    };
   };
 
   const _getLocalStorageAccounts = async (password: string) => {

programming_language: TypeScript
filename: Router.tsx
change_type: ModificationType.MODIFY
diff: @@ -283,7 +283,7 @@ export const Router = () => {
           <GrantAccess />
         </PublicKeyRoute>
         <PublicKeyRoute path={ROUTES.mnemonicPhrase}>
-          <MnemonicPhrase />
+          <MnemonicPhrase mnemonicPhrase="" />
         </PublicKeyRoute>
         <PublicKeyRoute path={ROUTES.settings} exact>
           <Settings />

programming_language: TypeScript
filename: index.tsx
change_type: ModificationType.MODIFY
diff: @@ -6,9 +6,11 @@ import { BackButton } from "popup/basics/buttons/BackButton";
 import "./styles.scss";
 
 export const Onboarding = ({
+  customBackAction,
   hasGoBackBtn,
   children,
 }: {
+  customBackAction?: () => void;
   hasGoBackBtn?: boolean;
   children: React.ReactNode;
 }) => {
@@ -19,7 +21,7 @@ export const Onboarding = ({
     <div className="Onboarding">
       {hasGoBackBtn && !isNewTabSession ? (
         <div className="Onboarding--back">
-          <BackButton hasBackCopy />
+          <BackButton customBackAction={customBackAction} hasBackCopy />
         </div>
       ) : null}
       {children}

programming_language: TypeScript
filename: index.tsx
change_type: ModificationType.MODIFY
diff: @@ -3,8 +3,6 @@ import { useTranslation } from "react-i18next";
 
 import { InfoBlock } from "popup/basics/InfoBlock";
 import { Button } from "popup/basics/buttons/Button";
-import { ROUTES } from "popup/constants/routes";
-import { navigateTo } from "popup/helpers/navigate";
 
 import {
   OnboardingScreen,
@@ -18,8 +16,10 @@ import "./styles.scss";
 
 export const DisplayMnemonicPhrase = ({
   mnemonicPhrase,
+  setIsConfirmed,
 }: {
   mnemonicPhrase: string;
+  setIsConfirmed: (confirmed: boolean) => void;
 }) => {
   const { t } = useTranslation();
 
@@ -60,7 +60,7 @@ export const DisplayMnemonicPhrase = ({
             data-testid="display-mnemonic-phrase-next-btn"
             fullWidth
             onClick={() => {
-              navigateTo(ROUTES.mnemonicPhraseConfirm);
+              setIsConfirmed(true);
             }}
           >
             {t("Next")}

programming_language: TypeScript
filename: index.tsx
change_type: ModificationType.MODIFY
diff: @@ -11,7 +11,7 @@ interface generateMnemonicPhraseDisplayProps {
 }
 
 export const generateMnemonicPhraseDisplay = ({
-  mnemonicPhrase,
+  mnemonicPhrase = "",
 }: generateMnemonicPhraseDisplayProps) =>
   mnemonicPhrase.split(" ").map((word: string) => {
     /* 

programming_language: TypeScript
filename: useMnemonicPhrase.ts
change_type: ModificationType.DELETE
diff: @@ -1,29 +0,0 @@
-import { useEffect, useState } from "react";
-import { getMnemonicPhrase } from "@shared/api/internal";
-
-export const useMnemonicPhrase = () => {
-  const [mnemonicPhrase, setMnemonicPhrase] = useState("");
-
-  useEffect(() => {
-    let res = { mnemonicPhrase: "" };
-
-    const fetchMnemonicPhrase = async () => {
-      try {
-        res = await getMnemonicPhrase();
-      } catch (e) {
-        console.error(e);
-      }
-
-      const { mnemonicPhrase: fetchedMnemonicPhrase } = res;
-      setMnemonicPhrase(fetchedMnemonicPhrase);
-    };
-
-    fetchMnemonicPhrase();
-
-    return () => {
-      setMnemonicPhrase("");
-    };
-  }, []);
-
-  return mnemonicPhrase;
-};

programming_language: unknown
filename: index.tsx
change_type: ModificationType.MODIFY
diff: @@ -1,13 +1,12 @@
-import React, { useEffect } from "react";
+import React, { useState } from "react";
 import { useDispatch, useSelector } from "react-redux";
 import { Input, Checkbox, TextLink } from "@stellar/design-system";
 import { Field, FieldProps, Formik, Form } from "formik";
 import { object as YupObject } from "yup";
 import { useTranslation } from "react-i18next";
 
+import { showBackupPhrase } from "@shared/api/internal";
 import { Button } from "popup/basics/buttons/Button";
-import { ROUTES } from "popup/constants/routes";
-import { navigateTo } from "popup/helpers/navigate";
 import {
   password as passwordValidator,
   confirmPassword as confirmPasswordValidator,
@@ -30,6 +29,8 @@ import {
 import { Header } from "popup/components/Header";
 import { PasswordRequirements } from "popup/components/PasswordRequirements";
 
+import { MnemonicPhrase } from "popup/views/MnemonicPhrase";
+
 import "./styles.scss";
 
 export const AccountCreator = () => {
@@ -37,6 +38,7 @@ export const AccountCreator = () => {
   const dispatch = useDispatch();
   const authError = useSelector(authErrorSelector);
   const { t } = useTranslation();
+  const [mnemonicPhrase, setMnemonicPhrase] = useState("");
 
   interface FormValues {
     password: string;
@@ -52,6 +54,9 @@ export const AccountCreator = () => {
 
   const handleSubmit = async (values: FormValues) => {
     await dispatch(createAccount(values.password));
+    const res = await showBackupPhrase(values.password);
+
+    setMnemonicPhrase(res.mnemonicPhrase);
   };
 
   const AccountCreatorSchema = YupObject().shape({
@@ -60,13 +65,9 @@ export const AccountCreator = () => {
     termsOfUse: termsofUseValidator,
   });
 
-  useEffect(() => {
-    if (publicKey) {
-      navigateTo(ROUTES.mnemonicPhrase);
-    }
-  }, [publicKey]);
-
-  return (
+  return mnemonicPhrase && publicKey ? (
+    <MnemonicPhrase mnemonicPhrase={mnemonicPhrase} />
+  ) : (
     <>
       <FullscreenStyle />
       <Header />

programming_language: TypeScript
filename: index.tsx
change_type: ModificationType.MODIFY
diff: @@ -9,7 +9,6 @@ import { ROUTES } from "popup/constants/routes";
 import { Button } from "popup/basics/buttons/Button";
 import { navigateTo } from "popup/helpers/navigate";
 import { emitMetric } from "helpers/metrics";
-import { useMnemonicPhrase } from "popup/helpers/useMnemonicPhrase";
 
 import { METRIC_NAMES } from "popup/constants/metricsNames";
 
@@ -24,7 +23,7 @@ export const DisplayBackupPhrase = () => {
   const { t } = useTranslation();
   const [errorMessage, setErrorMessage] = useState("");
   const [isPhraseUnlocked, setIsPhraseUnlocked] = useState(false);
-  const mnemonicPhrase = useMnemonicPhrase();
+  const [mnemonicPhrase, setMnemonicPhrase] = useState("");
 
   useEffect(() => {
     emitMetric(
@@ -51,6 +50,7 @@ export const DisplayBackupPhrase = () => {
         error_type: res.error,
       });
     } else {
+      setMnemonicPhrase(res.mnemonicPhrase);
       setIsPhraseUnlocked(true);
       setErrorMessage("");
       emitMetric(METRIC_NAMES.backupPhraseSuccess);

programming_language: TypeScript
filename: MnemonicPhrase.tsx
change_type: ModificationType.MODIFY
diff: @@ -1,46 +1,52 @@
-import React from "react";
+import React, { useState } from "react";
 import { useSelector } from "react-redux";
 import shuffle from "lodash/shuffle";
-import { Switch, Redirect } from "react-router-dom";
+import { Redirect } from "react-router-dom";
 
 import { APPLICATION_STATE } from "@shared/constants/applicationState";
 
-import { PublicKeyRoute } from "popup/Router";
 import { ROUTES } from "popup/constants/routes";
 import { FullscreenStyle } from "popup/components/FullscreenStyle";
-import { useMnemonicPhrase } from "popup/helpers/useMnemonicPhrase";
 import { Header } from "popup/components/Header";
 import { Onboarding } from "popup/components/Onboarding";
 import { ConfirmMnemonicPhrase } from "popup/components/mnemonicPhrase/ConfirmMnemonicPhrase";
 import { DisplayMnemonicPhrase } from "popup/components/mnemonicPhrase/DisplayMnemonicPhrase";
 import { applicationStateSelector } from "popup/ducks/accountServices";
 
-export const MnemonicPhrase = () => {
-  const mnemonicPhrase = useMnemonicPhrase();
+interface MnemonicPhraseProps {
+  mnemonicPhrase: string;
+}
+
+export const MnemonicPhrase = ({
+  mnemonicPhrase = "",
+}: MnemonicPhraseProps) => {
   const applicationState = useSelector(applicationStateSelector);
+  const [isConfirmed, setIsConfirmed] = useState(false);
 
   if (applicationState === APPLICATION_STATE.MNEMONIC_PHRASE_CONFIRMED) {
     return <Redirect to={ROUTES.pinExtension} />;
   }
 
   if (mnemonicPhrase) {
-    return (
-      <Switch>
-        <PublicKeyRoute exact path={ROUTES.mnemonicPhrase}>
-          <Header />
-          <FullscreenStyle />
-          <Onboarding>
-            <DisplayMnemonicPhrase mnemonicPhrase={mnemonicPhrase} />
-          </Onboarding>
-        </PublicKeyRoute>
-        <PublicKeyRoute exact path={ROUTES.mnemonicPhraseConfirm}>
-          <Header />
-          <FullscreenStyle />
-          <Onboarding hasGoBackBtn>
-            <ConfirmMnemonicPhrase words={shuffle(mnemonicPhrase.split(" "))} />
-          </Onboarding>
-        </PublicKeyRoute>
-      </Switch>
+    return isConfirmed ? (
+      <>
+        <Header />
+        <FullscreenStyle />
+        <Onboarding customBackAction={() => setIsConfirmed(false)} hasGoBackBtn>
+          <ConfirmMnemonicPhrase words={shuffle(mnemonicPhrase.split(" "))} />
+        </Onboarding>
+      </>
+    ) : (
+      <>
+        <Header />
+        <FullscreenStyle />
+        <Onboarding>
+          <DisplayMnemonicPhrase
+            mnemonicPhrase={mnemonicPhrase}
+            setIsConfirmed={setIsConfirmed}
+          />
+        </Onboarding>
+      </>
     );
   }
 

programming_language: TypeScript
filename: MnemonicPhrase.test.tsx
change_type: ModificationType.MODIFY
diff: @@ -27,9 +27,6 @@ jest.mock("popup/constants/history", () => ({
   },
 }));
 
-jest.mock("popup/helpers/useMnemonicPhrase", () => ({
-  useMnemonicPhrase: () => "dummy mnemonic",
-}));
 jest.mock("react-router-dom", () => {
   const ReactRouter = jest.requireActual("react-router-dom");
   return {
@@ -68,7 +65,7 @@ describe.skip("MnemonicPhrase", () => {
           },
         }}
       >
-        <MnemonicPhrase />
+        <MnemonicPhrase mnemonicPhrase={MNEMONIC} />
       </Wrapper>,
     );
     await waitFor(() => screen.getByTestId("display-mnemonic-phrase"));
@@ -96,7 +93,7 @@ describe.skip("MnemonicPhrase", () => {
             },
           }}
         >
-          <MnemonicPhrase />
+          <MnemonicPhrase mnemonicPhrase={MNEMONIC} />
         </Wrapper>,
       );
 

programming_language: TypeScript