Data Exposure in Strapi (Typescript)
Data Exposure in Strapi with Typescript — how this specific combination creates or exposes the vulnerability
Strapi is a headless CMS that exposes content through REST or GraphQL APIs. When built with Typescript, developers often define entity interfaces and service methods with strong typing, but misconfigurations in route policies or controller logic can still lead to unintended data exposure. A common issue occurs when developers rely solely on Typescript interfaces for data shaping without enforcing server-side access controls, assuming that client-side filtering or TypeScript types prevent over-fetching.
For example, a Strapi content type like 'Article' might have sensitive fields such as 'internalNotes' or 'authorId' that should not be exposed to public endpoints. If the route configuration for GET /articles does not explicitly restrict which fields are returned, Strapi will return all fields defined in the model — including those marked as private — unless privateAttributes is set in the model schema or sanitizeEntity is used in the controller.
In a Typescript Strapi project, a developer might define an interface like:
interface Article {
id: number;
title: string;
content: string;
internalNotes?: string; // Should not be exposed
authorId: number;
createdAt: string;
updatedAt: string;
}
And then create a service method that returns raw entity data:
// src/api/article/services/article.ts
import { factory } from '@strapi/strapi'
export default factory.createService(({ strapi }) => ({
async find() {
const entities = await strapi.entityService.findMany('api::article.article', {});
return entities; // Returns all fields, including internalNotes
}
}));
Even though Typescript ensures the Article interface is respected in development, at runtime Strapi serializes the full database entity. Without explicit field selection or sanitization, the API exposes internalNotes to anyone who can access the endpoint — a classic data exposure vulnerability (OWASP API1:2023 – Broken Object Property Level Authorization).
This risk is amplified in Strapi because its default behavior favors developer convenience over security: controllers return full entities unless restricted. Typescript does not prevent this; it only provides compile-time safety. The vulnerability exists in the gap between compile-time assurances and runtime data exposure.
Typescript-Specific Remediation in Strapi — concrete code fixes
To fix data exposure in a Strapi application using Typescript, you must enforce field-level access controls at the controller or service level, using Strapi’s built-in sanitization utilities. Relying on Typescript interfaces alone is insufficient; you must explicitly strip or omit sensitive fields before sending the response.
One effective approach is to use sanitizeEntity (or sanitizeOutput in Strapi v4) within your service or controller to remove private attributes based on the model definition. First, ensure your model’s schema marks sensitive fields as private:
// src/api/article/content-types/article/schema.json
{
"kind": "collectionType",
"collectionName": "articles",
"info": {
"singularName": "article",
"pluralName": "articles",
"displayName": "Article"
},
"options": {
"draftAndPublish": true
},
"attributes": {
"title": {
"type": "string"
},
"content": {
"type": "text"
},
"internalNotes": {
"type": "text",
"private": true // Marks field as private
},
"authorId": {
"type": "integer"
},
"createdAt": {
"type": "datetime"
},
"updatedAt": {
"type": "datetime"
}
}
}
Then, in your service, use sanitizeEntity to strip private fields:
// src/api/article/services/article.ts
import { factory } from '@strapi/strapi'
import { sanitizeEntity } from '@strapi/utils'
export default factory.createService(({ strapi }) => ({
async find() {
const entities = await strapi.entityService.findMany('api::article.article', {});
return entities.map(entity =>
sanitizeEntity(entity, { model: strapi.getModel('api::article.article') })
);
}
}));
Alternatively, if you need fine-grained control (e.g., exposing different fields based on user role), you can manually construct the response using Typescript interfaces to ensure type safety:
interface PublicArticle {
id: number;
title: string;
content: string;
authorId: number;
createdAt: string;
updatedAt: string;
}
// Inside service method
const sanitized = entities.map(entity => ({
id: entity.id,
title: entity.title,
content: entity.content,
authorId: entity.authorId,
createdAt: entity.createdAt,
updatedAt: entity.updatedAt
} as PublicArticle));
return sanitized;
This approach guarantees that only intended fields are returned, and the as PublicArticle assertion ensures Typescript validates the shape at compile time. Combine this with route-level policies (<Policy> <Policy> in routes.json) to enforce authentication where needed, but always sanitize data at the service or controller layer to prevent exposure regardless of how the endpoint is accessed.
Related CWEs: dataExposure
| CWE ID | Name | Severity |
|---|---|---|
| CWE-200 | Exposure of Sensitive Information | HIGH |
| CWE-209 | Error Information Disclosure | MEDIUM |
| CWE-213 | Exposure of Sensitive Information Due to Incompatible Policies | HIGH |
| CWE-215 | Insertion of Sensitive Information Into Debugging Code | MEDIUM |
| CWE-312 | Cleartext Storage of Sensitive Information | HIGH |
| CWE-359 | Exposure of Private Personal Information (PII) | HIGH |
| CWE-522 | Insufficiently Protected Credentials | CRITICAL |
| CWE-532 | Insertion of Sensitive Information into Log File | MEDIUM |
| CWE-538 | Insertion of Sensitive Information into Externally-Accessible File | HIGH |
| CWE-540 | Inclusion of Sensitive Information in Source Code | HIGH |
Frequently Asked Questions
Does using Typescript in Strapi automatically prevent data exposure?
sanitizeEntity or manually construct the response to exclude private attributes.