round33: API-Scanner Kontrollprobe gegen Geraete, die jede Anfrage einheitlich beantworten (false positives behoben)

This commit is contained in:
2026-07-24 17:26:37 +02:00
parent ba057e7911
commit 70da526921

View File

@@ -106,32 +106,48 @@ function looksLikeJson(body: string): boolean {
return trimmed.startsWith("{") || trimmed.startsWith("["); return trimmed.startsWith("{") || trimmed.startsWith("[");
} }
function isApiLikeResponse(result: RawProbeResult): boolean {
const contentTypeIsApi =
!!result.contentType &&
(result.contentType.includes("json") ||
result.contentType.includes("graphql") ||
result.contentType.includes("xml"));
const bodyIsJson = looksLikeJson(result.bodySnippet);
const hasAuthChallenge = !!result.wwwAuthenticate;
return contentTypeIsApi || bodyIsJson || hasAuthChallenge;
}
/** /**
* Prüft die kuratierten Kandidaten-Pfade unter einer Basis-URL parallel und * Prüft die kuratierten Kandidaten-Pfade unter einer Basis-URL parallel und
* liefert alle, die auf eine tatsächlich vorhandene API hindeuten: * liefert alle, die auf eine tatsächlich vorhandene API hindeuten:
* - eine JSON-Antwort (egal ob 200 oder z. B. 401 "unauthorized" - eine * - eine JSON-Antwort (egal ob 200 oder z. B. 401 "unauthorized" - eine
* JSON-Fehlermeldung zeigt trotzdem "hier läuft eine API"), * JSON-Fehlermeldung zeigt trotzdem "hier läuft eine API"),
* - ein Content-Type, der explizit auf JSON/GraphQL/XML-API hindeutet, * - ein Content-Type, der explizit auf JSON/GraphQL/XML-API hindeutet,
* - oder ein "WWW-Authenticate"-Header (401 mit diesem Header ist ein sehr * - oder ein "WWW-Authenticate"-Header (401 mit diesem Header ist normalerweise
* starkes Signal für eine authentifizierungspflichtige API, selbst wenn * ein starkes Signal für eine authentifizierungspflichtige API).
* der Body selbst nur eine schlichte Textmeldung ist).
* Reine HTML-Seiten (z. B. eine 404-Fehlerseite des Frontends oder eine * Reine HTML-Seiten (z. B. eine 404-Fehlerseite des Frontends oder eine
* Login-Weiterleitung ohne API-Signal) zählen nicht. * Login-Weiterleitung ohne API-Signal) zählen nicht.
*
* VORAB ein Kontroll-Check gegen einen garantiert nicht existierenden Pfad:
* manche Geräte (z. B. IP-Kameras) beantworten JEDE Anfrage einheitlich mit
* z. B. Basic-Auth-Aufforderung oder demselben JSON-Fehlerobjekt - dann
* würden buchstäblich ALLE Kandidaten-Pfade fälschlich als "API gefunden"
* durchgehen (siehe Bugreport: eine Reolink-Türklingel "hatte" plötzlich
* Home-Assistant-, Proxmox- UND Portainer-APIs). Reagiert das Kontroll-Pfad
* genauso "positiv" wie ein echter Treffer, ist dem Gerät für diese Prüfung
* grundsätzlich nicht zu trauen - dann lieber gar kein Ergebnis als 30 falsche.
*/ */
export async function detectApis(baseUrl: string): Promise<DetectedApi[]> { export async function detectApis(baseUrl: string): Promise<DetectedApi[]> {
const control = await fetchRaw(`${baseUrl}/__launchpad_api_probe_${Date.now()}__`);
if (control && control.status !== 404 && control.status !== 0 && isApiLikeResponse(control)) {
return [];
}
const checks = await Promise.all( const checks = await Promise.all(
CANDIDATE_PATHS.map(async ({ path, type }) => { CANDIDATE_PATHS.map(async ({ path, type }) => {
const result = await fetchRaw(`${baseUrl}${path}`); const result = await fetchRaw(`${baseUrl}${path}`);
if (!result || result.status === 0 || result.status === 404) return null; if (!result || result.status === 0 || result.status === 404) return null;
if (!isApiLikeResponse(result)) return null;
const contentTypeIsApi =
result.contentType?.includes("json") ||
result.contentType?.includes("graphql") ||
result.contentType?.includes("xml");
const bodyIsJson = looksLikeJson(result.bodySnippet);
const hasAuthChallenge = !!result.wwwAuthenticate;
if (!contentTypeIsApi && !bodyIsJson && !hasAuthChallenge) return null;
const detected: DetectedApi = { path, type, status: result.status }; const detected: DetectedApi = { path, type, status: result.status };
return detected; return detected;