VYPR
High severityNVD Advisory· Published Mar 24, 2026· Updated Mar 25, 2026

Froxlor is vulnerable to BIND zone file injection via unsanitized DNS record content in DomainZones API

CVE-2026-30932

Description

Froxlor is open source server administration software. Prior to version 2.3.5, the DomainZones.add API endpoint (accessible to customers with DNS enabled) does not validate the content field for several DNS record types (LOC, RP, SSHFP, TLSA). An attacker can inject newlines and BIND zone file directives (e.g. $INCLUDE) into the zone file that gets written to disk when the DNS rebuild cron job runs. This issue has been patched in version 2.3.5.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
froxlor/froxlorPackagist
< 2.3.52.3.5

Affected products

1

Patches

1
b34829262dc3

validate dns LOC, RP, SSHFP and TLSA record-content according to specifications

https://github.com/froxlor/froxlorMichael KaufmannFeb 24, 2026via ghsa
4 files changed · +203 4
  • lib/Froxlor/Api/Commands/DomainZones.php+24 4 modified
    @@ -211,7 +211,12 @@ public function add()
     			// append trailing dot (again)
     			$content .= '.';
     		} elseif ($type == 'LOC' && !empty($content)) {
    -			$content = $content;
    +			if (!Validate::validateDnsLoc($content)) {
    +				$errors[] = lng('error.dns_loc_invalid');
    +			} else {
    +				// keep content
    +				$content = $content;
    +			}
     		} elseif ($type == 'MX') {
     			if ($prio === null || $prio < 0) {
     				$errors[] = lng('error.dns_mx_prioempty');
    @@ -251,7 +256,12 @@ public function add()
     			// append trailing dot (again)
     			$content .= '.';
     		} elseif ($type == 'RP' && !empty($content)) {
    -			$content = $content;
    +			if (!Validate::validateDnsRp($content)) {
    +				$errors[] = lng('error.dns_rp_invalid');
    +			} else {
    +				// keep content
    +				$content = $content;
    +			}
     		} elseif ($type == 'SRV') {
     			if ($prio === null || $prio < 0) {
     				$errors[] = lng('error.dns_srv_prioempty');
    @@ -288,9 +298,19 @@ public function add()
     				$content .= '.';
     			}
     		} elseif ($type == 'SSHFP' && !empty($content)) {
    -			$content = $content;
    +			if (!Validate::validateDnsSshfp($content)) {
    +				$errors[] = lng('error.dns_sshfp_invalid');
    +			} else {
    +				// keep content
    +				$content = $content;
    +			}
     		} elseif ($type == 'TLSA' && !empty($content)) {
    -			$content = $content;
    +			if (!Validate::validateDnsTlsa($content)) {
    +				$errors[] = lng('error.dns_tlsa_invalid');
    +			} else {
    +				// keep content
    +				$content = $content;
    +			}
     		} elseif ($type == 'TXT' && !empty($content)) {
     			// check that TXT content is enclosed in " "
     			$content = Dns::encloseTXTContent($content);
    
  • lib/Froxlor/Validate/Validate.php+171 0 modified
    @@ -386,4 +386,175 @@ public static function validateBase64Image(string $base64string)
     		// If everything is okay, return true
     		return true;
     	}
    +
    +	public static function validateDnsLoc(string $input)
    +	{
    +		$pattern = '/^
    +        (\d{1,2})\s+                # latitude degrees
    +        (\d{1,2})\s+                # latitude minutes
    +        (\d{1,2}(?:\.\d+)?)\s+      # latitude seconds
    +        ([NS])\s+                   # latitude direction
    +        (\d{1,3})\s+                # longitude degrees
    +        (\d{1,2})\s+                # longitude minutes
    +        (\d{1,2}(?:\.\d+)?)\s+      # longitude seconds
    +        ([EW])\s+                   # longitude direction
    +        (-?\d+(?:\.\d+)?)m          # altitude
    +        (?:\s+(\d+(?:\.\d+)?)m      # size (optional)
    +        (?:\s+(\d+(?:\.\d+)?)m      # horiz precision (optional)
    +        (?:\s+(\d+(?:\.\d+)?)m)?    # vert precision (optional)
    +        )?)?$/x';
    +
    +		if (!preg_match($pattern, $input, $matches)) {
    +			return false;
    +		}
    +
    +		[
    +			,
    +			$latDeg, $latMin, $latSec, $latDir,
    +			$lonDeg, $lonMin, $lonSec, $lonDir,
    +			$alt,
    +			$size, $hPrec, $vPrec
    +		] = $matches + array_fill(0, 13, null);
    +
    +		// Range checks
    +		if ($latDeg > 90) return false;
    +		if ($latMin > 59) return false;
    +		if ($latSec >= 60) return false;
    +
    +		if ($lonDeg > 180) return false;
    +		if ($lonMin > 59) return false;
    +		if ($lonSec >= 60) return false;
    +
    +		return $input;
    +	}
    +
    +	public static function validateDnsRp(string $input)
    +	{
    +		$parts = preg_split('/\s+/', trim($input));
    +
    +		if (count($parts) !== 2) {
    +			return false;
    +		}
    +
    +		[$mboxDname, $txtDname] = $parts;
    +
    +		// remove trailing dot if any
    +		$mboxDname = rtrim($mboxDname, '.');
    +		$txtDname  = rtrim($txtDname, '.');
    +
    +		if (!self::validateDomain($mboxDname)) {
    +			return false;
    +		}
    +
    +		if (!self::validateDomain($txtDname)) {
    +			return false;
    +		}
    +
    +		return $input;
    +	}
    +
    +	public static function validateDnsSshfp(string $input)
    +	{
    +		$parts = preg_split('/\s+/', trim($input));
    +
    +		if (count($parts) !== 3) {
    +			return false;
    +		}
    +
    +		[$algorithm, $type, $fingerprint] = $parts;
    +
    +		// ---- algorithm ----
    +		$validAlgorithms = [1, 2, 3, 4, 6];
    +
    +		if (!ctype_digit($algorithm) || !in_array((int)$algorithm, $validAlgorithms, true)) {
    +			return false;
    +		}
    +
    +		// ---- fingerprint type ----
    +		$validTypes = [1, 2];
    +
    +		if (!ctype_digit($type) || !in_array((int)$type, $validTypes, true)) {
    +			return false;
    +		}
    +
    +		// ---- check fingerprint ----
    +		if (!ctype_xdigit($fingerprint)) {
    +			return false;
    +		}
    +
    +		$type = (int)$type;
    +
    +		switch ($type) {
    +			case 1: // SHA-1
    +				$expectedLength = 40;
    +				break;
    +
    +			case 2: // SHA-256
    +				$expectedLength = 64;
    +				break;
    +
    +			default:
    +				$expectedLength = 0;
    +				break;
    +		}
    +
    +		if (strlen($fingerprint) !== $expectedLength) {
    +			return false;
    +		}
    +
    +		return $input;
    +	}
    +
    +	public static function validateDnsTlsa(string $input)
    +	{
    +		$parts = preg_split('/\s+/', trim($input));
    +
    +		if (count($parts) !== 4) {
    +			return false;
    +		}
    +
    +		[$usage, $selector, $matchingType, $data] = $parts;
    +
    +		// ---- usage ----
    +		$validUsage = [0, 1, 2, 3];
    +
    +		if (!ctype_digit($usage) || !in_array((int)$usage, $validUsage, true)) {
    +			return false;
    +		}
    +
    +		// ---- selector ----
    +		$validSelector = [0, 1];
    +
    +		if (!ctype_digit($selector) || !in_array((int)$selector, $validSelector, true)) {
    +			return false;
    +		}
    +
    +		// ---- matching type ----
    +		$validMatching = [0, 1, 2];
    +
    +		if (!ctype_digit($matchingType) || !in_array((int)$matchingType, $validMatching, true)) {
    +			return false;
    +		}
    +
    +		// ---- certificate association data ----
    +		if (!ctype_xdigit($data)) {
    +			return false;
    +		}
    +
    +		$matchingType = (int)$matchingType;
    +
    +		if ($matchingType === 1 && strlen($data) !== 64) {
    +			return false; // SHA-256
    +		}
    +
    +		if ($matchingType === 2 && strlen($data) !== 128) {
    +			return false; // SHA-512
    +		}
    +
    +		if ($matchingType === 0 && strlen($data) < 2) {
    +			return false; // at least 1 byte hex
    +		}
    +
    +		return $input;
    +	}
     }
    
  • lng/de.lng.php+4 0 modified
    @@ -969,6 +969,10 @@
     		'dns_srv_noalias' => 'Der SRV Eintrag darf kein CNAME Eintrag sein.',
     		'dns_duplicate_entry' => 'Eintrag existiert bereits',
     		'dns_notfoundorallowed' => 'Domain nicht gefunden oder keine Berechtigung',
    +		'dns_loc_invalid' => 'Ungültiger LOC Eintrag',
    +		'dns_rp_invalid' => 'Ungültiger RP Eintrag',
    +		'dns_sshfp_invalid' => 'Ungültiger SSHFP Eintrag',
    +		'dns_tlsa_invalid' => 'Ungültiger TLSA Eintrag',
     		'domain_nopunycode' => 'Die Eingabe von Punycode (IDNA) ist nicht notwendig. Die Domain wird automatisch konvertiert.',
     		'domain_noipaddress' => 'Eine IP-Adresse kann nicht als Domain angelegt werden',
     		'dns_record_toolong' => 'Records/Labels können maximal 63 Zeichen lang sein',
    
  • lng/en.lng.php+4 0 modified
    @@ -1040,6 +1040,10 @@
     		'dns_srv_noalias' => 'The SRV-target value cannot be an CNAME entry.',
     		'dns_duplicate_entry' => 'Record already exists',
     		'dns_notfoundorallowed' => 'Domain not found or no permission',
    +		'dns_loc_invalid' => 'The LOC record has invalid content',
    +		'dns_rp_invalid' => 'The RP record has invalid content',
    +		'dns_sshfp_invalid' => 'The SSHFP record has invalid content',
    +		'dns_tlsa_invalid' => 'The TLSA record has invalid content',
     		'domain_nopunycode' => 'You must not specify punycode (IDNA). The domain will automatically be converted',
     		'domain_noipaddress' => 'Cannot add an IP address as domain',
     		'dns_record_toolong' => 'Records/labels can only be up to 63 characters',
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

5

News mentions

0

No linked articles in our index yet.