fix(backend): use URL parser for registry scheme + template host check (#808)

Two small hardenings flagged by CodeQL:

- routes/registries.ts: drop the redundant startsWith block-list and rely
  solely on URL parsing + protocol allow-list. The startsWith pass was
  unreachable defense (any non-http/https scheme already fails the
  protocol check below it) and was tripping js/incomplete-url-scheme-check.
- services/TemplateService.ts: replace the .includes('api.linuxserver.io')
  substring match with new URL(registryUrl).hostname comparison. The
  substring form would mis-classify a malicious admin-set URL like
  https://evil.example/api.linuxserver.io/... as the LSIO registry and
  apply the LSIO response parser to its payload. Hostname compare closes
  that.

Behavioral parity for the happy path: every previously-accepted URL still
parses; the LSIO branch still triggers when the hostname is exactly
api.linuxserver.io.
This commit is contained in:
Anso
2026-04-27 11:02:10 -04:00
committed by GitHub
parent 4e5ba17710
commit 6ac02c792a
2 changed files with 7 additions and 8 deletions
+4 -7
View File
@@ -11,17 +11,14 @@ function isValidRegistryUrl(url: string, type: string): boolean {
if (type === 'dockerhub') return true;
const trimmed = url.trim();
if (!trimmed) return false;
const lower = trimmed.toLowerCase();
if (lower.startsWith('javascript:') || lower.startsWith('data:') || lower.startsWith('file:') || lower.startsWith('ftp:')) {
return false;
}
let parsed: URL;
try {
const parsed = new URL(trimmed.includes('://') ? trimmed : `https://${trimmed}`);
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') return false;
if (!parsed.hostname) return false;
parsed = new URL(trimmed.includes('://') ? trimmed : `https://${trimmed}`);
} catch {
return false;
}
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') return false;
if (!parsed.hostname) return false;
return true;
}
+3 -1
View File
@@ -233,7 +233,9 @@ export class TemplateService {
console.log(`[Templates] Fetching from registry: ${registryUrl}`);
const debug = isDebugEnabled();
if (registryUrl.includes('api.linuxserver.io')) {
let registryHost = '';
try { registryHost = new URL(registryUrl).hostname.toLowerCase(); } catch { /* invalid URL, treated as non-LSIO */ }
if (registryHost === 'api.linuxserver.io') {
const response = await axios.get<LsioApiResponse>(registryUrl, { timeout: 20_000 });
// Official LSIO API Schema Mapping
const lsioApps = response.data?.data?.repositories?.linuxserver ?? {};