CVE-2022-24375: Uncontrolled Resource Consumption (CWE-400)
value: The package node-opcua before 2.74.0 are vulnerable to Denial of Service (DoS) when bypassing the limitations for excessive memory consumption by sending multiple CloseSession requests with the deleteSubscription parameter equal to False.
CWE description: The product does not properly control the allocation and maintenance of a limited resource, thereby enabling an actor to influence the amount of resources consumed, eventually leading to the exhaustion of available resources.
CVSS v3 base score: 7.5
Published: 2022-08-24T05:15Z
Repository: https://github.com/node-opcua/node-opcua
Language: JavaScript
Files changed: packages/node-opcua-server/source/server_subscription.ts, packages/node-opcua-server/test/test_subscription.js
Commit date: 2022-08-13 11:38:40 +0200
Commit message: server: fix Subscription.modify assert

Diff: diff --git a/packages/node-opcua-server/source/server_subscription.ts b/packages/node-opcua-server/source/server_subscription.ts
index 0e08e9319..358ed605b 100644
--- a/packages/node-opcua-server/source/server_subscription.ts
+++ b/packages/node-opcua-server/source/server_subscription.ts
@@ -627,11 +627,11 @@ export class Subscription extends EventEmitter {
         // NotificationMessage or a keep-alive Message is sent out the first time the publishing
         // timer expires.
         this.messageSent = false;
 
         this.timerId = null;
-        this._start_timer();
+        this._start_timer({ firstTime: true });
 
         debugLog(chalk.green(`creating subscription ${this.id}`));
 
         this.serverCapabilities = options.serverCapabilities;
         this.serverCapabilities.maxMonitoredItems =
@@ -684,11 +684,11 @@ export class Subscription extends EventEmitter {
 
         if (publishingInterval_old !== this.publishingInterval) {
             // todo
         }
         this._stop_timer();
-        this._start_timer();
+        this._start_timer({ firstTime: false });
     }
 
     /**
      * set publishing mode
      * @param publishingEnabled
@@ -730,11 +730,11 @@ export class Subscription extends EventEmitter {
      * @private
      */
     public increaseLifeTimeCounter(): void {
         this._life_time_counter += 1;
         if (this._life_time_counter >= this.lifeTimeCount) {
-           this.emit("lifeTimeExpired");
+            this.emit("lifeTimeExpired");
         }
         this.emit("lifeTimeCounterChanged", this._life_time_counter);
     }
 
     /**
@@ -1456,11 +1456,11 @@ export class Subscription extends EventEmitter {
             clearInterval(this.timerId);
             this.timerId = null;
         }
     }
 
-    private _start_timer() {
+    private _start_timer({ firstTime }: { firstTime: boolean }) {
         debugLog(
             chalk.bgWhite.blue("Subscription#_start_timer  subscriptionId="),
             this.id,
             " publishingInterval = ",
             this.publishingInterval
@@ -1476,12 +1476,15 @@ export class Subscription extends EventEmitter {
         // to be reached, as specified in (f) above.
 
         // make sure that a keep-alive Message will be send at the end of the first publishing cycle
         // if there are no Notifications ready.
         this._keep_alive_counter = 0; // this.maxKeepAliveCount;
-        assert(this.messageSent === false);
-        assert(this.state === SubscriptionState.CREATING);
+
+        if (firstTime) {
+            assert(this.messageSent === false);
+            assert(this.state === SubscriptionState.CREATING);
+        }
 
         assert(this.publishingInterval >= Subscription.minimumPublishingInterval);
         this.timerId = setInterval(this._tick.bind(this), this.publishingInterval);
     }
 
diff --git a/packages/node-opcua-server/test/test_subscription.js b/packages/node-opcua-server/test/test_subscription.js
index f6ad95970..998456b4a 100644
--- a/packages/node-opcua-server/test/test_subscription.js
+++ b/packages/node-opcua-server/test/test_subscription.js
@@ -1060,10 +1060,75 @@ describe("Subscriptions", function () {
 
         subscription.terminate();
         subscription.dispose();
     });
 
+    it("T17 - should be possible to  modify subscription publishing interval", function(){
+        // pretend the client has sent many pending PublishRequests
+        fake_publish_engine.pendingPublishRequestCount = 1000;
+
+        /**
+         * When a Subscription is created, the first Message is sent at the end of the first publishing cycle to
+         * inform the Client that the Subscription is operational. A Notification Message is sent if there are
+         * Notifications ready to be reported. If there are none, a keep-alive Message is sent instead that
+         * contains a sequence number of 1, indicating that the first Notification Message has not yet been
+         * sent. This is the only time a keep-alive Message is sent without waiting for the maximum keep-alive
+         * count to be reached, as specified in (f) above.
+         *
+         */
+        const subscription = new Subscription({
+            publishingInterval: 1000,
+            maxKeepAliveCount: 20,
+            //
+            publishEngine: fake_publish_engine,
+            globalCounter: { totalMonitoredItemCount: 0 },
+            serverCapabilities: { maxMonitoredItems: 10000, maxMonitoredItemsPerSubscription: 1000 }
+        });
+        const monitoredItem = add_mock_monitored_item(subscription);
+
+        // pretend that we already have notification messages
+        // a notification finally arrived !
+        monitoredItem.simulateMonitoredItemAddingNotification();
+
+        const notification_event_spy = sinon.spy();
+        const keepalive_event_spy = sinon.spy();
+        const expire_event_spy = sinon.spy();
+
+        subscription.on("notification", notification_event_spy);
+        subscription.on("keepalive", keepalive_event_spy);
+        subscription.on("expired", expire_event_spy);
+
+        test.clock.tick(200);
+        keepalive_event_spy.callCount.should.equal(0);
+        notification_event_spy.callCount.should.eql(0);
+
+        test.clock.tick(1000);
+        keepalive_event_spy.callCount.should.equal(0);
+        notification_event_spy.callCount.should.eql(1);
+
+        test.clock.tick(1000);
+        keepalive_event_spy.callCount.should.equal(0);
+        notification_event_spy.callCount.should.eql(1);
+
+        monitoredItem.simulateMonitoredItemAddingNotification();
+        // now change the publishin Intervale
+        subscription.modify({ requestedPublishingInterval: 2000});
+       
+        test.clock.tick(1000);
+        keepalive_event_spy.callCount.should.equal(0);
+        notification_event_spy.callCount.should.eql(1);
+
+        test.clock.tick(1010);
+        keepalive_event_spy.callCount.should.equal(0);
+        notification_event_spy.callCount.should.eql(2);
+
+        subscription.terminate();
+        subscription.dispose();
+
+    });
+
+
     xit("closing a Subscription causes its MonitoredItems to be deleted. ", function () {
         /** */
     });
 });
 

