Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 72c98d3

Browse files
committed
Issue #17997: Change behavior of ssl.match_hostname() to follow RFC 6125,
for security reasons. It now doesn't match multiple wildcards nor wildcards inside IDN fragments.
1 parent ca580f4 commit 72c98d3

4 files changed

Lines changed: 97 additions & 32 deletions

File tree

Doc/library/ssl.rst

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -283,10 +283,10 @@ Certificate handling
283283
Verify that *cert* (in decoded format as returned by
284284
:meth:`SSLSocket.getpeercert`) matches the given *hostname*. The rules
285285
applied are those for checking the identity of HTTPS servers as outlined
286-
in :rfc:`2818`, except that IP addresses are not currently supported.
287-
In addition to HTTPS, this function should be suitable for checking the
288-
identity of servers in various SSL-based protocols such as FTPS, IMAPS,
289-
POPS and others.
286+
in :rfc:`2818` and :rfc:`6125`, except that IP addresses are not currently
287+
supported. In addition to HTTPS, this function should be suitable for
288+
checking the identity of servers in various SSL-based protocols such as
289+
FTPS, IMAPS, POPS and others.
290290

291291
:exc:`CertificateError` is raised on failure. On success, the function
292292
returns nothing::
@@ -301,6 +301,13 @@ Certificate handling
301301

302302
.. versionadded:: 3.2
303303

304+
.. versionchanged:: 3.3.3
305+
The function now follows :rfc:`6125`, section 6.4.3 and does neither
306+
match multiple wildcards (e.g. ``*.*.com`` or ``*a*.example.org``) nor
307+
a wildcard inside an internationalized domain names (IDN) fragment.
308+
IDN A-labels such as ``www*.xn--pthon-kva.org`` are still supported,
309+
but ``x*.python.org`` no longer matches ``xn--tda.python.org``.
310+
304311
.. function:: cert_time_to_seconds(timestring)
305312

306313
Returns a floating-point value containing a normal seconds-after-the-epoch

Lib/ssl.py

Lines changed: 50 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -129,31 +129,59 @@ class CertificateError(ValueError):
129129
pass
130130

131131

132-
def _dnsname_to_pat(dn, max_wildcards=1):
132+
def _dnsname_match(dn, hostname, max_wildcards=1):
133+
"""Matching according to RFC 6125, section 6.4.3
134+
135+
http://tools.ietf.org/html/rfc6125#section-6.4.3
136+
"""
133137
pats = []
134-
for frag in dn.split(r'.'):
135-
if frag.count('*') > max_wildcards:
136-
# Issue #17980: avoid denials of service by refusing more
137-
# than one wildcard per fragment. A survey of established
138-
# policy among SSL implementations showed it to be a
139-
# reasonable choice.
140-
raise CertificateError(
141-
"too many wildcards in certificate DNS name: " + repr(dn))
142-
if frag == '*':
143-
# When '*' is a fragment by itself, it matches a non-empty dotless
144-
# fragment.
145-
pats.append('[^.]+')
146-
else:
147-
# Otherwise, '*' matches any dotless fragment.
148-
frag = re.escape(frag)
149-
pats.append(frag.replace(r'\*', '[^.]*'))
150-
return re.compile(r'\A' + r'\.'.join(pats) + r'\Z', re.IGNORECASE)
138+
if not dn:
139+
return False
140+
141+
leftmost, *remainder = dn.split(r'.')
142+
143+
wildcards = leftmost.count('*')
144+
if wildcards > max_wildcards:
145+
# Issue #17980: avoid denials of service by refusing more
146+
# than one wildcard per fragment. A survery of established
147+
# policy among SSL implementations showed it to be a
148+
# reasonable choice.
149+
raise CertificateError(
150+
"too many wildcards in certificate DNS name: " + repr(dn))
151+
152+
# speed up common case w/o wildcards
153+
if not wildcards:
154+
return dn.lower() == hostname.lower()
155+
156+
# RFC 6125, section 6.4.3, subitem 1.
157+
# The client SHOULD NOT attempt to match a presented identifier in which
158+
# the wildcard character comprises a label other than the left-most label.
159+
if leftmost == '*':
160+
# When '*' is a fragment by itself, it matches a non-empty dotless
161+
# fragment.
162+
pats.append('[^.]+')
163+
elif leftmost.startswith('xn--') or hostname.startswith('xn--'):
164+
# RFC 6125, section 6.4.3, subitem 3.
165+
# The client SHOULD NOT attempt to match a presented identifier
166+
# where the wildcard character is embedded within an A-label or
167+
# U-label of an internationalized domain name.
168+
pats.append(re.escape(leftmost))
169+
else:
170+
# Otherwise, '*' matches any dotless string, e.g. www*
171+
pats.append(re.escape(leftmost).replace(r'\*', '[^.]*'))
172+
173+
# add the remaining fragments, ignore any wildcards
174+
for frag in remainder:
175+
pats.append(re.escape(frag))
176+
177+
pat = re.compile(r'\A' + r'\.'.join(pats) + r'\Z', re.IGNORECASE)
178+
return pat.match(hostname)
151179

