CVE-2024-29181: Insufficient Information (NVD-CWE-noinfo)
value: Strapi is an open-source content management system. Prior to version 4.19.1, a super admin can create a collection where an item in the collection has an association to another collection. When this happens, another user with Author Role can see the list of associated items they did not create. They should see nothing but their own items they created not all items ever created. Users should upgrade @strapi/plugin-content-manager to version 4.19.1 to receive a patch.
CWE description: There is insufficient information about the issue to classify it; details are unkown or unspecified.
Published: 2024-06-12T15:15Z
Repository: https://github.com/strapi/strapi
Language: TypeScript
Files changed: packages/core/content-type-builder/admin/src/components/FormModal/FormModal.tsx, packages/core/content-type-builder/admin/src/components/FormModal/component/createComponentSchema.ts, packages/core/content-type-builder/admin/src/components/FormModal/forms/forms.ts, packages/core/content-type-builder/admin/src/components/FormModal/forms/utils/createCollectionName.ts, packages/core/content-type-builder/server/src/services/schema-builder/component-builder.ts
Commit date: 2024-01-29 15:29:46 +0100
Commit message: fix: Check component names against collectionTypeNames


Diff: diff --git a/packages/core/content-type-builder/admin/src/components/FormModal/FormModal.tsx b/packages/core/content-type-builder/admin/src/components/FormModal/FormModal.tsx
index 062e4da8275..5035fb55664 100644
--- a/packages/core/content-type-builder/admin/src/components/FormModal/FormModal.tsx
+++ b/packages/core/content-type-builder/admin/src/components/FormModal/FormModal.tsx
@@ -370,10 +370,12 @@ export const FormModal = () => {
       schema = forms.component.schema(
         Object.keys(components) as Common.UID.Component[],
         modifiedData.category || '',
         reservedNames,
         actionType === 'edit',
+        components,
+        modifiedData.displayName || '',
         get(allDataSchema, [...pathToSchema, 'uid'], null)
         // ctbFormsAPI
       );
     } else if (isCreatingCustomFieldAttribute) {
       schema = forms.customField.schema({
@@ -392,11 +394,13 @@ export const FormModal = () => {
     } else if (isComponentAttribute && isCreatingComponentFromAView && isInFirstComponentStep) {
       schema = forms.component.schema(
         Object.keys(components) as Common.UID.Component[],
         get(modifiedData, 'componentToCreate.category', ''),
         reservedNames,
-        ctbFormsAPI
+        actionType === 'edit',
+        components,
+        modifiedData.componentToCreate.displayName || ''
       );
 
       // Check form validity for creating a 'common attribute'
       // We need to make sure that it is independent from the step
     } else if (isCreatingAttribute && !isInFirstComponentStep) {
@@ -445,11 +449,13 @@ export const FormModal = () => {
       if (isInFirstComponentStep && isCreatingComponentFromAView) {
         schema = forms.component.schema(
           Object.keys(components) as Common.UID.Component[],
           get(modifiedData, 'componentToCreate.category', ''),
           reservedNames,
-          ctbFormsAPI
+          actionType === 'edit',
+          components,
+          modifiedData.componentToCreate.displayName || ''
         );
       } else {
         // The form is valid
         // The case here is being in the addComponentToDynamicZone modal and not creating a component
         return;
diff --git a/packages/core/content-type-builder/admin/src/components/FormModal/component/createComponentSchema.ts b/packages/core/content-type-builder/admin/src/components/FormModal/component/createComponentSchema.ts
index 49ca3c87bb5..44e568b6ca8 100644
--- a/packages/core/content-type-builder/admin/src/components/FormModal/component/createComponentSchema.ts
+++ b/packages/core/content-type-builder/admin/src/components/FormModal/component/createComponentSchema.ts
@@ -6,11 +6,13 @@ import { CATEGORY_NAME_REGEX } from '../category/regex';
 import { createComponentUid } from '../utils/createUid';
 
 export const createComponentSchema = (
   usedComponentNames: Array<string>,
   reservedNames: Array<string>,
-  category: string
+  category: string,
+  takenCollectionNames: Array<string>,
+  currentCollectionName: string
 ) => {
   const shape = {
     displayName: yup
       .string()
       .test({
@@ -21,11 +23,14 @@ export const createComponentSchema = (
             return false;
           }
 
           const name = createComponentUid(value, category);
 
-          return !usedComponentNames.includes(name);
+          return (
+            !usedComponentNames.includes(name) &&
+            !takenCollectionNames.includes(currentCollectionName)
+          );
         },
       })
       .test({
         name: 'nameNotAllowed',
         message: getTrad('error.contentTypeName.reserved-name'),
diff --git a/packages/core/content-type-builder/admin/src/components/FormModal/forms/forms.ts b/packages/core/content-type-builder/admin/src/components/FormModal/forms/forms.ts
index d56f89dd397..9a7ab2dac0d 100644
--- a/packages/core/content-type-builder/admin/src/components/FormModal/forms/forms.ts
+++ b/packages/core/content-type-builder/admin/src/components/FormModal/forms/forms.ts
@@ -10,10 +10,11 @@ import { createComponentSchema } from '../component/createComponentSchema';
 import { contentTypeForm } from '../contentType/contentTypeForm';
 import { createContentTypeSchema } from '../contentType/createContentTypeSchema';
 import { dynamiczoneForm } from '../dynamiczoneForm';
 
 import { addItemsToFormSection, FormTypeOptions } from './utils/addItemsToFormSection';
+import { createComponentCollectionName } from './utils/createCollectionName';
 import { Attribute, getUsedAttributeNames, SchemaData } from './utils/getUsedAttributeNames';
 
 import type { Common } from '@strapi/types';
 
 type ContentType = {
@@ -306,17 +307,37 @@ export const forms = {
       componentCategory: string,
       reservedNames: {
         models: any;
       },
       isEditing = false,
+      components: Record<string, any>,
+      componentDisplayName: string,
       compoUid: Common.UID.Component | null = null
     ) {
       const takenNames = isEditing
         ? alreadyTakenAttributes.filter((uid: Common.UID.Component) => uid !== compoUid)
         : alreadyTakenAttributes;
+      const collectionNames = Object.values(components).map((component: any) => {
+        return component?.schema?.collectionName;
+      });
+
+      const currentCollectionName = createComponentCollectionName(
+        componentDisplayName,
+        componentCategory
+      );
 
-      return createComponentSchema(takenNames, reservedNames.models, componentCategory);
+      const takenCollectionNames = isEditing
+        ? collectionNames.filter((collectionName) => collectionName !== currentCollectionName)
+        : collectionNames;
+
+      return createComponentSchema(
+        takenNames,
+        reservedNames.models,
+        componentCategory,
+        takenCollectionNames,
+        currentCollectionName
+      );
     },
     form: {
       advanced() {
         return {
           sections: componentForm.advanced(),
diff --git a/packages/core/content-type-builder/admin/src/components/FormModal/forms/utils/createCollectionName.ts b/packages/core/content-type-builder/admin/src/components/FormModal/forms/utils/createCollectionName.ts
new file mode 100644
index 00000000000..2928d857efa
--- /dev/null
+++ b/packages/core/content-type-builder/admin/src/components/FormModal/forms/utils/createCollectionName.ts
@@ -0,0 +1,10 @@
+import { snakeCase } from 'lodash/fp';
+import pluralize from 'pluralize';
+
+// TODO: create a utility for this
+// Duplicate in server/src/services/schema-builder/component-builder.ts
+const createComponentCollectionName = (name: string, category: string) => {
+  return `components_${snakeCase(category)}_${pluralize(snakeCase(name))}`;
+};
+
+export { createComponentCollectionName };
diff --git a/packages/core/content-type-builder/server/src/services/schema-builder/component-builder.ts b/packages/core/content-type-builder/server/src/services/schema-builder/component-builder.ts
index 019ca9d97ff..c200444bfc7 100644
--- a/packages/core/content-type-builder/server/src/services/schema-builder/component-builder.ts
+++ b/packages/core/content-type-builder/server/src/services/schema-builder/component-builder.ts
@@ -35,14 +35,22 @@ export default function createComponentBuilder() {
       const handler = createSchemaHandler({
         dir: path.join(strapi.dirs.app.components, nameToSlug(infos.category)),
         filename: `${nameToSlug(infos.displayName)}.json`,
       });
 
+      // TODO: create a utility for this
+      // Duplicate in admin/src/components/FormModal/forms/utils/createCollectionName.ts
       const collectionName = `components_${nameToCollectionName(
         infos.category
       )}_${nameToCollectionName(pluralize(infos.displayName))}`;
 
+      this.components.forEach((compo: any) => {
+        if (compo.schema.collectionName === collectionName) {
+          throw new ApplicationError('component.alreadyExists');
+        }
+      });
+
       handler
         .setUID(uid)
         .set('collectionName', collectionName)
         .set(['info', 'displayName'], infos.displayName)
         .set(['info', 'icon'], infos.icon)

Vulnerable code:         ctbFormsAPI
          ctbFormsAPI
  category: string
          return !usedComponentNames.includes(name);
      return createComponentSchema(takenNames, reservedNames.models, componentCategory);
Fixed code:         components,
        modifiedData.displayName || '',
        actionType === 'edit',
        components,
        modifiedData.componentToCreate.displayName || ''
          actionType === 'edit',
          components,
          modifiedData.componentToCreate.displayName || ''
  category: string,
  takenCollectionNames: Array<string>,
  currentCollectionName: string
          return (
            !usedComponentNames.includes(name) &&
            !takenCollectionNames.includes(currentCollectionName)
          );
import { createComponentCollectionName } from './utils/createCollectionName';
      components: Record<string, any>,
      componentDisplayName: string,
      const collectionNames = Object.values(components).map((component: any) => {
        return component?.schema?.collectionName;
      });

      const currentCollectionName = createComponentCollectionName(
        componentDisplayName,
        componentCategory
      );
      const takenCollectionNames = isEditing
        ? collectionNames.filter((collectionName) => collectionName !== currentCollectionName)
        : collectionNames;

      return createComponentSchema(
        takenNames,
        reservedNames.models,
        componentCategory,
        takenCollectionNames,
        currentCollectionName
      );
import { snakeCase } from 'lodash/fp';
import pluralize from 'pluralize';

// TODO: create a utility for this
// Duplicate in server/src/services/schema-builder/component-builder.ts
const createComponentCollectionName = (name: string, category: string) => {
  return `components_${snakeCase(category)}_${pluralize(snakeCase(name))}`;
};

export { createComponentCollectionName };
      // TODO: create a utility for this
      // Duplicate in admin/src/components/FormModal/forms/utils/createCollectionName.ts
      this.components.forEach((compo: any) => {
        if (compo.schema.collectionName === collectionName) {
          throw new ApplicationError('component.alreadyExists');
        }
      });