Vulnerable code:         this._start_timer();
        this._start_timer();
           this.emit("lifeTimeExpired");
    private _start_timer() {
        assert(this.messageSent === false);
        assert(this.state === SubscriptionState.CREATING);
Fixed code:         this._start_timer({ firstTime: true });
        this._start_timer({ firstTime: false });
            this.emit("lifeTimeExpired");
    private _start_timer({ firstTime }: { firstTime: boolean }) {

        if (firstTime) {
            assert(this.messageSent === false);
            assert(this.state === SubscriptionState.CREATING);
        }
    it("T17 - should be possible to  modify subscription publishing interval", function(){
        // pretend the client has sent many pending PublishRequests
        fake_publish_engine.pendingPublishRequestCount = 1000;

        /**
         * When a Subscription is created, the first Message is sent at the end of the first publishing cycle to
         * inform the Client that the Subscription is operational. A Notification Message is sent if there are
         * Notifications ready to be reported. If there are none, a keep-alive Message is sent instead that
         * contains a sequence number of 1, indicating that the first Notification Message has not yet been
         * sent. This is the only time a keep-alive Message is sent without waiting for the maximum keep-alive
         * count to be reached, as specified in (f) above.
         *
         */
        const subscription = new Subscription({
            publishingInterval: 1000,
            maxKeepAliveCount: 20,
            //
            publishEngine: fake_publish_engine,
            globalCounter: { totalMonitoredItemCount: 0 },
            serverCapabilities: { maxMonitoredItems: 10000, maxMonitoredItemsPerSubscription: 1000 }
        });
        const monitoredItem = add_mock_monitored_item(subscription);

        // pretend that we already have notification messages
        // a notification finally arrived !
        monitoredItem.simulateMonitoredItemAddingNotification();

        const notification_event_spy = sinon.spy();
        const keepalive_event_spy = sinon.spy();
        const expire_event_spy = sinon.spy();

        subscription.on("notification", notification_event_spy);
        subscription.on("keepalive", keepalive_event_spy);
        subscription.on("expired", expire_event_spy);

        test.clock.tick(200);
        keepalive_event_spy.callCount.should.equal(0);
        notification_event_spy.callCount.should.eql(0);

        test.clock.tick(1000);
        keepalive_event_spy.callCount.should.equal(0);
        notification_event_spy.callCount.should.eql(1);

        test.clock.tick(1000);
        keepalive_event_spy.callCount.should.equal(0);
        notification_event_spy.callCount.should.eql(1);

        monitoredItem.simulateMonitoredItemAddingNotification();
        // now change the publishin Intervale
        subscription.modify({ requestedPublishingInterval: 2000});
       
        test.clock.tick(1000);
        keepalive_event_spy.callCount.should.equal(0);
        notification_event_spy.callCount.should.eql(1);

        test.clock.tick(1010);
        keepalive_event_spy.callCount.should.equal(0);
        notification_event_spy.callCount.should.eql(2);

        subscription.terminate();
        subscription.dispose();

    });