mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-30 23:25:24 +00:00
fix: trust devices by a server-issued token, not a client header (GHSA-6gr3-r7jx-wfmh)
This commit is contained in:
1 parent
ad3a18224f
commit
8a45d952fd
8 files changed
+130
-131
No files matched your search
@@ -14,11 +14,7 @@ import type { Request, Response } from "express";
|
||||
import { AuthManager } from "../utils/auth-manager.js";
|
||||
import { loginRateLimiter } from "../utils/login-rate-limiter.js";
|
||||
import { authLogger } from "../utils/logger.js";
|
||||
import {
|
||||
generateDeviceFingerprint,
|
||||
getDeviceId,
|
||||
parseUserAgent,
|
||||
} from "../utils/user-agent-parser.js";
|
||||
import { parseUserAgent } from "../utils/user-agent-parser.js";
|
||||
import { logAudit, getRequestMeta } from "../utils/audit-logger.js";
|
||||
import {
|
||||
createCurrentUserAuthRepository,
|
||||
@@ -37,6 +33,7 @@ import {
|
||||
type SecondFactor,
|
||||
} from "./registry.js";
|
||||
import {
|
||||
isNativeAppRequest,
|
||||
issueSession,
|
||||
sendSession,
|
||||
syncSharedCredentialsForUserRoles,
|
||||
@@ -131,10 +128,33 @@ function shouldRunSecondFactors(methodId: string): boolean {
|
||||
return isSecondFactorAfterExternalLoginEnabled();
|
||||
}
|
||||
|
||||
export const TRUST_DEVICE_COOKIE = "termix_trust_device";
|
||||
const TRUST_TOKEN_PATTERN = /^[a-f0-9]{64}$/;
|
||||
const TRUST_TOKEN_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1000;
|
||||
|
||||
/**
|
||||
* The remember-this-device token the server handed out after a full login.
|
||||
* Browsers carry it in an HttpOnly cookie; the desktop app, which talks to
|
||||
* the server cross-origin without cookies, sends it back as a header.
|
||||
*/
|
||||
function readTrustToken(req: Request): string | null {
|
||||
const cookies = (req as Request & { cookies?: Record<string, string> })
|
||||
.cookies;
|
||||
const value =
|
||||
cookies?.[TRUST_DEVICE_COOKIE] || req.get("x-termix-trust-token");
|
||||
return typeof value === "string" && TRUST_TOKEN_PATTERN.test(value)
|
||||
? value
|
||||
: null;
|
||||
}
|
||||
|
||||
export function hashTrustToken(token: string): string {
|
||||
return crypto.createHash("sha256").update(`trust-v2|${token}`).digest("hex");
|
||||
}
|
||||
|
||||
async function isTrustedDevice(req: Request, userId: string): Promise<boolean> {
|
||||
const deviceInfo = parseUserAgent(req);
|
||||
const fingerprint = generateDeviceFingerprint(deviceInfo, getDeviceId(req));
|
||||
if (!fingerprint) return false;
|
||||
const token = readTrustToken(req);
|
||||
if (!token) return false;
|
||||
const fingerprint = hashTrustToken(token);
|
||||
const trusted = await AuthManager.getInstance().isTrustedDevice(
|
||||
userId,
|
||||
fingerprint,
|
||||
@@ -473,22 +493,29 @@ export async function verifySecondFactorAndRespond(
|
||||
? req.body.rememberMe
|
||||
: (lookup.pending?.rememberMe ?? false);
|
||||
|
||||
let trustToken: string | null = null;
|
||||
if (rememberMe) {
|
||||
const deviceInfo = parseUserAgent(req);
|
||||
const fingerprint = generateDeviceFingerprint(deviceInfo, getDeviceId(req));
|
||||
if (fingerprint) {
|
||||
await AuthManager.getInstance().addTrustedDevice(
|
||||
user.id,
|
||||
fingerprint,
|
||||
deviceInfo.type,
|
||||
deviceInfo.deviceInfo,
|
||||
);
|
||||
authLogger.info("Device automatically trusted via Remember Me", {
|
||||
operation: "totp_auto_trust",
|
||||
userId: user.id,
|
||||
deviceType: deviceInfo.type,
|
||||
});
|
||||
}
|
||||
trustToken = crypto.randomBytes(32).toString("hex");
|
||||
await AuthManager.getInstance().addTrustedDevice(
|
||||
user.id,
|
||||
hashTrustToken(trustToken),
|
||||
deviceInfo.type,
|
||||
deviceInfo.deviceInfo,
|
||||
);
|
||||
res.cookie(
|
||||
TRUST_DEVICE_COOKIE,
|
||||
trustToken,
|
||||
AuthManager.getInstance().getSecureCookieOptions(
|
||||
req,
|
||||
TRUST_TOKEN_MAX_AGE_MS,
|
||||
),
|
||||
);
|
||||
authLogger.info("Device automatically trusted via Remember Me", {
|
||||
operation: "totp_auto_trust",
|
||||
userId: user.id,
|
||||
deviceType: deviceInfo.type,
|
||||
});
|
||||
}
|
||||
|
||||
const pending = lookup.pending;
|
||||
@@ -498,6 +525,10 @@ export async function verifySecondFactorAndRespond(
|
||||
externalSession: pending?.externalSession ?? null,
|
||||
});
|
||||
|
||||
if (trustToken && isNativeAppRequest(req)) {
|
||||
session.body.trustToken = trustToken;
|
||||
}
|
||||
|
||||
consumePendingLogin(lookup.token);
|
||||
res.clearCookie(
|
||||
PENDING_LOGIN_COOKIE,
|
||||
|
||||
Reference in new issue
Block a user