Strapi allows actors to make all attributes on a content-type public without noticing it
Description
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.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
A getter removal bug in Strapi before 4.10.8 can expose all content-type attributes as public, potentially leaking sensitive data like password hashes.
Vulnerability
Analysis
CVE-2023-34093 affects Strapi, an open-source headless CMS, in versions prior to 4.10.8. The vulnerability arises from how the system's content-type handling removes a getter for privateAttributes when content types are extended programmatically. Specifically, when developers or plugins copy a content-type object (e.g., via the spread operator) during runtime extension (using strapi.container.get('content-types').extend), the getter that computes which attributes should be private is stripped away. This causes all attributes to be considered public by the sanitization routines, regardless of their intended visibility settings [1][4].
Exploitation
An attacker with low privileges—or even an unauthenticated position depending on the endpoint—could exploit this by querying API responses that include content-type data. The attack vector is indirect: it requires a developer or plugin to extend a content-type in a way that creates a shallow copy of the object, which inadvertently removes the getter. Once the getter is missing, every attribute (including sensitive ones like password hashes, email addresses, or internal identifiers) is returned in API responses without sanitization. No special authentication is needed beyond normal access to content-type endpoints [1][4].
Impact
Successful exploitation can lead to exposure of sensitive information, most critically password hashes stored in user content types. An attacker with access to these hashes may be able to crack passwords offline and gain administrative control over the Strapi instance. This could further escalate to full system compromise, as described by the vendor [1]. The impact is broad because any content type extended via a shallow copy is affected, and the issue may go unnoticed during development or plugin usage.
Mitigation
Strapi has released version 4.10.8, which includes a fix in the commit referenced in [3]. The patch ensures that privateAttributes is computed reliably, preventing the accidental exposure of all attributes. Users are strongly advised to update to the latest version. If upgrading immediately is not possible, developers should avoid using shallow copies when extending content types and instead use mutation-based approaches to preserve the getter [1][3][4].
AI Insight generated on May 20, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
@strapi/strapinpm | < 4.10.8 | 4.10.8 |
@strapi/utilsnpm | < 4.10.8 | 4.10.8 |
@strapi/databasenpm | < 4.10.8 | 4.10.8 |
Affected products
4- ghsa-coords3 versions
< 4.10.8+ 2 more
- (no CPE)range: < 4.10.8
- (no CPE)range: < 4.10.8
- (no CPE)range: < 4.10.8
- strapi/strapiv5Range: < 4.10.8
Patches
12fa8f30371bffix(content-types): remove getter for private attributes
17 files changed · +16 −31
api-tests/core/database/transactions.test.api.js+2 −2 modified@@ -284,7 +284,7 @@ describe('transactions', () => { .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 () => { @@ -323,7 +323,7 @@ describe('transactions', () => { .where({ id: 1 }) .execute(); - expect(end[0].key).toEqual('strapi_content_types_schema'); + expect(end[0].key).toEqual(original[0].key); }); }); });
packages/core/admin/server/services/__tests__/permissions-manager.test.js+0 −1 modified@@ -143,7 +143,6 @@ describe('Permissions Manager', () => { global.strapi = { getModel() { return { - privateAttributes: [], attributes: { title: { type: 'text',
packages/core/strapi/lib/core/domain/content-type/index.js+1 −7 modified@@ -2,7 +2,7 @@ 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, @@ -58,12 +58,6 @@ const createContentType = (uid, definition) => { ); } - Object.defineProperty(schema, 'privateAttributes', { - get() { - return getPrivateAttributes(schema); - }, - }); - // attributes Object.assign(schema.attributes, { [CREATED_AT_ATTRIBUTE]: {
packages/core/strapi/lib/services/entity-service/__tests__/entity-service-events.test.js+0 −1 modified@@ -13,7 +13,6 @@ describe('Entity service triggers webhooks', () => { uid: 'api::test.test', kind: 'singleType', modelName: 'test-model', - privateAttributes: [], attributes: { attr: { type: 'string' }, },
packages/core/strapi/lib/services/entity-service/__tests__/entity-service.test.js+1 −3 modified@@ -62,7 +62,7 @@ describe('Entity service', () => { const fakeStrapi = { getModel: jest.fn(() => { - return { kind: 'singleType', privateAttributes: [] }; + return { kind: 'singleType' }; }), }; @@ -133,7 +133,6 @@ describe('Entity service', () => { uid: entityUID, kind: 'contentType', modelName: 'test-model', - privateAttributes: [], options: {}, attributes: { attrStringDefaultRequired: { @@ -467,7 +466,6 @@ describe('Entity service', () => { modelName: 'entity', collectionName: 'entity', uid: entityUID, - privateAttributes: [], options: {}, info: { singularName: 'entity',
packages/core/strapi/lib/services/entity-validator/__tests__/biginteger-validators.test.js+0 −1 modified@@ -27,7 +27,6 @@ describe('BigInteger validator', () => { kind: 'contentType', modelName: 'test-model', uid: 'test-uid', - privateAttributes: [], options: {}, attributes: { attrBigIntegerUnique: { type: 'biginteger', unique: true },
packages/core/strapi/lib/services/entity-validator/__tests__/datetime-validators.test.js+0 −1 modified@@ -27,7 +27,6 @@ describe('Datetime validator', () => { kind: 'contentType', modelName: 'test-model', uid: 'test-uid', - privateAttributes: [], options: {}, attributes: { attrDateTimeUnique: { type: 'datetime', unique: true },
packages/core/strapi/lib/services/entity-validator/__tests__/date-validators.test.js+0 −1 modified@@ -27,7 +27,6 @@ describe('Date validator', () => { kind: 'contentType', modelName: 'test-model', uid: 'test-uid', - privateAttributes: [], options: {}, attributes: { attrDateUnique: { type: 'date', unique: true },
packages/core/strapi/lib/services/entity-validator/__tests__/float-validators.test.js+0 −1 modified@@ -27,7 +27,6 @@ describe('Float validator', () => { kind: 'contentType', uid: 'test-uid', modelName: 'test-model', - privateAttributes: [], options: {}, attributes: { attrFloatUnique: { type: 'float', unique: true },
packages/core/strapi/lib/services/entity-validator/__tests__/integer-validators.test.js+0 −1 modified@@ -27,7 +27,6 @@ describe('Integer validator', () => { kind: 'contentType', uid: 'test-uid', modelName: 'test-model', - privateAttributes: [], options: {}, attributes: { attrIntegerUnique: { type: 'integer', unique: true },
packages/core/strapi/lib/services/entity-validator/__tests__/string-validators.test.js+0 −1 modified@@ -27,7 +27,6 @@ describe('String validator', () => { kind: 'contentType', modelName: 'test-model', uid: 'test-uid', - privateAttributes: [], options: {}, attributes: { attrStringUnique: { type: 'string', unique: true },
packages/core/strapi/lib/services/entity-validator/__tests__/timestamp-validators.test.js+0 −1 modified@@ -27,7 +27,6 @@ describe('Time validator', () => { kind: 'contentType', modelName: 'test-model', uid: 'test-uid', - privateAttributes: [], options: {}, attributes: { attrTimestampUnique: { type: 'timestamp', unique: true },
packages/core/strapi/lib/services/entity-validator/__tests__/time-validators.test.js+0 −1 modified@@ -27,7 +27,6 @@ describe('Time validator', () => { kind: 'contentType', modelName: 'test-model', uid: 'test-uid', - privateAttributes: [], options: {}, attributes: { attrTimeUnique: { type: 'time', unique: true },
packages/core/strapi/lib/services/entity-validator/__tests__/uid-validators.test.js+0 −1 modified@@ -26,7 +26,6 @@ describe('UID validator', () => { kind: 'contentType', modelName: 'test-model', uid: 'test-uid', - privateAttributes: [], options: {}, attributes: { attrUidUnique: { type: 'uid' },
packages/core/utils/lib/content-types.js+11 −4 modified@@ -1,7 +1,7 @@ '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'; @@ -92,16 +92,23 @@ 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) => {
packages/core/utils/lib/sanitize/visitors/remove-private.js+1 −1 modified@@ -7,7 +7,7 @@ module.exports = ({ schema, key, attribute }, { remove }) => { return; } - const isPrivate = isPrivateAttribute(schema, key) || attribute.private === true; + const isPrivate = attribute.private === true || isPrivateAttribute(schema, key); if (isPrivate) { remove(key);
packages/core/utils/lib/__tests__/content-types.test.js+0 −3 modified@@ -153,7 +153,6 @@ describe('Content types utils', () => { 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(); @@ -164,7 +163,6 @@ describe('Content types utils', () => { 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(); @@ -175,7 +173,6 @@ describe('Content types utils', () => { 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();
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
5- github.com/advisories/GHSA-chmr-rg2f-9jmfghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2023-34093ghsaADVISORY
- github.com/strapi/strapi/commit/2fa8f30371bfd1db44c15e5747860ee5789096deghsax_refsource_MISCWEB
- github.com/strapi/strapi/releases/tag/v4.10.8ghsax_refsource_MISCWEB
- github.com/strapi/strapi/security/advisories/GHSA-chmr-rg2f-9jmfghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.