Skip to content

Commit dd9ded0

Browse files
committed
BUG#37145655: MySQL Connector/Python Configuration Files RCE
Change-Id: I7e41ba87775e8e7ef871e797ab2cd5adfd48645f
1 parent 90fb325 commit dd9ded0

File tree

4 files changed

+97
-13
lines changed

4 files changed

+97
-13
lines changed

CHANGES.txt

+1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ v9.2.0
1212
======
1313

1414
- WL16285: Remake Multi Statement Execution
15+
- BUG#37145655: MySQL Connector/Python Configuration Files RCE
1516
- BUG#36126909: "Unread result found" exception/bad MySQLCursor.statement when query text contains code comments
1617
- BUG#35810050: Executing multiple statements fails when importing Sakila
1718

mysql-connector-python/lib/mysql/connector/optionfiles.py

+19-11
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030

3131
"""Implements parser to parse MySQL option files."""
3232

33+
import ast
3334
import codecs
3435
import io
3536
import os
@@ -74,9 +75,11 @@ def read_option_files(**config: Union[str, List[str]]) -> Dict[str, Any]:
7475
for group in groups:
7576
try:
7677
for option, value in config_from_file[group].items():
78+
value += (group,)
7779
try:
7880
if option == "socket":
7981
option = "unix_socket"
82+
option_parser.set(group, "unix_socket", value[0])
8083

8184
if option not in CNX_POOL_ARGS and option != "failover":
8285
_ = DEFAULT_CONFIGURATION[option]
@@ -94,17 +97,22 @@ def read_option_files(**config: Union[str, List[str]]) -> Dict[str, Any]:
9497
except KeyError:
9598
continue
9699

97-
not_evaluate = ("password", "passwd")
98-
for option, value in config_options.items():
99-
if option not in config:
100-
try:
101-
if option in not_evaluate:
102-
config[option] = value[0]
103-
else:
104-
config[option] = eval(value[0]) # pylint: disable=eval-used
105-
except (NameError, SyntaxError):
106-
config[option] = value[0]
107-
100+
for option, values in config_options.items():
101+
value, _, section = values
102+
if (
103+
option not in config
104+
and option_parser.has_section(section)
105+
and option_parser.has_option(section, option)
106+
):
107+
if option in ("password", "passwd"): # keep the value as string
108+
config[option] = str(value)
109+
else:
110+
try:
111+
config[option] = ast.literal_eval(value)
112+
except (ValueError, TypeError, SyntaxError):
113+
config[option] = value
114+
if "socket" in config:
115+
config["unix_socket"] = config.pop("socket")
108116
return config
109117

110118

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
[correct_config]
2+
user=root
3+
password=mypass
4+
database=cpydata
5+
port=10000
6+
failover=[{"host": "127.0.1.2", "port": 3306, "user": "root", "priority": 80},{"host": "127.0.1.5", "port": 2450, "user": "root_123", "priority": 10},]
7+
8+
[correct_config_with_unix_socket]
9+
user=root
10+
password=mypass
11+
database=cpydata
12+
port=10000
13+
unix_socket=unix_socket_path_for_bug37145655
14+
15+
[correct_config_with_socket]
16+
user=root
17+
password=mypass
18+
database=cpydata
19+
port=10000
20+
socket=unix_socket_path_for_bug37145655
21+
22+
[incorrect_config]
23+
user=root
24+
password=mypass
25+
database=cpydata
26+
port=int(10000)
27+
allow_local_infile=__import__('os').system('whoami')

mysql-connector-python/tests/test_bugs.py

+50-2
Original file line numberDiff line numberDiff line change
@@ -3527,10 +3527,8 @@ def test_unsupported_arguments(self):
35273527
)
35283528
}
35293529
exp.update(config)
3530-
35313530
self.assertEqual(exp, new_config)
35323531

3533-
35343532
class BugOra21530100(tests.MySQLConnectorTests):
35353533
"""BUG#21530100: CONNECT FAILS WHEN USING MULTIPLE OPTION_GROUPS WITH
35363534
PYTHON 3.3
@@ -8911,3 +8909,53 @@ def test_incorrect_host_err_msg(self):
89118909
str(err),
89128910
"Error message cannot contain unstructured bind address",
89138911
)
8912+
8913+
class BugOra37145655(tests.MySQLConnectorTests):
8914+
"""BUG#37145655: MySQL Connector/Python Configuration Files RCE"""
8915+
8916+
def test_read_optionfiles(self):
8917+
option_file_dir = os.path.join("tests", "data", "option_files")
8918+
opt_file = os.path.join(option_file_dir, "bug_37145655.cnf")
8919+
exp = {
8920+
"user": "root",
8921+
"password": "mypass",
8922+
"database": "cpydata",
8923+
"port": 10000,
8924+
"failover": [
8925+
{
8926+
"host": "127.0.1.2",
8927+
"port": 3306,
8928+
"user": "root",
8929+
"priority": 80,
8930+
},
8931+
{
8932+
"host": "127.0.1.5",
8933+
"port": 2450,
8934+
"user": "root_123",
8935+
"priority": 10,
8936+
},
8937+
],
8938+
}
8939+
self.assertEqual(
8940+
exp, read_option_files(option_files=opt_file, option_groups=["correct_config"])
8941+
)
8942+
exp.pop("failover")
8943+
exp["unix_socket"] = "unix_socket_path_for_bug37145655"
8944+
self.assertEqual(
8945+
exp, read_option_files(option_files=opt_file, option_groups=["correct_config_with_unix_socket"])
8946+
)
8947+
self.assertEqual(
8948+
exp, read_option_files(option_files=opt_file, option_groups=["correct_config_with_socket"])
8949+
)
8950+
del exp["unix_socket"]
8951+
exp["port"] = "int(10000)"
8952+
exp["allow_local_infile"] = "__import__('os').system('whoami')"
8953+
self.assertEqual(
8954+
exp, read_option_files(option_files=opt_file, option_groups=["incorrect_config"])
8955+
)
8956+
with self.assertRaises((errors.InterfaceError, TypeError),) as _:
8957+
with mysql.connector.connect(
8958+
read_default_file=opt_file,
8959+
option_groups=["incorrect_config"],
8960+
) as _:
8961+
pass

0 commit comments

Comments
 (0)