diff --git a/.gitignore b/.gitignore index 441e8861..fa84af9e 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,15 @@ test-complete-app-mlDeploy/.gradle # Compiled TypeScript test files test-typescript/*.js + +# AI generated documents +documents + +# Generated proxy stubs (created by gulp setupProxyTests / generateProxyTests) +# Files in the generated directory are fully generated +test-basic-proxy/lib/generated/ +# the proxy stubs (non-Test.js) are generated; the *Test.js files are hand-written and must be kept +test-basic-proxy/lib/positive/*.js +!test-basic-proxy/lib/positive/*Test.js +test-basic-proxy/lib/negative/*.js +!test-basic-proxy/lib/negative/*Test.js diff --git a/CHANGELOG.md b/CHANGELOG.md index b9566f56..1e7b5e94 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # CHANGELOG +## 4.2.0 + +#### Security Fix + +- Session IDs generated by `SessionState` now use `crypto.randomBytes()` instead of `Math.random()`, replacing a + non-cryptographic PRNG (xorshift128+) with a cryptographically secure source of randomness (CWE-338). + The session ID is now a 16-character hex string (8 random bytes), matching the format MarkLogic uses for its own + server-generated session IDs. The return type of `SessionState.sessionId()` changes from `number` to `string`. + Callers that only use the session ID for logging or cookie headers are unaffected. + ## 4.1.0 - Added TypeScript typings for core client/document/connection APIs, with better autocomplete and compile-time checks to catch mistakes earlier. diff --git a/lib/session-state.js b/lib/session-state.js index 29c5d0d7..e90863bd 100644 --- a/lib/session-state.js +++ b/lib/session-state.js @@ -1,8 +1,10 @@ /* -* Copyright (c) 2015-2025 Progress Software Corporation and/or its subsidiaries or affiliates. All Rights Reserved. +* Copyright (c) 2015-2026 Progress Software Corporation and/or its subsidiaries or affiliates. All Rights Reserved. */ 'use strict'; +const crypto = require('crypto'); + /** * Identifies a server state for sharing across multiple calls to server endpoints. * @@ -14,13 +16,12 @@ class SessionState { * Constructs an identifier for session state on the server. */ constructor() { - // TODO: use BigInt? - this._sessionId = Math.floor(Math.random() * Number.MAX_SAFE_INTEGER); + this._sessionId = crypto.randomBytes(8).toString('hex'); } /** * Provides the identifier used for the server state (for instance, for use in logging). - * @returns {number} the session identifier + * @returns {string} the session identifier */ sessionId() { return this._sessionId; diff --git a/test-basic/session-state-test.js b/test-basic/session-state-test.js new file mode 100644 index 00000000..440b4fef --- /dev/null +++ b/test-basic/session-state-test.js @@ -0,0 +1,38 @@ +/* +* Copyright (c) 2015-2026 Progress Software Corporation and/or its subsidiaries or affiliates. All Rights Reserved. +*/ +'use strict'; + +const assert = require('assert'); +const SessionState = require('../lib/session-state.js'); + +describe('SessionState', function () { + it('sessionId() returns a non-empty string', function () { + const session = new SessionState(); + const id = session.sessionId(); + assert.strictEqual(typeof id, 'string'); + assert.ok(id.length > 0, 'sessionId should not be empty'); + }); + + it('sessionId() returns a 16-character hex string (8 random bytes)', function () { + const session = new SessionState(); + const id = session.sessionId(); + assert.match(id, /^[0-9a-f]{16}$/, 'sessionId should be a 16-character lowercase hex string'); + }); + + it('sessionId() returns the same value on repeated calls to the same instance', function () { + const session = new SessionState(); + const id1 = session.sessionId(); + const id2 = session.sessionId(); + const id3 = session.sessionId(); + assert.strictEqual(id1, id2, 'sessionId should be stable across calls'); + assert.strictEqual(id2, id3, 'sessionId should be stable across calls'); + }); + + it('two instances created in rapid succession produce different session IDs', function () { + const session1 = new SessionState(); + const session2 = new SessionState(); + assert.notStrictEqual(session1.sessionId(), session2.sessionId(), + 'session IDs from two distinct instances should not be equal'); + }); +});