CVE-2023-34093: Exposure of Sensitive Information to an Unauthorized Actor (CWE-200)
value: Strapi is an open-source headless content management system. Prior to version 4.10.8, anyone (Strapi developers, users, plugins) can make every attribute of a Content-Type public without knowing it. The vulnerability only affects the handling of content types by Strapi, not the actual content types themselves. Users can use plugins or modify their own content types without realizing that the `privateAttributes` getter is being removed, which can result in any attribute becoming public. This can lead to sensitive information being exposed or the entire system being taken control of by an attacker(having access to password hashes). Anyone can be impacted, depending on how people are using/extending content-types. If the users are mutating the content-type, they will not be affected. Version 4.10.8 contains a patch for this issue.
CWE description: The product exposes sensitive information to an actor that is not explicitly authorized to have access to that information.
CVSS v3 base score: 7.1
Published: 2023-07-25T15:15Z
Repository: https://github.com/strapi/strapi
Language: JavaScript
Files changed: api-tests/core/database/transactions.test.api.js, packages/core/admin/server/services/__tests__/permissions-manager.test.js, packages/core/strapi/lib/core/domain/content-type/index.js, packages/core/strapi/lib/services/entity-service/__tests__/entity-service-events.test.js, packages/core/strapi/lib/services/entity-service/__tests__/entity-service.test.js, packages/core/strapi/lib/services/entity-validator/__tests__/biginteger-validators.test.js, packages/core/strapi/lib/services/entity-validator/__tests__/date-validators.test.js, packages/core/strapi/lib/services/entity-validator/__tests__/datetime-validators.test.js, packages/core/strapi/lib/services/entity-validator/__tests__/float-validators.test.js, packages/core/strapi/lib/services/entity-validator/__tests__/integer-validators.test.js, packages/core/strapi/lib/services/entity-validator/__tests__/string-validators.test.js, packages/core/strapi/lib/services/entity-validator/__tests__/time-validators.test.js, packages/core/strapi/lib/services/entity-validator/__tests__/timestamp-validators.test.js, packages/core/strapi/lib/services/entity-validator/__tests__/uid-validators.test.js, packages/core/utils/lib/__tests__/content-types.test.js, packages/core/utils/lib/content-types.js, packages/core/utils/lib/sanitize/visitors/remove-private.js
Commit date: 2023-06-07 14:44:04 +0200
Commit message: fix(content-types): remove getter for private attributes

+ Fixes a flaky test (transactions.test.api.js)

Diff: diff --git a/api-tests/core/database/transactions.test.api.js b/api-tests/core/database/transactions.test.api.js
index 7b8ad2aff0..a444d75a7e 100644
--- a/api-tests/core/database/transactions.test.api.js
+++ b/api-tests/core/database/transactions.test.api.js
@@ -282,11 +282,11 @@ describe('transactions', () => {
         .queryBuilder('strapi::core-store')
         .select(['*'])
         .where({ id: 1 })
         .execute();
 
-      expect(end[0].key).toEqual('strapi_content_types_schema');
+      expect(end[0].key).toEqual(original[0].key);
     });
 
     test('rollback successfully', async () => {
       const trx = await strapi.db.transaction();
 
@@ -321,9 +321,9 @@ describe('transactions', () => {
         .queryBuilder('strapi::core-store')
         .select(['*'])
         .where({ id: 1 })
         .execute();
 
-      expect(end[0].key).toEqual('strapi_content_types_schema');
+      expect(end[0].key).toEqual(original[0].key);
     });
   });
 });
