mastercontroller 1.2.11 → 1.2.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,416 @@
1
+ // version 1.0.0
2
+ // MasterController Session Security - Secure cookie handling, session fixation prevention
3
+
4
+ /**
5
+ * Secure session handling for MasterController
6
+ * Prevents: Session fixation, session hijacking, cookie theft
7
+ */
8
+
9
+ const crypto = require('crypto');
10
+ const { logger } = require('./MasterErrorLogger');
11
+
12
+ // Session store (use Redis in production)
13
+ const sessionStore = new Map();
14
+
15
+ class SessionSecurity {
16
+ constructor(options = {}) {
17
+ this.cookieName = options.cookieName || 'mc_session';
18
+ this.secret = options.secret || this._generateSecret();
19
+ this.maxAge = options.maxAge || 86400000; // 24 hours
20
+ this.httpOnly = options.httpOnly !== false;
21
+ this.secure = options.secure !== false; // Only send over HTTPS
22
+ this.sameSite = options.sameSite || 'strict'; // 'strict', 'lax', or 'none'
23
+ this.rolling = options.rolling !== false; // Extend expiry on each request
24
+ this.regenerateInterval = options.regenerateInterval || 3600000; // 1 hour
25
+ this.domain = options.domain || null;
26
+ this.path = options.path || '/';
27
+
28
+ // Session fingerprinting
29
+ this.useFingerprint = options.useFingerprint !== false;
30
+
31
+ // Start cleanup interval
32
+ this._startCleanup();
33
+ }
34
+
35
+ /**
36
+ * Session middleware
37
+ */
38
+ middleware() {
39
+ return async (req, res, next) => {
40
+ // Parse session from cookie
41
+ const sessionId = this._parseCookie(req);
42
+
43
+ if (sessionId) {
44
+ // Load existing session
45
+ const session = sessionStore.get(sessionId);
46
+
47
+ if (session && this._isSessionValid(session, req)) {
48
+ // Check if session needs regeneration
49
+ if (this._shouldRegenerate(session)) {
50
+ req.session = await this._regenerateSession(sessionId, session, res);
51
+ } else {
52
+ // Use existing session
53
+ req.session = session.data;
54
+ req.sessionId = sessionId;
55
+
56
+ // Update last access time
57
+ session.lastAccess = Date.now();
58
+
59
+ // Extend expiry if rolling
60
+ if (this.rolling) {
61
+ session.expiry = Date.now() + this.maxAge;
62
+ this._setCookie(res, sessionId);
63
+ }
64
+ }
65
+ } else {
66
+ // Invalid or expired session
67
+ if (session) {
68
+ sessionStore.delete(sessionId);
69
+ logger.warn({
70
+ code: 'MC_SECURITY_SESSION_INVALID',
71
+ message: 'Invalid session detected',
72
+ sessionId: sessionId.substring(0, 10) + '...'
73
+ });
74
+ }
75
+
76
+ // Create new session
77
+ req.session = await this._createSession(req, res);
78
+ }
79
+ } else {
80
+ // No session cookie, create new session
81
+ req.session = await this._createSession(req, res);
82
+ }
83
+
84
+ // Save session on response
85
+ const originalEnd = res.end;
86
+ res.end = (...args) => {
87
+ this._saveSession(req);
88
+ originalEnd.apply(res, args);
89
+ };
90
+
91
+ next();
92
+ };
93
+ }
94
+
95
+ /**
96
+ * Create new session
97
+ */
98
+ async _createSession(req, res) {
99
+ const sessionId = this._generateSessionId();
100
+ const fingerprint = this.useFingerprint ? this._generateFingerprint(req) : null;
101
+
102
+ const sessionData = {
103
+ id: sessionId,
104
+ data: {},
105
+ createdAt: Date.now(),
106
+ lastAccess: Date.now(),
107
+ expiry: Date.now() + this.maxAge,
108
+ fingerprint,
109
+ regeneratedAt: Date.now()
110
+ };
111
+
112
+ sessionStore.set(sessionId, sessionData);
113
+
114
+ // Set cookie
115
+ this._setCookie(res, sessionId);
116
+
117
+ req.sessionId = sessionId;
118
+
119
+ return sessionData.data;
120
+ }
121
+
122
+ /**
123
+ * Regenerate session (prevent session fixation)
124
+ */
125
+ async _regenerateSession(oldSessionId, oldSession, res) {
126
+ const newSessionId = this._generateSessionId();
127
+
128
+ // Copy session data to new session
129
+ const newSession = {
130
+ id: newSessionId,
131
+ data: { ...oldSession.data },
132
+ createdAt: oldSession.createdAt,
133
+ lastAccess: Date.now(),
134
+ expiry: Date.now() + this.maxAge,
135
+ fingerprint: oldSession.fingerprint,
136
+ regeneratedAt: Date.now()
137
+ };
138
+
139
+ // Delete old session
140
+ sessionStore.delete(oldSessionId);
141
+
142
+ // Store new session
143
+ sessionStore.set(newSessionId, newSession);
144
+
145
+ // Update cookie
146
+ this._setCookie(res, newSessionId);
147
+
148
+ logger.info({
149
+ code: 'MC_SECURITY_SESSION_REGENERATED',
150
+ message: 'Session regenerated',
151
+ oldSessionId: oldSessionId.substring(0, 10) + '...',
152
+ newSessionId: newSessionId.substring(0, 10) + '...'
153
+ });
154
+
155
+ return newSession.data;
156
+ }
157
+
158
+ /**
159
+ * Save session data
160
+ */
161
+ _saveSession(req) {
162
+ if (!req.sessionId) return;
163
+
164
+ const session = sessionStore.get(req.sessionId);
165
+ if (session) {
166
+ session.data = req.session;
167
+ session.lastAccess = Date.now();
168
+ }
169
+ }
170
+
171
+ /**
172
+ * Check if session is valid
173
+ */
174
+ _isSessionValid(session, req) {
175
+ // Check expiry
176
+ if (Date.now() > session.expiry) {
177
+ return false;
178
+ }
179
+
180
+ // Check fingerprint
181
+ if (this.useFingerprint && session.fingerprint) {
182
+ const currentFingerprint = this._generateFingerprint(req);
183
+ if (currentFingerprint !== session.fingerprint) {
184
+ logger.warn({
185
+ code: 'MC_SECURITY_SESSION_HIJACK_ATTEMPT',
186
+ message: 'Session hijacking attempt detected',
187
+ sessionId: session.id.substring(0, 10) + '...',
188
+ expectedFingerprint: session.fingerprint,
189
+ actualFingerprint: currentFingerprint
190
+ });
191
+ return false;
192
+ }
193
+ }
194
+
195
+ return true;
196
+ }
197
+
198
+ /**
199
+ * Check if session should be regenerated
200
+ */
201
+ _shouldRegenerate(session) {
202
+ const age = Date.now() - session.regeneratedAt;
203
+ return age > this.regenerateInterval;
204
+ }
205
+
206
+ /**
207
+ * Generate session ID
208
+ */
209
+ _generateSessionId() {
210
+ return crypto.randomBytes(32).toString('hex');
211
+ }
212
+
213
+ /**
214
+ * Generate secret for signing
215
+ */
216
+ _generateSecret() {
217
+ return crypto.randomBytes(64).toString('hex');
218
+ }
219
+
220
+ /**
221
+ * Generate fingerprint for session hijacking detection
222
+ */
223
+ _generateFingerprint(req) {
224
+ const components = [
225
+ req.headers['user-agent'] || '',
226
+ req.headers['accept-language'] || '',
227
+ req.connection.remoteAddress || '',
228
+ // Don't include Accept-Encoding (changes too often)
229
+ ];
230
+
231
+ return crypto
232
+ .createHash('sha256')
233
+ .update(components.join('|'))
234
+ .digest('hex');
235
+ }
236
+
237
+ /**
238
+ * Parse session cookie from request
239
+ */
240
+ _parseCookie(req) {
241
+ const cookies = req.headers.cookie;
242
+ if (!cookies) return null;
243
+
244
+ const match = cookies.match(new RegExp(`${this.cookieName}=([^;]+)`));
245
+ return match ? match[1] : null;
246
+ }
247
+
248
+ /**
249
+ * Set session cookie
250
+ */
251
+ _setCookie(res, sessionId) {
252
+ const options = [
253
+ `${this.cookieName}=${sessionId}`,
254
+ `Max-Age=${Math.floor(this.maxAge / 1000)}`,
255
+ `Path=${this.path}`
256
+ ];
257
+
258
+ if (this.domain) {
259
+ options.push(`Domain=${this.domain}`);
260
+ }
261
+
262
+ if (this.httpOnly) {
263
+ options.push('HttpOnly');
264
+ }
265
+
266
+ if (this.secure) {
267
+ options.push('Secure');
268
+ }
269
+
270
+ if (this.sameSite) {
271
+ options.push(`SameSite=${this.sameSite}`);
272
+ }
273
+
274
+ res.setHeader('Set-Cookie', options.join('; '));
275
+ }
276
+
277
+ /**
278
+ * Destroy session
279
+ */
280
+ destroySession(req, res) {
281
+ if (req.sessionId) {
282
+ sessionStore.delete(req.sessionId);
283
+
284
+ // Clear cookie
285
+ const options = [
286
+ `${this.cookieName}=`,
287
+ 'Max-Age=0',
288
+ `Path=${this.path}`
289
+ ];
290
+
291
+ if (this.domain) {
292
+ options.push(`Domain=${this.domain}`);
293
+ }
294
+
295
+ res.setHeader('Set-Cookie', options.join('; '));
296
+
297
+ req.session = null;
298
+ req.sessionId = null;
299
+
300
+ logger.info({
301
+ code: 'MC_SECURITY_SESSION_DESTROYED',
302
+ message: 'Session destroyed'
303
+ });
304
+ }
305
+ }
306
+
307
+ /**
308
+ * Get session by ID
309
+ */
310
+ getSession(sessionId) {
311
+ const session = sessionStore.get(sessionId);
312
+ return session ? session.data : null;
313
+ }
314
+
315
+ /**
316
+ * Update session expiry
317
+ */
318
+ touch(sessionId) {
319
+ const session = sessionStore.get(sessionId);
320
+ if (session) {
321
+ session.lastAccess = Date.now();
322
+ session.expiry = Date.now() + this.maxAge;
323
+ }
324
+ }
325
+
326
+ /**
327
+ * Cleanup expired sessions
328
+ */
329
+ _startCleanup() {
330
+ setInterval(() => {
331
+ const now = Date.now();
332
+ let cleaned = 0;
333
+
334
+ for (const [sessionId, session] of sessionStore.entries()) {
335
+ if (now > session.expiry) {
336
+ sessionStore.delete(sessionId);
337
+ cleaned++;
338
+ }
339
+ }
340
+
341
+ if (cleaned > 0) {
342
+ logger.info({
343
+ code: 'MC_SECURITY_SESSION_CLEANUP',
344
+ message: `Cleaned up ${cleaned} expired sessions`,
345
+ totalSessions: sessionStore.size
346
+ });
347
+ }
348
+ }, 60000); // Run every minute
349
+ }
350
+
351
+ /**
352
+ * Get session store size (for monitoring)
353
+ */
354
+ getSessionCount() {
355
+ return sessionStore.size;
356
+ }
357
+
358
+ /**
359
+ * Clear all sessions (for testing)
360
+ */
361
+ clearAllSessions() {
362
+ const count = sessionStore.size;
363
+ sessionStore.clear();
364
+ logger.warn({
365
+ code: 'MC_SECURITY_ALL_SESSIONS_CLEARED',
366
+ message: `Cleared all ${count} sessions`
367
+ });
368
+ }
369
+ }
370
+
371
+ // Create singleton instance
372
+ const session = new SessionSecurity();
373
+
374
+ /**
375
+ * Factory functions
376
+ */
377
+
378
+ function createSessionMiddleware(options = {}) {
379
+ const instance = new SessionSecurity(options);
380
+ return instance.middleware();
381
+ }
382
+
383
+ function destroySession(req, res) {
384
+ return session.destroySession(req, res);
385
+ }
386
+
387
+ /**
388
+ * Security best practices for sessions
389
+ */
390
+
391
+ const SESSION_BEST_PRACTICES = {
392
+ production: {
393
+ secure: true,
394
+ httpOnly: true,
395
+ sameSite: 'strict',
396
+ maxAge: 3600000, // 1 hour
397
+ regenerateInterval: 900000, // 15 minutes
398
+ useFingerprint: true
399
+ },
400
+ development: {
401
+ secure: false, // Allow HTTP in dev
402
+ httpOnly: true,
403
+ sameSite: 'lax',
404
+ maxAge: 86400000, // 24 hours
405
+ regenerateInterval: 3600000, // 1 hour
406
+ useFingerprint: true
407
+ }
408
+ };
409
+
410
+ module.exports = {
411
+ SessionSecurity,
412
+ session,
413
+ createSessionMiddleware,
414
+ destroySession,
415
+ SESSION_BEST_PRACTICES
416
+ };
package/package.json CHANGED
@@ -18,5 +18,5 @@
18
18
  "scripts": {
19
19
  "test": "echo \"Error: no test specified\" && exit 1"
20
20
  },
21
- "version": "1.2.11"
22
- }
21
+ "version": "1.2.13"
22
+ }