--- Doc/library/mailcap.rst | 13 +++++++++++++ Lib/mailcap.py | 28 ++++++++++++++++++++++++++-- 2 files changed, 39 insertions(+), 2 deletions(-) --- a/Doc/library/mailcap.rst +++ b/Doc/library/mailcap.rst @@ -55,6 +55,19 @@ standard. However, mailcap files are su will automatically check such conditions and skip the entry if the check fails. +.. versionchanged:: 3.11 + + To prevent security issues with shell metacharacters (symbols that have + special effects in a shell command line), ``findmatch`` will refuse + to inject ASCII characters other than alphanumerics and ``@+=:,./-_`` + into the returned command line. + + If a disallowed character appears in *filename*, ``findmatch`` will always + return ``(None, None)`` as if no entry was found. + If such a character appears elsewhere (a value in *plist* or in *MIMEtype*), + ``findmatch`` will ignore all mailcap entries which use that value. + A :mod:`warning ` will be raised in either case. + .. function:: getcaps() Returns a dictionary mapping MIME types to a list of mailcap file entries. This --- a/Lib/mailcap.py +++ b/Lib/mailcap.py @@ -1,9 +1,17 @@ """Mailcap file handling. See RFC 1524.""" import os +import warnings +import re __all__ = ["getcaps","findmatch"] +_find_unsafe = re.compile(ur'[^\xa1-\U0010FFFF\w@+=:,./-]').search + +class UnsafeMailcapInput(Warning): + """Warning raised when refusing unsafe input""" + + # Part 1: top-level interface. def getcaps(): @@ -18,6 +26,10 @@ def getcaps(): """ caps = {} for mailcap in listmailcapfiles(): + if _find_unsafe(mailcap): + msg = "Refusing to use mailcap with filename %r. Use a safe temporary filename." % (mailcap,) + warnings.warn(msg, UnsafeMailcapInput) + return None, None try: fp = open(mailcap, 'r') except IOError: @@ -149,10 +161,13 @@ def findmatch(caps, MIMEtype, key='view' for e in entries: if 'test' in e: test = subst(e['test'], filename, plist) + if test is None: + continue if test and os.system(test) != 0: continue command = subst(e[key], MIMEtype, filename, plist) - return command, e + if command is not None: + return command, e return None, None def lookup(caps, MIMEtype, key=None): @@ -184,6 +199,10 @@ def subst(field, MIMEtype, filename, pli elif c == 's': res = res + filename elif c == 't': + if _find_unsafe(MIMEtype): + msg = "Refusing to substitute MIME type %r into a shell command." % (MIMEtype,) + warnings.warn(msg, UnsafeMailcapInput) + return None res = res + MIMEtype elif c == '{': start = i @@ -191,7 +210,12 @@ def subst(field, MIMEtype, filename, pli i = i+1 name = field[start:i] i = i+1 - res = res + findparam(name, plist) + param = findparam(name, plist) + if _find_unsafe(param): + msg = "Refusing to substitute parameter %r (%s) into a shell command" % (param, name) + warnings.warn(msg, UnsafeMailcapInput) + return None + res = res + param # XXX To do: # %n == number of parts if type is multipart/* # %F == list of alternating type and filename for parts