feat(api): Favoriten — Symbol trotz Zertifikatsfehler holen, Reihenfolge per PUT /favorites/order speichern
- icon-discovery.service.ts: undicis eigenes fetch mit Modul-Singleton
LENIENT_TLS_AGENT (Agent({ connect: { rejectUnauthorized: false } }))
als dispatcher in fetchWithRedirectGuard, der einzigen Ausgangsstelle
fuer HTML-Ermittlung und Icon-Byte-Holen; SSRF-Schutz unveraendert
- undici 7.28.0 (bereits im Lockfile aufgeloest) als direkte Abhaengigkeit
von @tessera/api via pnpm add --offline
- PUT /favorites/order (ReorderFavoritesDto) vor den :id-Routen;
FavoritesService.reorder() setzt position=index fuer die Favoriten
eines Widgets in EINER withTenantTransaction, userId+widgetId in jeder
Bedingung (zweites Netz), eine BadRequestException fuer alle
Abweichungen (T-JDD-06)
- getIcon: X-Content-Type-Options nosniff + restriktive CSP (T-JDD-02)
- 10 neue Tests (3 Dispatcher, 7 reorder); volle API-Suite 68 Dateien/
1101 Tests und type-check gruen
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { lookup } from 'dns/promises';
|
||||
import { isIP } from 'net';
|
||||
import { Agent, fetch as undiciFetch, type Response as UndiciResponse } from 'undici';
|
||||
|
||||
/**
|
||||
* Server-side favicon / icon discovery with SSRF protection (T-08-05).
|
||||
@@ -12,6 +13,9 @@ import { isIP } from 'net';
|
||||
* - 4000 ms AbortController timeout per request
|
||||
* - 200 000 character HTML cap to prevent memory exhaustion (T-08-09)
|
||||
* - Blocked hostnames: localhost, .local, 0.0.0.0
|
||||
* - 260917-jdd: Zertifikatsfehler des Zielhosts werden toleriert (siehe
|
||||
* LENIENT_TLS_AGENT unten) — DNS-Pruefung, Redirect-Limit, Timeout und
|
||||
* Groessendeckel bleiben davon unberuehrt.
|
||||
*/
|
||||
|
||||
const FALLBACK_ICON_PATH = '/favicon.ico';
|
||||
@@ -21,6 +25,27 @@ const MAX_REDIRECTS = 2;
|
||||
const MAX_HTML_CHARS = 200000;
|
||||
const MAX_ICON_BYTES = 1_000_000;
|
||||
|
||||
/**
|
||||
* 260917-jdd — Ziel ist ein Bildchen, kein Geheimnis: selbstsignierte,
|
||||
* abgelaufene oder falsch benannte Zertifikate sollen das Symbol eines
|
||||
* Favoriten nicht verhindern. Dieser Dispatcher gilt AUSSCHLIESSLICH fuer
|
||||
* die beiden Aufrufe in dieser Datei (Dispatcher pro Aufruf, keine
|
||||
* prozessweite Abschaltung der Zertifikatspruefung — insbesondere NICHT
|
||||
* ueber die Node-Umgebungsvariable, die mit NODE_TLS_ beginnt).
|
||||
*
|
||||
* Der Dispatcher wirkt nur zusammen mit undicis EIGENEM `fetch` — Nodes
|
||||
* globales `fetch` ignoriert einen Agent aus dem npm-Paket (andere Klasse,
|
||||
* Node 24 buendelt intern undici 7.25.0). Gemessen 2026-09-17 gegen
|
||||
* self-signed.badssl.com: `undiciFetch(url, { dispatcher: new Agent(...) })`
|
||||
* -> Status 200; `globalThis.fetch` derselben URL -> DEPTH_ZERO_SELF_SIGNED_CERT.
|
||||
* Deshalb der Modulimport oben statt des globalen `fetch`.
|
||||
*
|
||||
* DNS-Pruefung (isPublicHttpUrl), Redirect-Limit (MAX_REDIRECTS), Timeout
|
||||
* und Groessendeckel (MAX_ICON_BYTES/MAX_HTML_CHARS) bleiben davon
|
||||
* unberuehrt (T-JDD-01).
|
||||
*/
|
||||
const LENIENT_TLS_AGENT = new Agent({ connect: { rejectUnauthorized: false } });
|
||||
|
||||
type FetchHtmlResult = {
|
||||
html: string;
|
||||
finalUrl: string;
|
||||
@@ -243,7 +268,7 @@ function extractIconFromHtml(html: string, baseUrl: string): string | null {
|
||||
async function fetchWithRedirectGuard(
|
||||
pageUrl: URL,
|
||||
options: { accept: string; timeoutMs: number; userAgent?: string },
|
||||
): Promise<{ response: Response; finalUrl: URL } | null> {
|
||||
): Promise<{ response: UndiciResponse; finalUrl: URL } | null> {
|
||||
let currentUrl = pageUrl;
|
||||
|
||||
for (let redirectCount = 0; redirectCount <= MAX_REDIRECTS; redirectCount++) {
|
||||
@@ -255,7 +280,8 @@ async function fetchWithRedirectGuard(
|
||||
const timeout = setTimeout(() => controller.abort(), options.timeoutMs);
|
||||
|
||||
try {
|
||||
const response = await fetch(currentUrl.toString(), {
|
||||
const response = await undiciFetch(currentUrl.toString(), {
|
||||
dispatcher: LENIENT_TLS_AGENT, // 260917-jdd: siehe Kommentar an der Konstante
|
||||
redirect: 'manual', // SSRF: follow manually so each hop is re-validated
|
||||
signal: controller.signal,
|
||||
headers: {
|
||||
|
||||
Reference in New Issue
Block a user