From 5b0f3ffc6b42f6aa1373ccd05ac34b6f771b9d82 Mon Sep 17 00:00:00 2001 From: Daniel Pouzzner Date: Thu, 9 Apr 2026 19:13:32 -0500 Subject: [PATCH] src/internal.c: add IsValidFQDN(), and in MatchDomainName(), do pattern matching and case folding only if target string IsValidFQDN(). --- src/internal.c | 85 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) diff --git a/src/internal.c b/src/internal.c index ad1587e0f6..26ad423897 100644 --- a/src/internal.c +++ b/src/internal.c @@ -13272,6 +13272,78 @@ static int MatchIPv6(const char* pattern, int patternLen, } #endif /* WOLFSSL_IP_ALT_NAME && !WOLFSSL_USER_IO */ +/* Returns 1 if name is a syntactically valid DNS FQDN per RFC 952/1123. + * + * Rules enforced: + * - Total effective length (excluding optional trailing dot) in [1, 253] + * - Each label is 1-63 octets of [a-zA-Z0-9-] + * - No label starts or ends with '-' + * - At least two labels (single-label names are not "fully qualified") + * - Final label (TLD) contains at least one letter (rejects all-numeric + * strings that could be confused with IPv4 literals, and matches the + * ICANN constraint that TLDs are alphabetic) + * - Optional trailing dot is accepted (absolute FQDN form) + * - Internationalized names are valid in their ACE/punycode (xn--) form + */ +static int IsValidFQDN(const char* name, word32 nameSz) +{ + word32 i; + int labelLen = 0; + int labelCount = 0; + int tldHasAlpha = 0; /* tracks alpha presence in the *current* label */ + + if (name == NULL || nameSz <= 0) + return 0; + + /* Strip a single optional trailing dot before measuring. "example.com." + * is the absolute form of the same FQDN. + */ + if (name[nameSz - 1] == '.') + --nameSz; + + if (nameSz < 1 || nameSz > 253) + return 0; + + for (i = 0; i < nameSz; i++) { + byte c = (byte)name[i]; + + if (c == '.') { + if (labelLen == 0 || name[i - 1] == '-') + return 0; /* empty label or label ending in '-' */ + ++labelCount; + labelLen = 0; + tldHasAlpha = 0; /* reset for next label */ + continue; + } + + if (++labelLen > 63) + return 0; + + if (c == '-') { + if (labelLen == 1) /* label starts with '-' */ + return 0; + } + else if (((c | 0x20) >= 'a') && ((c | 0x20) <= 'z')) { + tldHasAlpha = 1; + } + else if ((c < '0') || (c > '9')) { + return 0; /* character outside [a-zA-Z0-9-] */ + } + } + + /* Final label (no trailing dot in the effective range to close it) */ + if ((labelLen == 0) || (name[nameSz - 1] == '-')) + return 0; + ++labelCount; + + /* Technically, "Fully qualified" requires at least two labels, and the TLD + * must not be purely numeric (no such TLD exists; also rejects bare IPv4). + * We enforce the not-purely-numeric rule, but we allow single labels, so + * that ad hoc "localhost" CN is allowed. + */ + return ((labelCount > 0) && tldHasAlpha); +} + /* Match names with wildcards, each wildcard can represent a single name component or fragment but not multiple names, i.e., *.z.com matches y.z.com but not x.y.z.com @@ -13296,6 +13368,19 @@ int MatchDomainName(const char* pattern, int patternLen, const char* str, return 1; #endif + if (! IsValidFQDN(str, strLen)) { + /* Not a valid FQDN or IPv6 address -- require byte-exact match, no case + * folding, no wildcard interpretation. This is appropriate for an IPv4 + * match, for example. + */ + return (((word32)patternLen == strLen) && + (XMEMCMP(pattern, str, patternLen) == 0)); + } + + /* strip trailing dot if necessary (FQDN designator). */ + if (str[strLen-1] == '.') + --strLen; + while (patternLen > 0) { /* Get the next pattern char to evaluate */ char p = (char)XTOLOWER((unsigned char)*pattern);