CVE-2023-1665 (TypeScript, published 2023-03-27T22:15Z)
Description: value: Improper Restriction of Excessive Authentication Attempts in GitHub repository linagora/twake prior to 0.0.0.
Severity: nan (CVSS v3 base score: 9.8)
Weakness: CWE-307 \u2014 Improper Restriction of Excessive Authentication Attempts
The product does not implement sufficient measures to prevent multiple failed authentication attempts within a short time frame, making it more susceptible to brute force attacks.
Repository: https://github.com/linagora/twake
Files changed:
twake/backend/node/src/cli/cmds/migration_cmds/php-drive-file/drive-migrator-service.ts
Commit message: 🛠 Fix drive migration (#2784)

* 🛠 Fix Drive migration

* Move timeout

Vulnerable code:
      const migrationRecord = await this.phpDriveService.getMigrationRecord(
        item.id,
        context.company.id,
      );
      const newDriveItem = getDefaultDriveItem(
        {
          name: item.name || item.id,
          extension: item.extension,
          added: item.added.toString(),
          content_keywords:
            item.content_keywords && item.content_keywords.length
              ? item.content_keywords.join(",")
              : "",
          creator: item.creator || context.user.id,
          is_directory: item.isdirectory,
          is_in_trash: item.isintrash,
          description: item.description,
          tags: item.tags || [],
          parent_id: parentId,
          company_id: context.company.id,
          access_info: access,
        },
        context,
      );
      if (migrationRecord && migrationRecord.company_id === context.company.id) {
        console.debug(`${item.id} is already migrated`);
      } else {
        await this.nodeRepository.save(newDriveItem);
      }
      if (item.isdirectory) {
        const newParentId =
          migrationRecord && migrationRecord.company_id === context.company.id
            ? migrationRecord.new_id
            : newDriveItem.id;

        let page: Pagination = { limitStr: "100" };

        do {
          const directoryChildren = await this.phpDriveService.listDirectory(
            page,
            item.id,
            context.workspace_id,
          );
          page = directoryChildren.nextPage as Pagination;

          for (const child of directoryChildren.getEntities()) {
            try {
              await this.migrateDriveFile(child, newParentId, access, context);
            } catch (error) {
              logger.error(`Failed to migrate drive item ${child.id}`);
              console.error(`Failed to migrate drive item ${child.id}`);
        } while (page.page_token);
      } else {
        let versionPage: Pagination = { limitStr: "100" };
        if (
          migrationRecord &&
          migrationRecord.item_id === item.id &&
          migrationRecord.company_id === context.company.id
        ) {
          logger.info(`item is already migrated - ${item.id} - skipping`);
          console.log(`item is already migrated - ${item.id} - skipping`);
          return;
        }
        const mime = mimes[item.extension];

        let createdVersions = 0;

        do {
          const itemVersions = await this.phpDriveService.listItemVersions(
            versionPage,
            item.id,
            context,
          );
          versionPage = itemVersions.nextPage as Pagination;

          for (const version of itemVersions.getEntities()) {
            try {
              const newVersion = getDefaultDriveItemVersion(
                {
                  creator_id: version.creator_id || context.user.id,
                  data: version.data,
                  date_added: +version.date_added,
                  drive_item_id: newDriveItem.id,
                  file_size: version.file_size,
                  filename: version.filename,
                  key: version.key,
                  provider: version.provider,
                  realname: version.realname,
                  mode: version.mode,
                },
                context,
              );

              logger.info(
                `Migrating version ${version.id} of item ${item.id}... (downloading then uploading...)`,
              );
              const file = await this.phpDriveService.migrate(
                version.file_id,
                item.workspace_id,
                version.id,
                {
                  filename: version.filename,
                  userId: version.creator_id || context.user.id,
                  totalSize: version.file_size,
                  waitForThumbnail: true,
                  chunkNumber: 1,
                  totalChunks: 1,
                  type: mime,
                },
                context,
              );

              if (!file) {
                throw Error("cannot download file version");

              newVersion.file_metadata = {
                external_id: file.id,
                mime: file.metadata.mime,
                name: file.metadata.name || version.filename,
                size: file.upload_data.size || version.file_size,
              };

              await globalResolver.services.documents.documents.createVersion(
                newDriveItem.id,
                newVersion,
                context,
              );

              createdVersions++;
            } catch (error) {
              logger.error(`Failed to migrate version ${version.id} for drive item ${item.id}`);
              console.error(`Failed to migrate version ${version.id} for drive item ${item.id}`);
        } while (versionPage.page_token);
        if (createdVersions === 0) {
          await this.nodeRepository.remove(newDriveItem);
          return;
      }
      if (!migrationRecord) {
        await this.phpDriveService.markAsMigrated(item.id, newDriveItem.id, context.company.id);
      }

Fixed code:

      console.log("Loop over companies...", page.page_token);
      await new Promise(async (resolve, reject) => {
        const migrationRecord = await this.phpDriveService.getMigrationRecord(
          item.id,
          context.company.id,
        );
        const newDriveItem = getDefaultDriveItem(
          {
            name: item.name || item.id,
            extension: item.extension,
            added: item.added.toString(),
            content_keywords:
              item.content_keywords && item.content_keywords.length
                ? item.content_keywords.join(",")
                : "",
            creator: item.creator || context.user.id,
            is_directory: item.isdirectory,
            is_in_trash: item.isintrash,
            description: item.description,
            tags: item.tags || [],
            parent_id: parentId,
            company_id: context.company.id,
            access_info: access,
          },
          context,
        );
        if (migrationRecord && migrationRecord.company_id === context.company.id) {
          console.debug(`${item.id} is already migrated`);
        } else {
          await this.nodeRepository.save(newDriveItem);
        }
        if (item.isdirectory) {
          const newParentId =
            migrationRecord && migrationRecord.company_id === context.company.id
              ? migrationRecord.new_id
              : newDriveItem.id;

          let page: Pagination = { limitStr: "100" };

          do {
            const directoryChildren = await this.phpDriveService.listDirectory(
              page,
              item.id,
              context.workspace_id,
            );
            page = directoryChildren.nextPage as Pagination;

            for (const child of directoryChildren.getEntities()) {
              try {
                await this.migrateDriveFile(child, newParentId, access, context);
              } catch (error) {
                logger.error(`Failed to migrate drive item ${child.id}`);
                console.error(`Failed to migrate drive item ${child.id}`);
              }
          } while (page.page_token);
        } else {
          let versionPage: Pagination = { limitStr: "100" };
          if (
            migrationRecord &&
            migrationRecord.item_id === item.id &&
            migrationRecord.company_id === context.company.id
          ) {
            logger.info(`item is already migrated - ${item.id} - skipping`);
            console.log(`item is already migrated - ${item.id} - skipping`);
            return;
          const mime = mimes[item.extension];

          let createdVersions = 0;

          const timeout = setTimeout(() => reject("Timeout"), 60000);

          do {
            const itemVersions = await this.phpDriveService.listItemVersions(
              versionPage,
              item.id,
              context,
            );
            versionPage = itemVersions.nextPage as Pagination;

            for (const version of itemVersions.getEntities()) {
              try {
                const newVersion = getDefaultDriveItemVersion(
                  {
                    creator_id: version.creator_id || context.user.id,
                    data: version.data,
                    date_added: +version.date_added,
                    drive_item_id: newDriveItem.id,
                    file_size: version.file_size,
                    filename: version.filename,
                    key: version.key,
                    provider: version.provider,
                    realname: version.realname,
                    mode: version.mode,
                  },
                  context,
                );

                logger.info(
                  `Migrating version ${version.id} of item ${item.id}... (downloading then uploading...)`,
                );
                const file = await this.phpDriveService.migrate(
                  version.file_id,
                  item.workspace_id,
                  version.id,
                  {
                    filename: version.filename,
                    userId: version.creator_id || context.user.id,
                    totalSize: version.file_size,
                    waitForThumbnail: true,
                    chunkNumber: 1,
                    totalChunks: 1,
                    type: mime,
                  },
                  context,
                );

                if (!file) {
                  throw Error("cannot download file version");
                }

                newVersion.file_metadata = {
                  external_id: file.id,
                  mime: file.metadata.mime,
                  name: file.metadata.name || version.filename,
                  size: file.upload_data.size || version.file_size,
                };

                await globalResolver.services.documents.documents.createVersion(
                  newDriveItem.id,
                  newVersion,
                  context,
                );

                createdVersions++;
              } catch (error) {
                logger.error(`Failed to migrate version ${version.id} for drive item ${item.id}`);
                console.error(`Failed to migrate version ${version.id} for drive item ${item.id}`);
          } while (versionPage.page_token);

          clearTimeout(timeout);

          if (createdVersions === 0) {
            await this.nodeRepository.remove(newDriveItem);
            return;
        }
        if (!migrationRecord) {
          await this.phpDriveService.markAsMigrated(item.id, newDriveItem.id, context.company.id);
        resolve(true);
      });