diff --git a/packages/core/admin/server/services/__tests__/permissions-manager.test.js b/packages/core/admin/server/services/__tests__/permissions-manager.test.js
index c5e86fc5ae..0dd621fc85 100644
--- a/packages/core/admin/server/services/__tests__/permissions-manager.test.js
+++ b/packages/core/admin/server/services/__tests__/permissions-manager.test.js
@@ -141,11 +141,10 @@ describe('Permissions Manager', () => {
 
   describe('pickPermittedFieldsOf', () => {
     global.strapi = {
       getModel() {
         return {
-          privateAttributes: [],
           attributes: {
             title: {
               type: 'text',
               private: false,
             },
diff --git a/packages/core/strapi/lib/core/domain/content-type/index.js b/packages/core/strapi/lib/core/domain/content-type/index.js
index 5323400748..209671219e 100644
--- a/packages/core/strapi/lib/core/domain/content-type/index.js
+++ b/packages/core/strapi/lib/core/domain/content-type/index.js
@@ -1,10 +1,10 @@
 'use strict';
 
 const { cloneDeep } = require('lodash/fp');
 const _ = require('lodash');
-const { hasDraftAndPublish, getPrivateAttributes } = require('@strapi/utils').contentTypes;
+const { hasDraftAndPublish } = require('@strapi/utils').contentTypes;
 const {
   CREATED_AT_ATTRIBUTE,
   UPDATED_AT_ATTRIBUTE,
   PUBLISHED_AT_ATTRIBUTE,
   CREATED_BY_ATTRIBUTE,
@@ -56,16 +56,10 @@ const createContentType = (uid, definition) => {
     throw new Error(
       `Incorrect Content Type UID "${uid}". The UID should start with api::, plugin:: or admin::.`
     );
   }
 
-  Object.defineProperty(schema, 'privateAttributes', {
-    get() {
-      return getPrivateAttributes(schema);
-    },
-  });
-
   // attributes
   Object.assign(schema.attributes, {
     [CREATED_AT_ATTRIBUTE]: {
       type: 'datetime',
     },
diff --git a/packages/core/strapi/lib/services/entity-service/__tests__/entity-service-events.test.js b/packages/core/strapi/lib/services/entity-service/__tests__/entity-service-events.test.js
index a82221cbe2..e5e5c51685 100644
--- a/packages/core/strapi/lib/services/entity-service/__tests__/entity-service-events.test.js
+++ b/packages/core/strapi/lib/services/entity-service/__tests__/entity-service-events.test.js
@@ -11,11 +11,10 @@ describe('Entity service triggers webhooks', () => {
   beforeAll(() => {
     const model = {
       uid: 'api::test.test',
       kind: 'singleType',
       modelName: 'test-model',
-      privateAttributes: [],
       attributes: {
         attr: { type: 'string' },
       },
     };
     instance = createEntityService({
diff --git a/packages/core/strapi/lib/services/entity-service/__tests__/entity-service.test.js b/packages/core/strapi/lib/services/entity-service/__tests__/entity-service.test.js
index 089d8007b2..e624fee8bf 100644
--- a/packages/core/strapi/lib/services/entity-service/__tests__/entity-service.test.js
+++ b/packages/core/strapi/lib/services/entity-service/__tests__/entity-service.test.js
@@ -60,11 +60,11 @@ describe('Entity service', () => {
         transaction: (cb) => cb(),
       };
 
       const fakeStrapi = {
         getModel: jest.fn(() => {
-          return { kind: 'singleType', privateAttributes: [] };
+          return { kind: 'singleType' };
         }),
       };
 
       const instance = createEntityService({
         strapi: fakeStrapi,
@@ -131,11 +131,10 @@ describe('Entity service', () => {
 
         fakeModels[entityUID] = {
           uid: entityUID,
           kind: 'contentType',
           modelName: 'test-model',
-          privateAttributes: [],
           options: {},
           attributes: {
             attrStringDefaultRequired: {
               type: 'string',
               default: 'default value',
@@ -465,11 +464,10 @@ describe('Entity service', () => {
         [entityUID]: {
           kind: 'collectionType',
           modelName: 'entity',
           collectionName: 'entity',
           uid: entityUID,
-          privateAttributes: [],
           options: {},
           info: {
             singularName: 'entity',
             pluralName: 'entities',
             displayName: 'ENTITY',
diff --git a/packages/core/strapi/lib/services/entity-validator/__tests__/biginteger-validators.test.js b/packages/core/strapi/lib/services/entity-validator/__tests__/biginteger-validators.test.js
index 787d8be5ee..fb33727261 100644
--- a/packages/core/strapi/lib/services/entity-validator/__tests__/biginteger-validators.test.js
+++ b/packages/core/strapi/lib/services/entity-validator/__tests__/biginteger-validators.test.js
@@ -25,11 +25,10 @@ describe('BigInteger validator', () => {
 
     const fakeModel = {
       kind: 'contentType',
       modelName: 'test-model',
       uid: 'test-uid',
-      privateAttributes: [],
       options: {},
       attributes: {
         attrBigIntegerUnique: { type: 'biginteger', unique: true },
       },
     };
diff --git a/packages/core/strapi/lib/services/entity-validator/__tests__/date-validators.test.js b/packages/core/strapi/lib/services/entity-validator/__tests__/date-validators.test.js
index 92fadf8c23..d280cd7920 100644
--- a/packages/core/strapi/lib/services/entity-validator/__tests__/date-validators.test.js
+++ b/packages/core/strapi/lib/services/entity-validator/__tests__/date-validators.test.js
@@ -25,11 +25,10 @@ describe('Date validator', () => {
 
     const fakeModel = {
       kind: 'contentType',
       modelName: 'test-model',
       uid: 'test-uid',
-      privateAttributes: [],
       options: {},
       attributes: {
         attrDateUnique: { type: 'date', unique: true },
       },
     };
diff --git a/packages/core/strapi/lib/services/entity-validator/__tests__/datetime-validators.test.js b/packages/core/strapi/lib/services/entity-validator/__tests__/datetime-validators.test.js
index 5d0a895737..204a561577 100644
--- a/packages/core/strapi/lib/services/entity-validator/__tests__/datetime-validators.test.js
+++ b/packages/core/strapi/lib/services/entity-validator/__tests__/datetime-validators.test.js
@@ -25,11 +25,10 @@ describe('Datetime validator', () => {
 
     const fakeModel = {
       kind: 'contentType',
       modelName: 'test-model',
       uid: 'test-uid',
-      privateAttributes: [],
       options: {},
       attributes: {
         attrDateTimeUnique: { type: 'datetime', unique: true },
       },
     };
diff --git a/packages/core/strapi/lib/services/entity-validator/__tests__/float-validators.test.js b/packages/core/strapi/lib/services/entity-validator/__tests__/float-validators.test.js
index d40d955ffa..f433702f35 100644
--- a/packages/core/strapi/lib/services/entity-validator/__tests__/float-validators.test.js
+++ b/packages/core/strapi/lib/services/entity-validator/__tests__/float-validators.test.js
@@ -25,11 +25,10 @@ describe('Float validator', () => {
 
     const fakeModel = {
       kind: 'contentType',
       uid: 'test-uid',
       modelName: 'test-model',
-      privateAttributes: [],
       options: {},
       attributes: {
         attrFloatUnique: { type: 'float', unique: true },
       },
     };
diff --git a/packages/core/strapi/lib/services/entity-validator/__tests__/integer-validators.test.js b/packages/core/strapi/lib/services/entity-validator/__tests__/integer-validators.test.js
index 415d7479fc..f429e33049 100644
--- a/packages/core/strapi/lib/services/entity-validator/__tests__/integer-validators.test.js
+++ b/packages/core/strapi/lib/services/entity-validator/__tests__/integer-validators.test.js
@@ -25,11 +25,10 @@ describe('Integer validator', () => {
 
     const fakeModel = {
       kind: 'contentType',
       uid: 'test-uid',
       modelName: 'test-model',
-      privateAttributes: [],
       options: {},
       attributes: {
         attrIntegerUnique: { type: 'integer', unique: true },
       },
     };
diff --git a/packages/core/strapi/lib/services/entity-validator/__tests__/string-validators.test.js b/packages/core/strapi/lib/services/entity-validator/__tests__/string-validators.test.js
index a6b7d9a69c..23f9b94942 100644
--- a/packages/core/strapi/lib/services/entity-validator/__tests__/string-validators.test.js
+++ b/packages/core/strapi/lib/services/entity-validator/__tests__/string-validators.test.js
@@ -25,11 +25,10 @@ describe('String validator', () => {
 
     const fakeModel = {
       kind: 'contentType',
       modelName: 'test-model',
       uid: 'test-uid',
-      privateAttributes: [],
       options: {},
       attributes: {
         attrStringUnique: { type: 'string', unique: true },
       },
     };
diff --git a/packages/core/strapi/lib/services/entity-validator/__tests__/time-validators.test.js b/packages/core/strapi/lib/services/entity-validator/__tests__/time-validators.test.js
index 7b83c2d183..26f3260861 100644
--- a/packages/core/strapi/lib/services/entity-validator/__tests__/time-validators.test.js
+++ b/packages/core/strapi/lib/services/entity-validator/__tests__/time-validators.test.js
@@ -25,11 +25,10 @@ describe('Time validator', () => {
 
     const fakeModel = {
       kind: 'contentType',
       modelName: 'test-model',
       uid: 'test-uid',
-      privateAttributes: [],
       options: {},
       attributes: {
         attrTimeUnique: { type: 'time', unique: true },
       },
     };
diff --git a/packages/core/strapi/lib/services/entity-validator/__tests__/timestamp-validators.test.js b/packages/core/strapi/lib/services/entity-validator/__tests__/timestamp-validators.test.js
index 3e3f40b9c5..2d808f02d0 100644
--- a/packages/core/strapi/lib/services/entity-validator/__tests__/timestamp-validators.test.js
+++ b/packages/core/strapi/lib/services/entity-validator/__tests__/timestamp-validators.test.js
@@ -25,11 +25,10 @@ describe('Time validator', () => {
 
     const fakeModel = {
       kind: 'contentType',
       modelName: 'test-model',
       uid: 'test-uid',
-      privateAttributes: [],
       options: {},
       attributes: {
         attrTimestampUnique: { type: 'timestamp', unique: true },
       },
     };
diff --git a/packages/core/strapi/lib/services/entity-validator/__tests__/uid-validators.test.js b/packages/core/strapi/lib/services/entity-validator/__tests__/uid-validators.test.js
index bd75ed796b..cb58d3ea65 100644
--- a/packages/core/strapi/lib/services/entity-validator/__tests__/uid-validators.test.js
+++ b/packages/core/strapi/lib/services/entity-validator/__tests__/uid-validators.test.js
@@ -24,11 +24,10 @@ describe('UID validator', () => {
 
   const fakeModel = {
     kind: 'contentType',
     modelName: 'test-model',
     uid: 'test-uid',
-    privateAttributes: [],
     options: {},
     attributes: {
       attrUidUnique: { type: 'uid' },
     },
   };
diff --git a/packages/core/utils/lib/__tests__/content-types.test.js b/packages/core/utils/lib/__tests__/content-types.test.js
index 6029d93fa8..491abe1227 100644
--- a/packages/core/utils/lib/__tests__/content-types.test.js
+++ b/packages/core/utils/lib/__tests__/content-types.test.js
@@ -151,33 +151,30 @@ describe('Content types utils', () => {
 
   describe('isPrivateAttribute', () => {
     test('Attribute is private in the model attributes', () => {
       const model = createModelWithPrivates();
       global.strapi = { config: createConfig() };
-      Object.assign(model, { privateAttributes: getPrivateAttributes(model) });
 
       expect(isPrivateAttribute(model, 'foo')).toBeTruthy();
       expect(isPrivateAttribute(model, 'bar')).toBeFalsy();
       expect(isPrivateAttribute(model, 'foobar')).toBeFalsy();
       expect(strapi.config.get).toHaveBeenCalledWith('api.responses.privateAttributes', []);
     });
 
     test('Attribute is set to private in the app config', () => {
       const model = createModelWithPrivates();
       global.strapi = { config: createConfig(['bar']) };
-      Object.assign(model, { privateAttributes: getPrivateAttributes(model) });
 
       expect(isPrivateAttribute(model, 'foo')).toBeTruthy();
       expect(isPrivateAttribute(model, 'bar')).toBeTruthy();
       expect(isPrivateAttribute(model, 'foobar')).toBeFalsy();
       expect(strapi.config.get).toHaveBeenCalledWith('api.responses.privateAttributes', []);
     });
 
     test('Attribute is set to private in the model options', () => {
       const model = createModelWithPrivates(['foobar']);
       global.strapi = { config: createConfig() };
-      Object.assign(model, { privateAttributes: getPrivateAttributes(model) });
 
       expect(isPrivateAttribute(model, 'foo')).toBeTruthy();
       expect(isPrivateAttribute(model, 'bar')).toBeFalsy();
       expect(isPrivateAttribute(model, 'foobar')).toBeTruthy();
       expect(strapi.config.get).toHaveBeenCalledWith('api.responses.privateAttributes', []);
diff --git a/packages/core/utils/lib/content-types.js b/packages/core/utils/lib/content-types.js
index aea8bf571c..35e9c635c7 100644
--- a/packages/core/utils/lib/content-types.js
+++ b/packages/core/utils/lib/content-types.js
@@ -1,9 +1,9 @@
 'use strict';
 
 const _ = require('lodash');
-const { has } = require('lodash/fp');
+const { getOr, has, union } = require('lodash/fp');
 
 const SINGLE_TYPE = 'singleType';
 const COLLECTION_TYPE = 'collectionType';
 
 const ID_ATTRIBUTE = 'id';
@@ -90,20 +90,27 @@ const isDraft = (data, model) =>
 
 const isSingleType = ({ kind = COLLECTION_TYPE }) => kind === SINGLE_TYPE;
 const isCollectionType = ({ kind = COLLECTION_TYPE }) => kind === COLLECTION_TYPE;
 const isKind = (kind) => (model) => model.kind === kind;
 
+const getStoredPrivateAttributes = (model) => union(
+  strapi?.config?.get('api.responses.privateAttributes', []) ?? [],
+  getOr([], 'options.privateAttributes', model)
+);
+
 const getPrivateAttributes = (model = {}) => {
   return _.union(
-    strapi.config.get('api.responses.privateAttributes', []),
-    _.get(model, 'options.privateAttributes', []),
+    getStoredPrivateAttributes(model),
     _.keys(_.pickBy(model.attributes, (attr) => !!attr.private))
   );
 };
 
 const isPrivateAttribute = (model, attributeName) => {
-  return model?.privateAttributes?.includes(attributeName) ?? false;
+  if (model?.attributes?.[attributeName]?.private === true) {
+    return true;
+  }
+  return getStoredPrivateAttributes(model).includes(attributeName);
 };
 
 const isScalarAttribute = (attribute) => {
   return !['media', 'component', 'relation', 'dynamiczone'].includes(attribute?.type);
 };
diff --git a/packages/core/utils/lib/sanitize/visitors/remove-private.js b/packages/core/utils/lib/sanitize/visitors/remove-private.js
index 209b9f1c62..1d01cd5b2d 100644
--- a/packages/core/utils/lib/sanitize/visitors/remove-private.js
+++ b/packages/core/utils/lib/sanitize/visitors/remove-private.js
@@ -5,11 +5,11 @@ const { isPrivateAttribute } = require('../../content-types');
 module.exports = ({ schema, key, attribute }, { remove }) => {
   if (!attribute) {
     return;
   }
 
-  const isPrivate = isPrivateAttribute(schema, key) || attribute.private === true;
+  const isPrivate = attribute.private === true || isPrivateAttribute(schema, key);
 
   if (isPrivate) {
     remove(key);
   }
 };

Vulnerable code:       expect(end[0].key).toEqual('strapi_content_types_schema');
      expect(end[0].key).toEqual('strapi_content_types_schema');
          privateAttributes: [],
const { hasDraftAndPublish, getPrivateAttributes } = require('@strapi/utils').contentTypes;
  Object.defineProperty(schema, 'privateAttributes', {
    get() {
      return getPrivateAttributes(schema);
    },
  });

      privateAttributes: [],
          return { kind: 'singleType', privateAttributes: [] };
          privateAttributes: [],
          privateAttributes: [],
      privateAttributes: [],
      privateAttributes: [],
      privateAttributes: [],
      privateAttributes: [],
      privateAttributes: [],
      privateAttributes: [],
      privateAttributes: [],
      privateAttributes: [],
    privateAttributes: [],
      Object.assign(model, { privateAttributes: getPrivateAttributes(model) });
      Object.assign(model, { privateAttributes: getPrivateAttributes(model) });
      Object.assign(model, { privateAttributes: getPrivateAttributes(model) });
const { has } = require('lodash/fp');
    strapi.config.get('api.responses.privateAttributes', []),
    _.get(model, 'options.privateAttributes', []),
  return model?.privateAttributes?.includes(attributeName) ?? false;
  const isPrivate = isPrivateAttribute(schema, key) || attribute.private === true;
Fixed code:       expect(end[0].key).toEqual(original[0].key);
      expect(end[0].key).toEqual(original[0].key);
const { hasDraftAndPublish } = require('@strapi/utils').contentTypes;
          return { kind: 'singleType' };
const { getOr, has, union } = require('lodash/fp');
const getStoredPrivateAttributes = (model) => union(
  strapi?.config?.get('api.responses.privateAttributes', []) ?? [],
  getOr([], 'options.privateAttributes', model)
);

    getStoredPrivateAttributes(model),
  if (model?.attributes?.[attributeName]?.private === true) {
    return true;
  }
  return getStoredPrivateAttributes(model).includes(attributeName);
  const isPrivate = attribute.private === true || isPrivateAttribute(schema, key);