fix(auth): compare HEALTH_TOKEN in constant time via tokenMatches #30

Merged
maximus merged 3 commits from issue-17-timing-safe-health-token into main 2026-09-27 23:27:45 +00:00
2 changed files with 6 additions and 2 deletions
Showing only changes of commit f006a1c05e - Show all commits

View file

@ -59,11 +59,11 @@ curl -H "Authorization: Bearer $(cat ~/.coolify-token)" \
## Tests
- `npm test` (vitest) — 299 cas
- `npm test` (vitest) — 301 cas
- `__tests__/findings.test.js` — `/defenseurs/findings` (14 cas : auth, validation, filtres severity/category, asymetrie INFO, scan clean vs no_data, JSON corrompu)
- `__tests__/state.test.js` — `/defenseurs/state` + non-regression `/defenseurs` apres extraction de `serveJsonFile()` (9 cas : corps identique octet pour octet sur un JSON non canonique, no_data, isolation des deux chemins, 401 sans token / mauvais token / token d'ingestion, 404 sur POST)
- `__tests__/health.test.js` — module `metrics.js` + `/health` (6 cas : exports, payload champ pour champ, garde de latence)
- `__tests__/auth.test.js` — filet de securite auth sur les routes de lecture (65 cas : matrice route x mode d'echec, en-tetes malformes, quasi-rates du token sur les 6 routes de lecture, fail-closed, **2 cas `404 (not 401)` qui figent l'ordre routage -> auth**, **un espion sur `timingSafeEqual`** qui exige deux empreintes de 32 octets a chaque requete, et **des epingles sur le source** : aucune egalite contre une chaine `Bearer`, un seul `tokenMatches()` et un seul `return null` dans `checkAuth()`, egalites limitees a une liste blanche, `tokenMatches()` qui hache les deux cotes sans test de longueur)
- `__tests__/auth.test.js` — filet de securite auth sur les routes de lecture (67 cas : matrice route x mode d'echec, en-tetes malformes (dont le separateur `Bearer` autre qu'un espace), quasi-rates du token sur les 6 routes de lecture, fail-closed, **2 cas `404 (not 401)` qui figent l'ordre routage -> auth**, **un espion sur `timingSafeEqual`** qui exige deux empreintes de 32 octets a chaque requete, et **des epingles sur le source** : aucune egalite contre une chaine `Bearer`, un seul `tokenMatches()` et un seul `return null` dans `checkAuth()`, egalites limitees a une liste blanche, `tokenMatches()` qui hache les deux cotes sans test de longueur)
- `__tests__/hosts.test.js` — matrice exhaustive des endpoints postes (155 cas : regex de route comme controle anti-traversee, 401 avant 403, cloisonnement des deux tokens, 503 fail-closed, reconstruction par liste blanche, 413, `neverSeen`, fraicheur serveur sous horloge figee, degradation de lecture)
- `__tests__/agent.test.js` — agent poste (50 cas : contrat de payload verifie contre `sanitizeSnapshot()`, une seule tentative sans rejeu, timeout, codes de sortie, wrapper shell, et **le token absent de toute sortie d'echec, fragments de 5 caracteres compris**)
- Runtime reste 0-dep ; vitest en devDep uniquement

View file

@ -125,6 +125,10 @@ describe("auth gate — malformed Authorization header", () => {
["raw token without the Bearer scheme", TOKEN],
["lowercase scheme", `bearer ${TOKEN}`],
["scheme with no token", "Bearer"],
// The separator is exactly one space: `startsWith("Bearer")` followed by
// slice(7) would accept any byte in its place.
["tab instead of the space", `Bearer\t${TOKEN}`],
["any other byte instead of the space", `BearerX${TOKEN}`],
];
test.each(MALFORMED)("401 on GET /defenseurs — %s", async (_label, header) => {