152180

153181
def match_hostname(cert, hostname):
154182
"""Verify that *cert* (in decoded format as returned by
155-
SSLSocket.getpeercert()) matches the *hostname*. RFC 2818 rules
156-
are mostly followed, but IP addresses are not accepted for *hostname*.
183+
SSLSocket.getpeercert()) matches the *hostname*. RFC 2818 and RFC 6125
184+
rules are followed, but IP addresses are not accepted for *hostname*.
157185
158186
CertificateError is raised on failure. On success, the function
159187
returns nothing.
@@ -164,7 +192,7 @@ def match_hostname(cert, hostname):
164192
san = cert.get('subjectAltName', ())
165193
for key, value in san:
166194
if key == 'DNS':
167-
if _dnsname_to_pat(value).match(hostname):
195+
if _dnsname_match(value, hostname):
168196
return
169197
dnsnames.append(value)
170198
if not dnsnames:
@@ -175,7 +203,7 @@ def match_hostname(cert, hostname):
175203
# XXX according to RFC 2818, the most specific Common Name
176204
# must be used.
177205
if key == 'commonName':
178-
if _dnsname_to_pat(value).match(hostname):
206+
if _dnsname_match(value, hostname):
179207
return
180208
dnsnames.append(value)
181209
if len(dnsnames) > 1:

Lib/test/test_ssl.py

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -344,11 +344,7 @@ def fail(cert, hostname):
344344
fail(cert, 'Xa.com')
345345
fail(cert, '.a.com')
346346

347-
cert = {'subject': ((('commonName', 'a.*.com'),),)}
348-
ok(cert, 'a.foo.com')
349-
fail(cert, 'a..com')
350-
fail(cert, 'a.com')
351-
347+
# only match one left-most wildcard
352348
cert = {'subject': ((('commonName', 'f*.com'),),)}
353349
ok(cert, 'foo.com')
354350
ok(cert, 'f.com')
@@ -363,6 +359,36 @@ def fail(cert, hostname):
363359
fail(cert, 'example.org')
364360
fail(cert, 'null.python.org')
365361

362+
# error cases with wildcards
363+
cert = {'subject': ((('commonName', '*.*.a.com'),),)}
364+
fail(cert, 'bar.foo.a.com')
365+
fail(cert, 'a.com')
366+
fail(cert, 'Xa.com')
367+
fail(cert, '.a.com')
368+
369+
cert = {'subject': ((('commonName', 'a.*.com'),),)}
370+
fail(cert, 'a.foo.com')
371+
fail(cert, 'a..com')
372+
fail(cert, 'a.com')
373+
374+
# wildcard doesn't match IDNA prefix 'xn--'
375+
idna = 'püthon.python.org'.encode("idna").decode("ascii")
376+
cert = {'subject': ((('commonName', idna),),)}
377+
ok(cert, idna)
378+
cert = {'subject': ((('commonName', 'x*.python.org'),),)}
379+
fail(cert, idna)
380+
cert = {'subject': ((('commonName', 'xn--p*.python.org'),),)}
381+
fail(cert, idna)
382+
383+
# wildcard in first fragment and IDNA A-labels in sequent fragments
384+
# are supported.
385+
idna = 'www*.pythön.org'.encode("idna").decode("ascii")
386+
cert = {'subject': ((('commonName', idna),),)}
387+
ok(cert, 'www.pythön.org'.encode("idna").decode("ascii"))
388+
ok(cert, 'www1.pythön.org'.encode("idna").decode("ascii"))
389+
fail(cert, 'ftp.pythön.org'.encode("idna").decode("ascii"))
390+
fail(cert, 'pythön.org'.encode("idna").decode("ascii"))
391+
366392
# Slightly fake real-world example
367393
cert = {'notAfter': 'Jun 26 21:41:46 2011 GMT',
368394
'subject': ((('commonName', 'linuxfrz.org'),),),
@@ -423,7 +449,7 @@ def fail(cert, hostname):
423449
cert = {'subject': ((('commonName', 'a*b.com'),),)}
424450
ok(cert, 'axxb.com')
425451
cert = {'subject': ((('commonName', 'a*b.co*'),),)}
426-
ok(cert, 'axxb.com')
452+
fail(cert, 'axxb.com')
427453
cert = {'subject': ((('commonName', 'a*b*.com'),),)}
428454
with self.assertRaises(ssl.CertificateError) as cm:
429455
ssl.match_hostname(cert, 'axxbxxc.com')

Misc/NEWS

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,10 @@ Core and Builtins
8181
Library
8282
-------
8383

84+
- Issue #17997: Change behavior of ``ssl.match_hostname()`` to follow RFC 6125,
85+
for security reasons. It now doesn't match multiple wildcards nor wildcards
86+
inside IDN fragments.
87+
8488
- Issue #16039: CVE-2013-1752: Change use of readline in imaplib module to limit
8589
line length. Patch by Emil Lind.
8690

0 commit comments

Comments
 (0)