Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[3.7] gh-68966: Make mailcap refuse to match unsafe filenames/types/params (GH-91993) #98191

Merged
merged 1 commit into from
Oct 11, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions Doc/library/mailcap.rst
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,18 @@ standard. However, mailcap files are supported on most Unix systems.
use) to determine whether or not the mailcap line applies. :func:`findmatch`
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 <warnings>` will be raised in either case.

.. function:: getcaps()

Expand Down
26 changes: 24 additions & 2 deletions Lib/mailcap.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import os
import warnings
import re

__all__ = ["getcaps","findmatch"]

Expand All @@ -13,6 +14,11 @@ def lineno_sort_key(entry):
else:
return 1, 0

_find_unsafe = re.compile(r'[^\xa1-\U0010FFFF\w@+=:,./-]').search

class UnsafeMailcapInput(Warning):
"""Warning raised when refusing unsafe input"""


# Part 1: top-level interface.

Expand Down Expand Up @@ -165,15 +171,22 @@ def findmatch(caps, MIMEtype, key='view', filename="/dev/null", plist=[]):
entry to use.

"""
if _find_unsafe(filename):
msg = "Refusing to use mailcap with filename %r. Use a safe temporary filename." % (filename,)
warnings.warn(msg, UnsafeMailcapInput)
return None, None
entries = lookup(caps, MIMEtype, key)
# XXX This code should somehow check for the needsterminal flag.
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):
Expand Down Expand Up @@ -206,14 +219,23 @@ def subst(field, MIMEtype, filename, plist=[]):
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
while i < n and field[i] != '}':
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
Expand Down
8 changes: 6 additions & 2 deletions Lib/test/test_mailcap.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,8 @@ def test_subst(self):
(["", "audio/*", "foo.txt"], ""),
(["echo foo", "audio/*", "foo.txt"], "echo foo"),
(["echo %s", "audio/*", "foo.txt"], "echo foo.txt"),
(["echo %t", "audio/*", "foo.txt"], "echo audio/*"),
(["echo %t", "audio/*", "foo.txt"], None),
(["echo %t", "audio/wav", "foo.txt"], "echo audio/wav"),
(["echo \\%t", "audio/*", "foo.txt"], "echo %t"),
(["echo foo", "audio/*", "foo.txt", plist], "echo foo"),
(["echo %{total}", "audio/*", "foo.txt", plist], "echo 3")
Expand Down Expand Up @@ -205,7 +206,10 @@ def test_findmatch(self):
('"An audio fragment"', audio_basic_entry)),
([c, "audio/*"],
{"filename": fname},
("/usr/local/bin/showaudio audio/*", audio_entry)),
(None, None)),
([c, "audio/wav"],
{"filename": fname},
("/usr/local/bin/showaudio audio/wav", audio_entry)),
([c, "message/external-body"],
{"plist": plist},
("showexternal /dev/null default john python.org /tmp foo bar", message_entry))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
The deprecated mailcap module now refuses to inject unsafe text (filenames,
MIME types, parameters) into shell commands. Instead of using such text, it
will warn and act as if a match was not found (or for test commands, as if
the test failed).