fix: hlavičky s identitou přijímat jen z důvěryhodných adres
CI / Generate TypeScript types (push) Successful in 11s
CI / Server unit tests (push) Successful in 31s
CI / Build server (push) Successful in 27s
CI / Build client (push) Successful in 47s
CI / Playwright E2E tests (push) Successful in 1m39s
CI / Build and push Docker image (push) Successful in 45s
CI / Notify (push) Successful in 2s

This commit is contained in:
2026-09-24 16:12:46 +02:00
parent 89d2cc2bc3
commit 7cda7cfdb6
5 changed files with 168 additions and 36 deletions
+64
View File
@@ -0,0 +1,64 @@
import net from "net";
import type { Request } from "express";
/**
* Seznam adres, ze kterých smí přijít hlavička s identitou uživatele (přihlášení přes proxy).
* Podporuje jednotlivé adresy i rozsahy CIDR, IPv4 i IPv6 (např. "10.42.0.0/16,127.0.0.1,::1").
* Neplatná položka je chyba konfigurace — server kvůli ní nenaběhne (lepší než tiše důvěřovat).
*/
export function parseTrustedIps(list: string): net.BlockList {
const blockList = new net.BlockList();
for (const raw of list.split(',').map(s => s.trim()).filter(Boolean)) {
const [address, prefix] = raw.split('/');
const family = net.isIP(address);
if (!family) {
throw new Error(`HTTP_REMOTE_TRUSTED_IPS: neplatná adresa "${raw}"`);
}
const type = family === 4 ? 'ipv4' : 'ipv6';
if (prefix === undefined) {
blockList.addAddress(address, type);
continue;
}
const bits = Number(prefix);
if (!Number.isInteger(bits) || bits < 0 || bits > (family === 4 ? 32 : 128)) {
throw new Error(`HTTP_REMOTE_TRUSTED_IPS: neplatný rozsah "${raw}"`);
}
blockList.addSubnet(address, bits, type);
}
return blockList;
}
/**
* Je adresa přímého protějšku spojení mezi důvěryhodnými? IPv4 mapovaná do IPv6
* („::ffff:10.42.0.84", jak ji hlásí Node na dual-stack socketu) se porovnává jako IPv4.
*/
export function isTrustedPeer(address: string | undefined, trusted: net.BlockList): boolean {
if (!address) return false;
const mapped = address.match(/^::ffff:(\d+\.\d+\.\d+\.\d+)$/i);
const normalized = mapped ? mapped[1] : address;
const family = net.isIP(normalized);
if (!family) return false;
return trusted.check(normalized, family === 4 ? 'ipv4' : 'ipv6');
}
const warnedPeers = new Set<string>();
/**
* Vrátí identitu uživatele z hlavičky proxy, ale jen pokud spojení přišlo z důvěryhodné adresy.
* Rozhoduje skutečná adresa TCP spojení (ne req.ip, které lze ovlivnit hlavičkou X-Forwarded-For).
* Hlavičku od nedůvěryhodné adresy ignoruje a jednou za běh ji zaloguje (odhalí chybnou konfiguraci).
*/
export function getTrustedRemoteUser(req: Request, headerName: string, trusted: net.BlockList): string | undefined {
const value = req.header(headerName);
if (!value) return undefined;
const peer = req.socket?.remoteAddress;
if (!isTrustedPeer(peer, trusted)) {
const key = peer ?? 'neznámá adresa';
if (!warnedPeers.has(key)) {
warnedPeers.add(key);
console.warn(`Přihlášení přes hlavičky: ignoruji hlavičku ${headerName} z nedůvěryhodné adresy ${key} (viz HTTP_REMOTE_TRUSTED_IPS)`);
}
return undefined;
}
return Buffer.from(value, 'latin1').toString();
}