Thanks to several community members and our internal security team, we have notified and patched five security vulnerabilities:
Per our security policy, we are performing our due diligence by publicly disclosing these vulnerabilities after careful testing, validation, and coordination with the reporting researchers. For further details of each patched vulnerability, please refer to the information below or consult the linked GitHub Advisories.
We would like to thank the following community members for their participation in our security program:
To immediately resolve all vulnerabilities detailed in this post, please update your Strapi v5 packages to the latest available release (containing all five fixes). The minimum applicable versions are:
CVSS:4.0/AV:N/AC:L/AT:N/PR:H/UI:N/VC:H/VI:H/VA:N/SC:H/SI:H/SA:N (9.3 — Critical)@strapi/content-type-builder <=5.33.1 (v5), @strapi/plugin-content-type-builder <=4.26.0 (v4)A database query injection vulnerability existed in the Strapi Content-Type Builder write API. An authenticated administrator could inject arbitrary database statements through the column.defaultTo attribute when creating or modifying a content type. Setting defaultTo as a tuple [value, { isRaw: true }] caused the value to be passed directly into Knex's db.connection.raw() during schema migration without sanitization, allowing arbitrary statement execution at the database layer. Depending on the database engine, this enabled arbitrary file read via database utility functions, denial of service via forced server crash on schema-migration error, and, on engines that permit external program execution, remote code execution against the database server.
The patch addresses this by restricting all Content-Type Builder write APIs to development mode only. Production deployments running v5.33.2 or later return 404 for requests against /content-type-builder/content-types and related endpoints, removing the network-reachable attack surface entirely.
Indicators that an instance running an unpatched version may have been exploited:
/content-type-builder/content-types from a non-internal source. Regex pattern: (POST|PUT)\s+/content-type-builder/passwd, etc, env, config)CVSS:4.0/AV:N/AC:H/AT:N/PR:H/UI:N/VC:N/VI:L/VA:N/SC:N/SI:N/SA:N (2.1 — Low)@strapi/admin and @strapi/plugin-users-permissions <=5.33.2In Strapi versions prior to 5.33.3, changing or resetting a user's password did not invalidate the user's existing refresh-token sessions by default. The refresh-token invalidation step in the users-permissions and admin authentication controllers was conditional on a caller-supplied deviceId. When a password change or reset request did not include a deviceId, no refresh tokens were revoked, leaving every prior session active.
An attacker who had previously obtained a refresh token could continue minting new access tokens after the legitimate user reset their password, allowing persistent unauthorized access for the lifetime of the refresh token (up to 30 days by default). Rotating credentials no longer terminates an active attacker session, defeating password reset as a containment measure.
The patch invalidates all refresh tokens associated with the user on every password change and password reset, regardless of whether a deviceId is supplied. A new device-scoped session is then issued to the caller as part of the response.
Indicators that an instance running an unpatched version may have been exploited:
POST /api/auth/refresh or POST /admin/access-token requests using a refresh token issued before the user's most recent password change. Reviewable by correlating refresh-token iat claims against password-change events in audit logsstrapi_session with created_at earlier than the user's most recent password-reset timestamp and status = 'active'CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:L/VI:L/VA:N/SC:N/SI:N/SA:N (5.3 — Medium)@strapi/upload <=5.33.2In Strapi versions prior to 5.33.3, the Upload plugin's Content API endpoints did not enforce the administrator-configured MIME type restrictions (plugin.upload.security.allowedTypes and deniedTypes). The same restrictions were correctly enforced on the Admin Panel upload path.
The upload plugin's enforceUploadSecurity security check was invoked in the admin upload controller, but was missing from the Content API controller. The Content API handlers uploadFiles and replaceFile (and the upload wrapper that dispatches to them) called the underlying upload service directly, bypassing both the magic-byte MIME detection and the configured allow/deny lists.
An authenticated user with the Content API upload permission could therefore upload file types the administrator had explicitly disallowed, including HTML and SVG content. In deployments serving uploaded files from the same origin as the admin panel (default), an attacker could upload an HTML or SVG file that, when opened directly by an admin, executed JavaScript in the admin origin, enabling admin-session hijack and authenticated administrative actions against the admin API.
The patch introduces a shared prepareUploadRequest helper that wraps enforceUploadSecurity and is called from both the Content API and admin upload controllers, ensuring identical security policy enforcement on every upload entry point.
Indicators that an instance running an unpatched version may have been exploited:
/uploads/ with extensions outside the configured allow-list, particularly .html, .htm, .svg, .js, .mjs, .xml, or .xhtml. Filesystem regex: \.(html?|svg|m?js|x?html|xml)$POST /api/upload where the uploaded file's MIME or extension is outside the configured allowedTypestext/html|application/javascript|image/svg\+xml/uploads/*.html or /uploads/*.svg shortly before unexpected administrative actions (user creation, role changes, permission modifications)CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:N/VA:N/SC:H/SI:N/SA:N (9.2 — Critical)@strapi/strapi <=5.36.1Strapi versions prior to 5.37.0 did not sufficiently sanitize query parameters when filtering content via relational fields. An unauthenticated attacker could use the where query parameter on any publicly-accessible content-type with an updatedBy (or other admin-relation) field to perform a boolean-oracle attack against private fields on the joined admin_users table, including the resetPasswordToken field. Extracting an admin reset token via this oracle made full administrative account takeover possible without authentication.
When a filter such as where[updatedBy][resetPasswordToken][$startsWith]=a was applied to a public Content API endpoint, the underlying query generation performed a LEFT JOIN against the admin_users table and emitted a WHERE clause referencing the joined column. The query parameter sanitization layer did not block operator chains that traversed into relational target schemas the caller had no read permission on, allowing the response count to be used as a one-bit oracle on any admin-table field.
The patch introduces explicit query-parameter sanitization at the controller and service boundary via three new primitives: strictParam, addQueryParams, and addBodyParams. Operator chains that traverse into restricted relational targets are now rejected before reaching the database.
Indicators that an instance running an unpatched version may have been exploited:
\?(.*&)?where\[(updatedBy|createdBy|publishedBy)\]\[(email|password|resetPasswordToken|confirmationToken|firstname|lastname|preferedLanguage)\]\[\$(startsWith|contains|eq|gt|lt|ge|le|in|notIn|notNull|null)\]=0-9, a-f) on the same content-type endpoint with progressively longer filter valuesPOST /admin/reset-password calls using a reset token that the legitimate admin did not requestwhere[updatedBy] query parametersCVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:L/SC:N/SI:N/SA:N (6.9 — Medium)@strapi/plugin-users-permissions <=5.44.0In Strapi versions prior to 5.45.0, the rate-limit middleware in the users-permissions plugin derived its rate-limit key in part from ctx.request.body.email, including on routes whose body schema does not contain an email field (/auth/local, /auth/reset-password, /auth/change-password). An unauthenticated attacker could include an arbitrary email value in the request body to obtain a fresh rate-limit key per request, effectively bypassing per-IP throttling on those routes and enabling high-volume credential brute-force, password-reset code brute-force, and credential-stuffing attempts.
The rate-limit key was constructed as ${userIdentifier}:${requestPath}:${ctx.request.ip}, where userIdentifier = ctx.request.body.email. On routes that legitimately use email as their identifier (e.g., /auth/forgot-password, /auth/local/register), this scoping is correct. On routes that use a different identifier (identifier for login, code for password reset, currentPassword for password change), the email field was not part of the route contract, but the middleware still incorporated it into the key, allowing a caller to rotate the value and obtain a unique key on every request.
The patch maintains an allow-list of routes that legitimately key on the email field and excludes that key component from every other route on which the middleware is mounted. OAuth callback paths (/connect/*) are treated identifier-less. On routes outside the allow-list, the middleware now falls back to a fixed identifier-less key, ensuring per-IP throttling remains effective even when the request body is attacker-controlled.
Indicators that an instance running an unpatched version may have been exploited:
POST requests to /api/auth/local, /api/auth/reset-password, or /api/auth/change-password from a single IP within a 5-minute window without 429 (Too Many Requests) responses/api/auth/local containing both identifier AND email fields where email varies per request. Body shape regex: "identifier"\s*:\s*"[^"]*",\s*"email"\s*:\s*"[^"]*"/api/auth/reset-password containing an unexpected email field alongside code. Body shape regex: "code"\s*:\s*"[^"]*",.*"email"\s*:We want to be transparent with our community: over the past several months, we have seen a significant increase in the volume of vulnerability reports submitted to our security program, driven largely by the proliferation of AI-assisted vulnerability scanning and report generation. While we welcome legitimate research and continue to value every submission, the reality is that roughly 92% of the reports we now receive are not valid — they describe non-issues, dependency-only concerns outside our codebase, configuration choices framed as flaws, or AI-generated reports that do not reproduce against Strapi.
Each report still has to be read, validated, and responded to by a human on our security team, and the increased volume has stretched our triage timelines. We are actively working to streamline our process, but contributors and customers should be aware that initial response and validation may take longer than they have historically. Valid, well-documented reports with clear reproduction steps continue to receive priority, and we remain deeply grateful to the researchers who invest the time to submit quality work.
We at Strapi believe in responsible disclosure. For each of the vulnerabilities in this post, we worked with the reporting security researchers to ensure that the vulnerabilities were patched before public disclosure. Once each vulnerability was patched, we added a notice to our release notes to inform users that a security issue had been resolved, and we notified our customers and partners directly via email so they could upgrade ahead of detailed public disclosure.
We are grateful to every researcher who participated in this disclosure for their professionalism and patience. Their work helps us improve the security of Strapi for the entire community.
We urge anyone who believes they have discovered a security vulnerability to assist us in responsibly disclosing it by submitting a GitHub Advisory on our main repo or by contacting our security team via security@strapi.io.
Thanks,
The Strapi Security Team