From 146c5d5c9ff3cb9f804b9005a9bbd0cb0af47429 Mon Sep 17 00:00:00 2001
From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com>
Date: Thu, 19 Mar 2026 17:08:21 +0000
Subject: [PATCH 1/2] fix: Auth provider validation bypass on login via partial
authData (GHSA-pfj7-wv7c-22pr)
---
DEPRECATIONS.md | 1 +
spec/vulnerabilities.spec.js | 127 +++++++++++++++++++++++++++++++++
src/Deprecator/Deprecations.js | 5 ++
src/RestWrite.js | 7 +-
4 files changed, 137 insertions(+), 3 deletions(-)
diff --git a/DEPRECATIONS.md b/DEPRECATIONS.md
index cd001e7162..b1593c224a 100644
--- a/DEPRECATIONS.md
+++ b/DEPRECATIONS.md
@@ -23,6 +23,7 @@ The following is a list of deprecations, according to the [Deprecation Policy](h
| DEPPS17 | Remove config option `playgroundPath` | [#10110](https://github.com/parse-community/parse-server/issues/10110) | 9.5.0 (2026) | 10.0.0 (2027) | deprecated | - |
| DEPPS18 | Config option `requestComplexity` limits enabled by default | [#10207](https://github.com/parse-community/parse-server/pull/10207) | 9.6.0 (2026) | 10.0.0 (2027) | deprecated | - |
| DEPPS19 | Remove config option `enableProductPurchaseLegacyApi` | [#10228](https://github.com/parse-community/parse-server/pull/10228) | 9.6.0 (2026) | 10.0.0 (2027) | deprecated | - |
+| DEPPS20 | Remove config option `allowExpiredAuthDataToken` | | 9.6.0 (2026) | 10.0.0 (2027) | deprecated | - |
[i_deprecation]: ## "The version and date of the deprecation."
[i_change]: ## "The version and date of the planned change."
diff --git a/spec/vulnerabilities.spec.js b/spec/vulnerabilities.spec.js
index 5b82e2bb9c..f2bd2c3bdc 100644
--- a/spec/vulnerabilities.spec.js
+++ b/spec/vulnerabilities.spec.js
@@ -3221,4 +3221,131 @@ describe('(GHSA-5hmj-jcgp-6hff) Protected fields leak via LiveQuery afterEvent t
obj.save({ publicField: 'changed' }, { useMasterKey: true }),
]);
});
+
+ describe('(GHSA-pfj7-wv7c-22pr) AuthData subset validation bypass with allowExpiredAuthDataToken', () => {
+ let validatorSpy;
+
+ const testAdapter = {
+ validateAppId: () => Promise.resolve(),
+ validateAuthData: () => Promise.resolve(),
+ };
+
+ beforeEach(async () => {
+ validatorSpy = spyOn(testAdapter, 'validateAuthData').and.resolveTo({});
+ await reconfigureServer({
+ auth: { testAdapter },
+ allowExpiredAuthDataToken: true,
+ });
+ });
+
+ it('validates authData on login when incoming data is a strict subset of stored data', async () => {
+ // Sign up a user with full authData (id + access_token)
+ const user = new Parse.User();
+ await user.save({
+ authData: { testAdapter: { id: 'user123', access_token: 'valid_token' } },
+ });
+ validatorSpy.calls.reset();
+
+ // Attempt to log in with only the id field (subset of stored data)
+ const res = await request({
+ method: 'POST',
+ url: 'http://localhost:8378/1/users',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'X-Parse-Application-Id': 'test',
+ 'X-Parse-REST-API-Key': 'rest',
+ },
+ body: JSON.stringify({
+ authData: { testAdapter: { id: 'user123' } },
+ }),
+ });
+ expect(res.data.objectId).toBe(user.id);
+ // The adapter MUST be called to validate the login attempt
+ expect(validatorSpy).toHaveBeenCalled();
+ });
+
+ it('prevents account takeover via partial authData when allowExpiredAuthDataToken is enabled', async () => {
+ // Sign up a user with full authData
+ const user = new Parse.User();
+ await user.save({
+ authData: { testAdapter: { id: 'victim123', access_token: 'secret_token' } },
+ });
+ validatorSpy.calls.reset();
+
+ // Simulate an attacker sending only the provider ID (no access_token)
+ // The adapter should reject this because the token is missing
+ validatorSpy.and.rejectWith(
+ new Parse.Error(Parse.Error.SCRIPT_FAILED, 'Invalid credentials')
+ );
+
+ const res = await request({
+ method: 'POST',
+ url: 'http://localhost:8378/1/users',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'X-Parse-Application-Id': 'test',
+ 'X-Parse-REST-API-Key': 'rest',
+ },
+ body: JSON.stringify({
+ authData: { testAdapter: { id: 'victim123' } },
+ }),
+ }).catch(e => e);
+
+ // Login must be rejected — adapter validation must not be skipped
+ expect(res.status).toBe(400);
+ expect(validatorSpy).toHaveBeenCalled();
+ });
+
+ it('validates authData on login even when authData is identical', async () => {
+ // Sign up with full authData
+ const user = new Parse.User();
+ await user.save({
+ authData: { testAdapter: { id: 'user456', access_token: 'expired_token' } },
+ });
+ validatorSpy.calls.reset();
+
+ // Log in with the exact same authData (all keys present, same values)
+ const res = await request({
+ method: 'POST',
+ url: 'http://localhost:8378/1/users',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'X-Parse-Application-Id': 'test',
+ 'X-Parse-REST-API-Key': 'rest',
+ },
+ body: JSON.stringify({
+ authData: { testAdapter: { id: 'user456', access_token: 'expired_token' } },
+ }),
+ });
+ expect(res.data.objectId).toBe(user.id);
+ // Auth providers are always validated on login regardless of allowExpiredAuthDataToken
+ expect(validatorSpy).toHaveBeenCalled();
+ });
+
+ it('skips validation on update when authData is a subset of stored data', async () => {
+ // Sign up with full authData
+ const user = new Parse.User();
+ await user.save({
+ authData: { testAdapter: { id: 'user789', access_token: 'valid_token' } },
+ });
+ validatorSpy.calls.reset();
+
+ // Update the user with a subset of authData (simulates afterFind stripping fields)
+ await request({
+ method: 'PUT',
+ url: `http://localhost:8378/1/users/${user.id}`,
+ headers: {
+ 'Content-Type': 'application/json',
+ 'X-Parse-Application-Id': 'test',
+ 'X-Parse-REST-API-Key': 'rest',
+ 'X-Parse-Session-Token': user.getSessionToken(),
+ },
+ body: JSON.stringify({
+ authData: { testAdapter: { id: 'user789' } },
+ }),
+ });
+ // On update with allowExpiredAuthDataToken: true, subset data skips validation
+ expect(validatorSpy).not.toHaveBeenCalled();
+ });
+ });
});
diff --git a/src/Deprecator/Deprecations.js b/src/Deprecator/Deprecations.js
index 18baa00927..0f811ce79e 100644
--- a/src/Deprecator/Deprecations.js
+++ b/src/Deprecator/Deprecations.js
@@ -76,4 +76,9 @@ module.exports = [
changeNewKey: '',
solution: "The product purchase API is an undocumented, unmaintained legacy feature that may not function as expected and will be removed in a future major version. We strongly advise against using it. Set 'enableProductPurchaseLegacyApi' to 'false' to disable it, or remove the option to accept the future removal.",
},
+ {
+ optionKey: 'allowExpiredAuthDataToken',
+ changeNewKey: '',
+ solution: "This option has a limited effect since auth providers are now always validated on login regardless of this setting. Set 'allowExpiredAuthDataToken' to 'false' or remove the option to accept the future removal.",
+ },
];
diff --git a/src/RestWrite.js b/src/RestWrite.js
index e986a35c67..80b233ec59 100644
--- a/src/RestWrite.js
+++ b/src/RestWrite.js
@@ -639,9 +639,10 @@ RestWrite.prototype.handleAuthData = async function (authData) {
return;
}
- // Force to validate all provided authData on login
- // on update only validate mutated ones
- if (hasMutatedAuthData || !this.config.allowExpiredAuthDataToken) {
+ // Always validate all provided authData on login to prevent authentication
+ // bypass via partial authData (e.g. sending only the provider ID without
+ // an access token); on update only validate mutated ones
+ if (isLogin || hasMutatedAuthData || !this.config.allowExpiredAuthDataToken) {
const res = await Auth.handleAuthDataValidation(
isLogin ? authData : mutatedAuthData,
this,
From e7c0ad67a7683b34e8e0a48800f9b83808119ea8 Mon Sep 17 00:00:00 2001
From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com>
Date: Thu, 19 Mar 2026 17:33:19 +0000
Subject: [PATCH 2/2] fix: Update allowExpiredAuthDataToken option description
and deprecation notice
---
src/Deprecator/Deprecations.js | 2 +-
src/Options/Definitions.js | 2 +-
src/Options/docs.js | 2 +-
src/Options/index.js | 2 +-
4 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/src/Deprecator/Deprecations.js b/src/Deprecator/Deprecations.js
index 0f811ce79e..dff88c78bb 100644
--- a/src/Deprecator/Deprecations.js
+++ b/src/Deprecator/Deprecations.js
@@ -79,6 +79,6 @@ module.exports = [
{
optionKey: 'allowExpiredAuthDataToken',
changeNewKey: '',
- solution: "This option has a limited effect since auth providers are now always validated on login regardless of this setting. Set 'allowExpiredAuthDataToken' to 'false' or remove the option to accept the future removal.",
+ solution: "Auth providers are always validated on login regardless of this setting. Set 'allowExpiredAuthDataToken' to 'false' or remove the option to accept the future removal.",
},
];
diff --git a/src/Options/Definitions.js b/src/Options/Definitions.js
index af9279d712..26c50a5d3f 100644
--- a/src/Options/Definitions.js
+++ b/src/Options/Definitions.js
@@ -72,7 +72,7 @@ module.exports.ParseServerOptions = {
},
allowExpiredAuthDataToken: {
env: 'PARSE_SERVER_ALLOW_EXPIRED_AUTH_DATA_TOKEN',
- help: 'Allow a user to log in even if the 3rd party authentication token that was used to sign in to their account has expired. If this is set to `false`, then the token will be validated every time the user signs in to their account. This refers to the token that is stored in the `_User.authData` field. Defaults to `false`.',
+ help: 'Deprecated. This option will be removed in a future version. Auth providers are always validated on login. On update, if this is set to `true`, auth providers are only re-validated when the auth data has changed. If this is set to `false`, auth providers are re-validated on every update. Defaults to `false`.',
action: parsers.booleanParser,
default: false,
},
diff --git a/src/Options/docs.js b/src/Options/docs.js
index 20d74fb770..082f9cba38 100644
--- a/src/Options/docs.js
+++ b/src/Options/docs.js
@@ -15,7 +15,7 @@
* @property {AccountLockoutOptions} accountLockout The account lockout policy for failed login attempts.
Note: Setting a user's ACL to an empty object `{}` via master key is a separate mechanism that only prevents new logins; it does not invalidate existing session tokens. To immediately revoke a user's access, destroy their sessions via master key in addition to setting the ACL.
* @property {Boolean} allowClientClassCreation Enable (or disable) client class creation, defaults to false
* @property {Boolean} allowCustomObjectId Enable (or disable) custom objectId
- * @property {Boolean} allowExpiredAuthDataToken Allow a user to log in even if the 3rd party authentication token that was used to sign in to their account has expired. If this is set to `false`, then the token will be validated every time the user signs in to their account. This refers to the token that is stored in the `_User.authData` field. Defaults to `false`.
+ * @property {Boolean} allowExpiredAuthDataToken Deprecated. This option will be removed in a future version. Auth providers are always validated on login. On update, if this is set to `true`, auth providers are only re-validated when the auth data has changed. If this is set to `false`, auth providers are re-validated on every update. Defaults to `false`.
* @property {String[]} allowHeaders Add headers to Access-Control-Allow-Headers
* @property {String|String[]} allowOrigin Sets origins for Access-Control-Allow-Origin. This can be a string for a single origin or an array of strings for multiple origins.
* @property {Adapter} analyticsAdapter Adapter module for the analytics
diff --git a/src/Options/index.js b/src/Options/index.js
index 459494cdab..8707d4c87a 100644
--- a/src/Options/index.js
+++ b/src/Options/index.js
@@ -379,7 +379,7 @@ export interface ParseServerOptions {
/* Set to true if new users should be created without public read and write access.
:DEFAULT: true */
enforcePrivateUsers: ?boolean;
- /* Allow a user to log in even if the 3rd party authentication token that was used to sign in to their account has expired. If this is set to `false`, then the token will be validated every time the user signs in to their account. This refers to the token that is stored in the `_User.authData` field. Defaults to `false`.
+ /* Deprecated. This option will be removed in a future version. Auth providers are always validated on login. On update, if this is set to `true`, auth providers are only re-validated when the auth data has changed. If this is set to `false`, auth providers are re-validated on every update. Defaults to `false`.
:DEFAULT: false */
allowExpiredAuthDataToken: ?boolean;
/* An array of keys and values that are prohibited in database read and write requests to prevent potential security vulnerabilities. It is possible to specify only a key (`{"key":"..."}`), only a value (`{"value":"..."}`) or a key-value pair (`{"key":"...","value":"..."}`). The specification can use the following types: `boolean`, `numeric` or `string`, where `string` will be interpreted as a regex notation. Request data is deep-scanned for matching definitions to detect also any nested occurrences. Defaults are patterns that are likely to be used in malicious requests. Setting this option will override the default patterns.