Parse Server: Auth provider validation bypass on login via partial authData
Description
Parse Server is an open source backend that can be deployed to any infrastructure that can run Node.js. Prior to versions 8.6.52 and 9.6.0-alpha.41, an authentication bypass vulnerability allows an attacker to log in as any user who has linked a third-party authentication provider, without knowing the user's credentials. The attacker only needs to know the user's provider ID to gain full access to their account, including a valid session token. This affects Parse Server deployments where the server option allowExpiredAuthDataToken is set to true. The default value is false. This issue has been patched in versions 8.6.52 and 9.6.0-alpha.41.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
parse-servernpm | >= 9.0.0, < 9.6.0-alpha.41 | 9.6.0-alpha.41 |
parse-servernpm | < 8.6.52 | 8.6.52 |
Affected products
1- Range: < 8.6.52
Patches
298f4ba5bcf2cfix: Auth provider validation bypass on login via partial authData ([GHSA-pfj7-wv7c-22pr](https://github.com/parse-community/parse-server/security/advisories/GHSA-pfj7-wv7c-22pr)) (#10246)
7 files changed · +140 −6
DEPRECATIONS.md+1 −0 modified@@ -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."
spec/vulnerabilities.spec.js+127 −0 modified@@ -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(); + }); + }); });
src/Deprecator/Deprecations.js+5 −0 modified@@ -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: "Auth providers are always validated on login regardless of this setting. Set 'allowExpiredAuthDataToken' to 'false' or remove the option to accept the future removal.", + }, ];
src/Options/Definitions.js+1 −1 modified@@ -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, },
src/Options/docs.js+1 −1 modified@@ -15,7 +15,7 @@ * @property {AccountLockoutOptions} accountLockout The account lockout policy for failed login attempts.<br><br>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>} analyticsAdapter Adapter module for the analytics
src/Options/index.js+1 −1 modified@@ -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.
src/RestWrite.js+4 −3 modified@@ -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,
8d7df5639c4afix: Auth provider validation bypass on login via partial authData ([GHSA-pfj7-wv7c-22pr](https://github.com/parse-community/parse-server/security/advisories/GHSA-pfj7-wv7c-22pr)) (#10247)
7 files changed · +140 −6
DEPRECATIONS.md+1 −0 modified@@ -16,6 +16,7 @@ The following is a list of deprecations, according to the [Deprecation Policy](h | DEPPS10 | Encode `Parse.Object` in Cloud Function and remove option `encodeParseObjectInCloudFunction` | [#8634](https://github.com/parse-community/parse-server/issues/8634) | 6.2.0 (2023) | 9.0.0 (2026) | deprecated | - | | DEPPS11 | Replace `PublicAPIRouter` with `PagesRouter` | [#7625](https://github.com/parse-community/parse-server/issues/7625) | 8.0.0 (2025) | 9.0.0 (2026) | deprecated | - | | DEPPS12 | Database option `allowPublicExplain` will default to `true` | [#7519](https://github.com/parse-community/parse-server/issues/7519) | 8.5.0 (2025) | 9.0.0 (2026) | deprecated | - | +| DEPPS20 | Remove config option `allowExpiredAuthDataToken` | | 8.6.0 (2026) | 9.0.0 (2026) | deprecated | - | [i_deprecation]: ## "The version and date of the deprecation." [i_removal]: ## "The version and date of the planned removal."
spec/vulnerabilities.spec.js+127 −0 modified@@ -2987,4 +2987,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 identical', 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 identical authData + 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', access_token: 'valid_token' } }, + }), + }); + // On update with allowExpiredAuthDataToken: true, identical data skips validation + expect(validatorSpy).not.toHaveBeenCalled(); + }); + }); });
src/Deprecator/Deprecations.js+5 −0 modified@@ -19,4 +19,9 @@ module.exports = [ { optionKey: 'encodeParseObjectInCloudFunction', changeNewDefault: 'true' }, { optionKey: 'enableInsecureAuthAdapters', changeNewDefault: 'false' }, { optionKey: 'databaseOptions.allowPublicExplain', changeNewDefault: 'false' }, + { + optionKey: 'allowExpiredAuthDataToken', + changeNewKey: '', + 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.", + }, ];
src/Options/Definitions.js+1 −1 modified@@ -78,7 +78,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`.', + '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, },
src/Options/docs.js+1 −1 modified@@ -15,7 +15,7 @@ * @property {AccountLockoutOptions} accountLockout The account lockout policy for failed login attempts. * @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>} analyticsAdapter Adapter module for the analytics
src/Options/index.js+1 −1 modified@@ -347,7 +347,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.
src/RestWrite.js+4 −3 modified@@ -632,9 +632,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,
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
7- github.com/advisories/GHSA-pfj7-wv7c-22prghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2026-33409ghsaADVISORY
- github.com/parse-community/parse-server/commit/8d7df5639c4a35768fe8b78b4580b30e8a74721cghsax_refsource_MISCWEB
- github.com/parse-community/parse-server/commit/98f4ba5bcf2c199bfe6225f672e8edcd08ba732dghsax_refsource_MISCWEB
- github.com/parse-community/parse-server/pull/10246ghsax_refsource_MISCWEB
- github.com/parse-community/parse-server/pull/10247ghsax_refsource_MISCWEB
- github.com/parse-community/parse-server/security/advisories/GHSA-pfj7-wv7c-22prghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.