CVE-2020-15270: Operation on a Resource after Expiration or Release (CWE-672)
value: Parse Server (npm package parse-server) broadcasts events to all clients without checking if the session token is valid. This allows clients with expired sessions to still receive subscription objects. It is not possible to create subscription objects with invalid session tokens. The issue is not patched.
CWE description: The product uses, accesses, or otherwise operates on a resource after that resource has been expired, released, or revoked.
CVSS v3 base score: 4.3
Published: 2020-10-22T22:15Z
Repository: https://github.com/parse-community/parse-server
Language: JavaScript
Files changed: spec/ParseLiveQuery.spec.js, src/LiveQuery/ParseLiveQueryServer.js, src/Options/Definitions.js, src/Options/docs.js, src/Options/index.js, src/ParseServer.js
Commit date: 2020-10-21 16:32:07 -0700
Commit message: Merge pull request from GHSA-2xm2-xj2q-qgpj

* Test case and fixes

* Change requestTimeout default to 5s

* Document new function argument
Diff: diff --git a/spec/ParseLiveQuery.spec.js b/spec/ParseLiveQuery.spec.js
index 8192597a..bb98306e 100644
--- a/spec/ParseLiveQuery.spec.js
+++ b/spec/ParseLiveQuery.spec.js
@@ -782,10 +782,52 @@ describe('ParseLiveQuery', function () {
           );
         });
     });
   });
 
