fix(#486): ревью — run-race контракт под новую политику + timing-safe metrics-токен
Ревью полной ветки #486 нашло два пункта в моих коммитах (3 и 4): - BLOCKER (коммит 4): ai-chat.service.run-race.spec.ts (#184 F14) пинил СТАРУЮ политику «plain begin() failure → swallow + стрим UNTRACKED» (resolves toBeUndefined). Коммит 4 её развернул → тест падал (1 failed/6). Кейс ИНВЕРТИРОВАН под новую политику: plain begin-failure теперь REJECTS с 503 A_RUN_BEGIN_FAILED, до первого байта, user-строка не вставлена, streamText не вызван — путь остаётся явно запинён, а не удалён. - NIT (коммит 3): metrics-токен сравнивался наивным !== (единственный слой auth эндпоинта) → тайминг-утечка токена. Заменено на crypto.timingSafeEqual с length-guard (разная длина → reject), семантика 401/200 без изменений. Внесено отдельным fixup-коммитом (rebase -i недоступен в окружении; ветка не запушена). Тесты: run-race 7/7 + metrics.server 7/7 зелёные.
This commit is contained in:
@@ -1,7 +1,27 @@
|
||||
import { createServer, Server } from 'node:http';
|
||||
import { timingSafeEqual } from 'node:crypto';
|
||||
import { Logger } from '@nestjs/common';
|
||||
import { getMetricsRegistry, isMetricsEnabled } from './metrics.registry';
|
||||
|
||||
/**
|
||||
* Constant-time compare of the presented Authorization header against the
|
||||
* expected `Bearer <token>`. This is the ONLY auth layer for the metrics
|
||||
* endpoint, so a naive `!==` would leak the token byte-by-byte via timing.
|
||||
* timingSafeEqual requires equal-length buffers, so a length mismatch short-
|
||||
* circuits to "not equal" (its own length is not itself a useful oracle: the
|
||||
* expected string length is fixed by config, not secret-derived).
|
||||
*/
|
||||
function bearerMatches(
|
||||
presented: string | undefined,
|
||||
expected: string,
|
||||
): boolean {
|
||||
if (typeof presented !== 'string') return false;
|
||||
const a = Buffer.from(presented);
|
||||
const b = Buffer.from(expected);
|
||||
if (a.length !== b.length) return false;
|
||||
return timingSafeEqual(a, b);
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the Prometheus scrape endpoint on a SEPARATE port, taken from
|
||||
* `METRICS_PORT`. There is NO default port: when `METRICS_PORT` is unset the
|
||||
@@ -64,7 +84,7 @@ export function startMetricsServer(): Server | null {
|
||||
// configured. This is the auth layer the old all-interfaces bind lacked.
|
||||
if (token) {
|
||||
const auth = req.headers['authorization'];
|
||||
if (auth !== `Bearer ${token}`) {
|
||||
if (!bearerMatches(auth, `Bearer ${token}`)) {
|
||||
res.statusCode = 401;
|
||||
res.setHeader('WWW-Authenticate', 'Bearer');
|
||||
res.end();
|
||||
|
||||
Reference in New Issue
Block a user