[object Object]

← back to Dw Boardroom Governance

Harden basic-auth middleware: timing-safe compare, malformed-header guard, fail-closed on unset creds

79f893e8890bf7291ea65dcb57a052f1a26b2b13 · 2026-05-18 20:17:15 -0700 · Steve Abrams

Files touched

Diff

commit 79f893e8890bf7291ea65dcb57a052f1a26b2b13
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Mon May 18 20:17:15 2026 -0700

    Harden basic-auth middleware: timing-safe compare, malformed-header guard, fail-closed on unset creds
---
 src/api/middleware/auth.ts | 24 +++++++++++++++++++++---
 1 file changed, 21 insertions(+), 3 deletions(-)

diff --git a/src/api/middleware/auth.ts b/src/api/middleware/auth.ts
index bd1fb37..dc00240 100644
--- a/src/api/middleware/auth.ts
+++ b/src/api/middleware/auth.ts
@@ -1,4 +1,13 @@
 import { Request, Response, NextFunction } from 'express';
+import { timingSafeEqual } from 'crypto';
+
+function safeEqual(a: string, b: string): boolean {
+  const bufA = Buffer.from(a);
+  const bufB = Buffer.from(b);
+  // timingSafeEqual requires equal-length buffers; bail early on mismatch.
+  if (bufA.length !== bufB.length) return false;
+  return timingSafeEqual(bufA, bufB);
+}
 
 export function authMiddleware(req: Request, res: Response, next: NextFunction): void {
   const auth = req.headers.authorization;
@@ -9,10 +18,19 @@ export function authMiddleware(req: Request, res: Response, next: NextFunction):
     return;
   }
 
-  const decoded = Buffer.from(auth.split(' ')[1], 'base64').toString();
-  const [user, pass] = decoded.split(':');
+  const token = auth.split(' ')[1] || '';
+  const decoded = Buffer.from(token, 'base64').toString();
+  // Split on the first colon only — passwords may legitimately contain colons.
+  const sep = decoded.indexOf(':');
+  const user = sep >= 0 ? decoded.slice(0, sep) : '';
+  const pass = sep >= 0 ? decoded.slice(sep + 1) : '';
+
+  const expectedUser = process.env.AUTH_USER;
+  const expectedPass = process.env.AUTH_PASS;
 
-  if (user === process.env.AUTH_USER && pass === process.env.AUTH_PASS) {
+  // Fail closed if server credentials are not configured.
+  if (expectedUser && expectedPass &&
+      safeEqual(user, expectedUser) && safeEqual(pass, expectedPass)) {
     next();
     return;
   }

← a6dac82 Fix meeting phase auto-advance to use per-phase durations in  ·  back to Dw Boardroom Governance  ·  Fix useWebSocket: prevent zombie reconnect timer firing afte 44b2fa8 →