termbeam 1.21.4 → 1.21.5
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.
- package/CHANGELOG.md +6 -0
- package/package.json +1 -1
- package/src/server/auth.js +25 -2
- package/src/server/routes.js +1 -1
- package/src/server/websocket.js +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.21.5] - 2026-04-23
|
|
4
|
+
|
|
5
|
+
- fix(auth): constant-time password comparison (#204) (@TickTockBent)
|
|
6
|
+
- fix(auth): use raw timingSafeEqual instead of HMAC for password compare (@dorlugasigal)
|
|
7
|
+
- fix(auth): pad to fixed length in safeCompare instead of self-compare (@dorlugasigal)
|
|
8
|
+
|
|
3
9
|
## [1.21.4] - 2026-04-23
|
|
4
10
|
|
|
5
11
|
- feat: add author attribution to changelog via gh api (@dorlugasigal)
|
package/package.json
CHANGED
package/src/server/auth.js
CHANGED
|
@@ -290,6 +290,28 @@ const LOGIN_HTML = `<!DOCTYPE html>
|
|
|
290
290
|
</body>
|
|
291
291
|
</html>`;
|
|
292
292
|
|
|
293
|
+
// Constant-time string compare using crypto.timingSafeEqual on raw bytes.
|
|
294
|
+
// We intentionally do NOT hash the inputs: this is an equality check against
|
|
295
|
+
// an in-memory secret (never stored), and hashing would trip CodeQL's
|
|
296
|
+
// js/insufficient-password-hash rule which targets password *storage*.
|
|
297
|
+
//
|
|
298
|
+
// To avoid leaking length via early-return, both inputs are copied into
|
|
299
|
+
// fixed-length zero-padded buffers before timingSafeEqual, and the final
|
|
300
|
+
// result is AND-ed with a real length check.
|
|
301
|
+
const SAFE_COMPARE_LEN = 256;
|
|
302
|
+
function safeCompare(a, b) {
|
|
303
|
+
if (typeof a !== 'string' || typeof b !== 'string') return false;
|
|
304
|
+
const ab = Buffer.from(a, 'utf8');
|
|
305
|
+
const bb = Buffer.from(b, 'utf8');
|
|
306
|
+
if (ab.length > SAFE_COMPARE_LEN || bb.length > SAFE_COMPARE_LEN) return false;
|
|
307
|
+
const ap = Buffer.alloc(SAFE_COMPARE_LEN);
|
|
308
|
+
const bp = Buffer.alloc(SAFE_COMPARE_LEN);
|
|
309
|
+
ab.copy(ap);
|
|
310
|
+
bb.copy(bp);
|
|
311
|
+
const bytesEqual = crypto.timingSafeEqual(ap, bp);
|
|
312
|
+
return bytesEqual && ab.length === bb.length;
|
|
313
|
+
}
|
|
314
|
+
|
|
293
315
|
function createAuth(password) {
|
|
294
316
|
const tokens = new Map();
|
|
295
317
|
const authAttempts = new Map();
|
|
@@ -373,7 +395,7 @@ function createAuth(password) {
|
|
|
373
395
|
log.warn(`Auth: rate limit exceeded for ${ip}`);
|
|
374
396
|
return res.status(429).json({ error: 'Too many attempts. Try again later.' });
|
|
375
397
|
}
|
|
376
|
-
if (authHeader
|
|
398
|
+
if (safeCompare(authHeader.slice('Bearer '.length), password)) return next();
|
|
377
399
|
recent.push(now);
|
|
378
400
|
authAttempts.set(ip, recent);
|
|
379
401
|
return res.status(401).json({ error: 'unauthorized' });
|
|
@@ -416,9 +438,10 @@ function createAuth(password) {
|
|
|
416
438
|
middleware,
|
|
417
439
|
rateLimit,
|
|
418
440
|
parseCookies,
|
|
441
|
+
safeCompare,
|
|
419
442
|
loginHTML: LOGIN_HTML,
|
|
420
443
|
cleanup: () => clearInterval(cleanupInterval),
|
|
421
444
|
};
|
|
422
445
|
}
|
|
423
446
|
|
|
424
|
-
module.exports = { createAuth };
|
|
447
|
+
module.exports = { createAuth, safeCompare };
|
package/src/server/routes.js
CHANGED
|
@@ -133,7 +133,7 @@ function setupRoutes(app, { auth, sessions, config, state, pushManager, copilotS
|
|
|
133
133
|
// Auth API
|
|
134
134
|
app.post('/api/auth', auth.rateLimit, (req, res) => {
|
|
135
135
|
const { password } = req.body || {};
|
|
136
|
-
if (password
|
|
136
|
+
if (auth.safeCompare(password, auth.password)) {
|
|
137
137
|
const token = auth.generateToken();
|
|
138
138
|
res.cookie('pty_token', token, {
|
|
139
139
|
httpOnly: true,
|
package/src/server/websocket.js
CHANGED
|
@@ -127,7 +127,7 @@ function setupWebSocket(wss, { auth, sessions, copilotService }) {
|
|
|
127
127
|
return;
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
-
if (msg.password
|
|
130
|
+
if (auth.safeCompare(msg.password, auth.password) || auth.validateToken(msg.token)) {
|
|
131
131
|
authenticated = true;
|
|
132
132
|
ws.send(JSON.stringify({ type: 'auth_ok' }));
|
|
133
133
|
log.info('WS: auth success');
|