+  it('should not broadcast event to client with invalid session token - avisory GHSA-2xm2-xj2q-qgpj', async done => {
+    await reconfigureServer({
+      liveQuery: {
+        classNames: ['TestObject'],
+      },
+      liveQueryServerOptions: {
+        cacheTimeout: 100,
+      },
+      startLiveQueryServer: true,
+      verbose: false,
+      silent: true,
+      cacheTTL: 100,
+    });
+    const user = new Parse.User();
+    user.setUsername('username');
+    user.setPassword('password');
+    await user.signUp();
+    const obj1 = new Parse.Object('TestObject');
+    const obj1ACL = new Parse.ACL();
+    obj1ACL.setPublicReadAccess(false);
+    obj1ACL.setReadAccess(user, true);
+    obj1.setACL(obj1ACL);
+    const obj2 = new Parse.Object('TestObject');
+    const obj2ACL = new Parse.ACL();
+    obj2ACL.setPublicReadAccess(false);
+    obj2ACL.setReadAccess(user, true);
+    obj2.setACL(obj2ACL);
+    const query = new Parse.Query('TestObject');
+    const subscription = await query.subscribe();
+    subscription.on('create', obj => {
+      if (obj.id !== obj1.id) {
+        done.fail('should not fire');
+      }
+    });
+    await obj1.save();
+    await Parse.User.logOut();
+    await new Promise(resolve => setTimeout(resolve, 200));
+    await obj2.save();
+    await new Promise(resolve => setTimeout(resolve, 200));
+    done();
+  });
+
   afterEach(async function (done) {
     const client = await Parse.CoreManager.getLiveQueryController().getDefaultLiveQueryClient();
     client.close();
     // Wait for live query client to disconnect
     setTimeout(() => {
diff --git a/src/LiveQuery/ParseLiveQueryServer.js b/src/LiveQuery/ParseLiveQueryServer.js
index f0d0db19..60468523 100644
--- a/src/LiveQuery/ParseLiveQueryServer.js
+++ b/src/LiveQuery/ParseLiveQueryServer.js
@@ -28,14 +28,15 @@ class ParseLiveQueryServer {
   parseWebSocketServer: Object;
   keyPairs: any;
   // The subscriber we use to get object update from publisher
   subscriber: Object;
 
-  constructor(server: any, config: any = {}) {
+  constructor(server: any, config: any = {}, parseServerConfig: any = {}) {
     this.server = server;
     this.clients = new Map();
     this.subscriptions = new Map();
+    this.config = config;
 
     config.appId = config.appId || Parse.applicationId;
     config.masterKey = config.masterKey || Parse.masterKey;
 
     // Store keys, convert obj to map
@@ -52,17 +53,19 @@ class ParseLiveQueryServer {
     Parse.serverURL = serverURL;
     Parse.initialize(config.appId, Parse.javaScriptKey, config.masterKey);
 
     // The cache controller is a proper cache controller
     // with access to User and Roles
-    this.cacheController = getCacheController(config);
+    this.cacheController = getCacheController(parseServerConfig);
+
+    config.cacheTimeout = config.cacheTimeout || 5 * 1000; // 5s
 
     // This auth cache stores the promises for each auth resolution.
     // The main benefit is to be able to reuse the same user / session token resolution.
     this.authCache = new LRU({
       max: 500, // 500 concurrent
-      maxAge: 60 * 60 * 1000, // 1h
+      maxAge: config.cacheTimeout,
     });
     // Initialize websocket server
     this.parseWebSocketServer = new ParseWebSocketServer(
       server,
       parseWebsocket => this._onConnect(parseWebsocket),
@@ -508,16 +511,15 @@ class ParseLiveQueryServer {
       })
       .catch(error => {
         // There was an error with the session token
         const result = {};
         if (error && error.code === Parse.Error.INVALID_SESSION_TOKEN) {
-          // Store a resolved promise with the error for 10 minutes
           result.error = error;
           this.authCache.set(
             sessionToken,
             Promise.resolve(result),
-            60 * 10 * 1000
+            this.config.cacheTimeout
           );
         } else {
           this.authCache.del(sessionToken);
         }
         return result;
diff --git a/src/Options/Definitions.js b/src/Options/Definitions.js
index 8eaf6d77..1d4f2b21 100644
--- a/src/Options/Definitions.js
+++ b/src/Options/Definitions.js
@@ -476,11 +476,11 @@ module.exports.LiveQueryServerOptions = {
       'This string should match the appId in use by your Parse Server. If you deploy the LiveQuery server alongside Parse Server, the LiveQuery server will try to use the same appId.',
   },
   cacheTimeout: {
     env: 'PARSE_LIVE_QUERY_SERVER_CACHE_TIMEOUT',
     help:
-      "Number in milliseconds. When clients provide the sessionToken to the LiveQuery server, the LiveQuery server will try to fetch its ParseUser's objectId from parse server and store it in the cache. The value defines the duration of the cache. Check the following Security section and our protocol specification for details, defaults to 30 * 24 * 60 * 60 * 1000 ms (~30 days).",
+      "Number in milliseconds. When clients provide the sessionToken to the LiveQuery server, the LiveQuery server will try to fetch its ParseUser's objectId from parse server and store it in the cache. The value defines the duration of the cache. Check the following Security section and our protocol specification for details, defaults to 5 * 1000 ms (5 seconds).",
     action: parsers.numberParser('cacheTimeout'),
   },
   keyPairs: {
     env: 'PARSE_LIVE_QUERY_SERVER_KEY_PAIRS',
     help:
diff --git a/src/Options/docs.js b/src/Options/docs.js
index 70f5e071..febe3d77 100644
--- a/src/Options/docs.js
+++ b/src/Options/docs.js
@@ -98,11 +98,11 @@
  */
 
 /**
  * @interface LiveQueryServerOptions
  * @property {String} appId This string should match the appId in use by your Parse Server. If you deploy the LiveQuery server alongside Parse Server, the LiveQuery server will try to use the same appId.
- * @property {Number} cacheTimeout Number in milliseconds. When clients provide the sessionToken to the LiveQuery server, the LiveQuery server will try to fetch its ParseUser's objectId from parse server and store it in the cache. The value defines the duration of the cache. Check the following Security section and our protocol specification for details, defaults to 30 * 24 * 60 * 60 * 1000 ms (~30 days).
+ * @property {Number} cacheTimeout Number in milliseconds. When clients provide the sessionToken to the LiveQuery server, the LiveQuery server will try to fetch its ParseUser's objectId from parse server and store it in the cache. The value defines the duration of the cache. Check the following Security section and our protocol specification for details, defaults to 5 * 1000 ms (5 seconds).
  * @property {Any} keyPairs A JSON object that serves as a whitelist of keys. It is used for validating clients when they try to connect to the LiveQuery server. Check the following Security section and our protocol specification for details.
  * @property {String} logLevel This string defines the log level of the LiveQuery server. We support VERBOSE, INFO, ERROR, NONE, defaults to INFO.
  * @property {String} masterKey This string should match the masterKey in use by your Parse Server. If you deploy the LiveQuery server alongside Parse Server, the LiveQuery server will try to use the same masterKey.
  * @property {Number} port The port to run the LiveQuery server, defaults to 1337.
  * @property {Adapter<PubSubAdapter>} pubSubAdapter LiveQuery pubsub adapter
diff --git a/src/Options/index.js b/src/Options/index.js
index 0e97d84b..1283f849 100644
--- a/src/Options/index.js
+++ b/src/Options/index.js
@@ -258,11 +258,11 @@ export interface LiveQueryServerOptions {
   serverURL: ?string;
   /* A JSON object that serves as a whitelist of keys. It is used for validating clients when they try to connect to the LiveQuery server. Check the following Security section and our protocol specification for details.*/
   keyPairs: ?any;
   /* Number of milliseconds between ping/pong frames. The WebSocket server sends ping/pong frames to the clients to keep the WebSocket alive. This value defines the interval of the ping/pong frame from the server to clients, defaults to 10 * 1000 ms (10 s).*/
   websocketTimeout: ?number;
-  /* Number in milliseconds. When clients provide the sessionToken to the LiveQuery server, the LiveQuery server will try to fetch its ParseUser's objectId from parse server and store it in the cache. The value defines the duration of the cache. Check the following Security section and our protocol specification for details, defaults to 30 * 24 * 60 * 60 * 1000 ms (~30 days).*/
+  /* Number in milliseconds. When clients provide the sessionToken to the LiveQuery server, the LiveQuery server will try to fetch its ParseUser's objectId from parse server and store it in the cache. The value defines the duration of the cache. Check the following Security section and our protocol specification for details, defaults to 5 * 1000 ms (5 seconds).*/
   cacheTimeout: ?number;
   /* This string defines the log level of the LiveQuery server. We support VERBOSE, INFO, ERROR, NONE, defaults to INFO.*/
   logLevel: ?string;
   /* The port to run the LiveQuery server, defaults to 1337.
   :DEFAULT: 1337 */
diff --git a/src/ParseServer.js b/src/ParseServer.js
index f7d4e6ab..383ad467 100644
--- a/src/ParseServer.js
+++ b/src/ParseServer.js
@@ -296,11 +296,12 @@ class ParseServer {
     this.server = server;
 
     if (options.startLiveQueryServer || options.liveQueryServerOptions) {
       this.liveQueryServer = ParseServer.createLiveQueryServer(
         server,
-        options.liveQueryServerOptions
+        options.liveQueryServerOptions,
+        options
       );
     }
     /* istanbul ignore next */
     if (!process.env.TESTING) {
       configureListeners(this);
@@ -322,20 +323,25 @@ class ParseServer {
 
   /**
    * Helper method to create a liveQuery server
    * @static
    * @param {Server} httpServer an optional http server to pass
-   * @param {LiveQueryServerOptions} config options fot he liveQueryServer
+   * @param {LiveQueryServerOptions} config options for the liveQueryServer
+   * @param {ParseServerOptions} options options for the ParseServer
    * @returns {ParseLiveQueryServer} the live query server instance
    */
-  static createLiveQueryServer(httpServer, config: LiveQueryServerOptions) {
+  static createLiveQueryServer(
+    httpServer,
+    config: LiveQueryServerOptions,
+    options: ParseServerOptions
+  ) {
     if (!httpServer || (config && config.port)) {
       var app = express();
       httpServer = require('http').createServer(app);
       httpServer.listen(config.port);
     }
-    return new ParseLiveQueryServer(httpServer, config);
+    return new ParseLiveQueryServer(httpServer, config, options);
   }
 
   static verifyServerUrl(callback) {
     // perform a health check on the serverURL value
     if (Parse.serverURL) {

Vulnerable code:   constructor(server: any, config: any = {}) {
    this.cacheController = getCacheController(config);
      maxAge: 60 * 60 * 1000, // 1h
          // Store a resolved promise with the error for 10 minutes
            60 * 10 * 1000
      "Number in milliseconds. When clients provide the sessionToken to the LiveQuery server, the LiveQuery server will try to fetch its ParseUser's objectId from parse server and store it in the cache. The value defines the duration of the cache. Check the following Security section and our protocol specification for details, defaults to 30 * 24 * 60 * 60 * 1000 ms (~30 days).",
 * @property {Number} cacheTimeout Number in milliseconds. When clients provide the sessionToken to the LiveQuery server, the LiveQuery server will try to fetch its ParseUser's objectId from parse server and store it in the cache. The value defines the duration of the cache. Check the following Security section and our protocol specification for details, defaults to 30 * 24 * 60 * 60 * 1000 ms (~30 days).
  /* Number in milliseconds. When clients provide the sessionToken to the LiveQuery server, the LiveQuery server will try to fetch its ParseUser's objectId from parse server and store it in the cache. The value defines the duration of the cache. Check the following Security section and our protocol specification for details, defaults to 30 * 24 * 60 * 60 * 1000 ms (~30 days).*/
        options.liveQueryServerOptions
   * @param {LiveQueryServerOptions} config options fot he liveQueryServer
  static createLiveQueryServer(httpServer, config: LiveQueryServerOptions) {
    return new ParseLiveQueryServer(httpServer, config);
Fixed code:   it('should not broadcast event to client with invalid session token - avisory GHSA-2xm2-xj2q-qgpj', async done => {
    await reconfigureServer({
      liveQuery: {
        classNames: ['TestObject'],
      },
      liveQueryServerOptions: {
        cacheTimeout: 100,
      },
      startLiveQueryServer: true,
      verbose: false,
      silent: true,
      cacheTTL: 100,
    });
    const user = new Parse.User();
    user.setUsername('username');
    user.setPassword('password');
    await user.signUp();
    const obj1 = new Parse.Object('TestObject');
    const obj1ACL = new Parse.ACL();
    obj1ACL.setPublicReadAccess(false);
    obj1ACL.setReadAccess(user, true);
    obj1.setACL(obj1ACL);
    const obj2 = new Parse.Object('TestObject');
    const obj2ACL = new Parse.ACL();
    obj2ACL.setPublicReadAccess(false);
    obj2ACL.setReadAccess(user, true);
    obj2.setACL(obj2ACL);
    const query = new Parse.Query('TestObject');
    const subscription = await query.subscribe();
    subscription.on('create', obj => {
      if (obj.id !== obj1.id) {
        done.fail('should not fire');
      }
    });
    await obj1.save();
    await Parse.User.logOut();
    await new Promise(resolve => setTimeout(resolve, 200));
    await obj2.save();
    await new Promise(resolve => setTimeout(resolve, 200));
    done();
  });

  constructor(server: any, config: any = {}, parseServerConfig: any = {}) {
    this.config = config;
    this.cacheController = getCacheController(parseServerConfig);

    config.cacheTimeout = config.cacheTimeout || 5 * 1000; // 5s
      maxAge: config.cacheTimeout,
            this.config.cacheTimeout
      "Number in milliseconds. When clients provide the sessionToken to the LiveQuery server, the LiveQuery server will try to fetch its ParseUser's objectId from parse server and store it in the cache. The value defines the duration of the cache. Check the following Security section and our protocol specification for details, defaults to 5 * 1000 ms (5 seconds).",
 * @property {Number} cacheTimeout Number in milliseconds. When clients provide the sessionToken to the LiveQuery server, the LiveQuery server will try to fetch its ParseUser's objectId from parse server and store it in the cache. The value defines the duration of the cache. Check the following Security section and our protocol specification for details, defaults to 5 * 1000 ms (5 seconds).
  /* Number in milliseconds. When clients provide the sessionToken to the LiveQuery server, the LiveQuery server will try to fetch its ParseUser's objectId from parse server and store it in the cache. The value defines the duration of the cache. Check the following Security section and our protocol specification for details, defaults to 5 * 1000 ms (5 seconds).*/
        options.liveQueryServerOptions,
        options
   * @param {LiveQueryServerOptions} config options for the liveQueryServer
   * @param {ParseServerOptions} options options for the ParseServer
  static createLiveQueryServer(
    httpServer,
    config: LiveQueryServerOptions,
    options: ParseServerOptions
  ) {
    return new ParseLiveQueryServer(httpServer, config, options);