CVE-2023-28321: IDN wildcard match
What the report claims
curl /libcurl uses wildcards for validation during TLS communication, even if the hostname is an IDN, violating RFC-6125 section 6.4.3.
Compare with curl's recorded outcome
Known vulnerability
A real vulnerability: CVE-2023-28321 IDN wildcard match
Shown for comparison after analysis. The pipeline did not see this label.
ProveIt's finding
The code supports the reported flaw. A maintainer should review its impact and decide what to do next.
Why this finding?
- The code at lib/vtls/hostcheck.c:105 checks `pattern` for "xn--" prefix, but `pattern` is the certificate's CN/SAN which contains wildcards (e.g., `x*.example.local`) and never starts with "xn--". The check should use `hostname`, which is the IDN in punycode (e.g., `xn--l8j.example.local`).
- RFC 6125 §6.4.3 (confirmed by erratum 3090) explicitly forbids wildcard matching when the hostname is an IDN (A-label/U-label).
- The bug allows a wildcard certificate to be accepted for an IDN hostname, enabling certificate spoofing for internationalized domain names.
- The fix is a one-word change: `pattern` → `hostname` at line 105.
How this analysis ran 5 claims · 5 citations verified
Checked against curl master on 2023-04-17 (fc2f1e547a4a). Dev evaluation split. Citation verification checks that a referenced line was in the evidence; it does not prove the model interpreted it correctly.
Review a draft reply to the reporter
Edit and verify before sending.
Thank you for the clear report. The bug is confirmed: lib/vtls/hostcheck.c:105 checks the certificate pattern for "xn--" but should check the hostname. This allows wildcard certificates to incorrectly match IDN hostnames, violating RFC 6125 §6.4.3. We will fix this by changing `pattern` to `hostname` in the strncasecompare call.
Inspect all claims and source code 5 claims · 4 code excerpts
Report claims
The code incorrectly uses pattern_label_end to check for wildcards, causing it to skip wildcard matching when the pattern contains "xn--"
Read the reasoning
The code at line 105 tests whether the certificate pattern starts with "xn--" to disable wildcard matching for IDNs. However, the pattern contains the wildcard (e.g., `x*.example.local`), so it never starts with "xn--". The hostname (e.g., `xn--l8j.example.local`) is the IDN and should be checked instead. This is a clear logic error.
The flaw occurs in lib/vtls/hostcheck.c in the function hostmatch on lines 100-106
Read the reasoning
Deterministic checks confirm the file and lines exist at the target revision.
A wildcard certificate with CN value containing "x*" is used, and the hostname contains "xn--" causing the pattern to be skipped
Read the reasoning
The trigger described matches the code path: a wildcard pattern (CN with `*`) and an IDN hostname (starting with `xn--`) will bypass the IDN check and proceed to wildcard matching at lines 124-128.
The communication succeeds even though it should result in a validation error, leading to improper validation of certificate with host mismatch
Read the reasoning
Because the IDN check is ineffective, wildcard matching proceeds and can incorrectly validate a certificate for an IDN hostname, violating RFC 6125 §6.4.3.
Create a wildcard certificate with CN `x*.example.local`, run openssl s_server, modify hosts to resolve `xn--l8j.example.local` to the local IP, then curl https://%E3%81%82.example.local --cacert server.crt
Read the reasoning
The steps are concrete and map directly to the flawed code path.
What the report says about other projects
confirmed RFC 6125 section 6.4.3 requires that clients SHOULD NOT attempt to match a presented identifier where the wildcard character is embedded within an A-label or U-label of an internationalized domain name.
“However, the client SHOULD NOT attempt to match a presented identifier where the wildcard character is embedded within an A-label or U-label [IDNA-DEFS] of an internationalized domain name [IDNA-PROTO].” source
Read the original report
Source code
| 75 | */ |
| 76 | |
| 77 | static bool hostmatch(const char *hostname, |
| C5: The reproduction steps exercise the `hostmatch` function with a wildcard pattern and an IDN hostname, which the code mishandles. | |
| 78 | size_t hostlen, |
| 79 | const char *pattern, |
| 80 | size_t patternlen) |
| 81 | { |
| 82 | const char *pattern_label_end, *wildcard, *hostname_label_end; |
| 83 | size_t prefixlen, suffixlen; |
| 84 | |
| 85 | /* normalize pattern and hostname by stripping off trailing dots */ |
| 86 | DEBUGASSERT(patternlen); |
| 87 | if(hostname[hostlen-1]=='.') |
| 88 | hostlen--; |
| 89 | if(pattern[patternlen-1]=='.') |
| 90 | patternlen--; |
| 91 | |
| 92 | wildcard = memchr(pattern, '*', patternlen); |
| C3: Line 92 finds wildcard in pattern; lines 100-106 decide whether to skip wildcard matching. The condition at line 105 incorrectly uses `pattern` instead of `hostname`. | |
| 93 | if(!wildcard) |
| 94 | return pmatch(hostname, hostlen, pattern, patternlen); |
| 95 | |
| 96 | /* detect IP address as hostname and fail the match if so */ |
| 97 | if(Curl_host_is_ipnum(hostname)) |
| 98 | return FALSE; |
| 99 | |
| 100 | /* We require at least 2 dots in the pattern to avoid too wide wildcard |
| C2: The function `hostmatch` at lines 77-129 contains the cited logic at lines 100-106 exactly as described. | |
| 101 | match. */ |
| 102 | pattern_label_end = memchr(pattern, '.', patternlen); |
| 103 | if(!pattern_label_end || |
| 104 | (memrchr(pattern, '.', patternlen) == pattern_label_end) || |
| 105 | strncasecompare(pattern, "xn--", 4)) |
| C1: Line 105 checks `strncasecompare(pattern, "xn--", 4)` but should check `hostname` per RFC 6125 §6.4.3. The pattern is the certificate's CN/SAN (may contain wildcards like `x*.example.local`); the hostname is the IDN in punycode (starts with `xn--`). The current check never triggers for wildcard patterns. | |
| 106 | return pmatch(hostname, hostlen, pattern, patternlen); |
| 107 | |
| 108 | hostname_label_end = memchr(hostname, '.', hostlen); |
| 109 | if(!hostname_label_end) |
| 110 | return FALSE; |
| 111 | else { |
| 112 | size_t skiphost = hostname_label_end - hostname; |
| 113 | size_t skiplen = pattern_label_end - pattern; |
| 114 | if(!pmatch(hostname_label_end, hostlen - skiphost, |
| 115 | pattern_label_end, patternlen - skiplen)) |
| 116 | return FALSE; |
| 117 | } |
| 118 | /* The wildcard must match at least one character, so the left-most |
| 119 | label of the hostname is at least as large as the left-most label |
| 120 | of the pattern. */ |
| 121 | if(hostname_label_end - hostname < pattern_label_end - pattern) |
| 122 | return FALSE; |
| 123 | |
| 124 | prefixlen = wildcard - pattern; |
| 125 | suffixlen = pattern_label_end - (wildcard + 1); |
| 126 | return strncasecompare(pattern, hostname, prefixlen) && |
| C4: Lines 124-128 perform wildcard prefix/suffix matching when the IDN check at line 105 fails to trigger. | |
| 127 | strncasecompare(wildcard + 1, hostname_label_end - suffixlen, |
| 128 | suffixlen) ? TRUE : FALSE; |
| 129 | } |
| 130 | |
| 131 | /* |
| 39 | #define strncasecompare(a,b,c) curl_strnequal(a,b,c) |
Show 1 lines (not cited by the judge)
| 42 | #define memrchr(x,y,z) Curl_memrchr((x),(y),(z)) |
Show 7 lines (not cited by the judge)
| 50 | static bool pmatch(const char *hostname, size_t hostlen, |
| 51 | const char *pattern, size_t patternlen) |
| 52 | { |
| 53 | if(hostlen != patternlen) |
| 54 | return FALSE; |
| 55 | return strncasecompare(hostname, pattern, hostlen); |
| 56 | } |