Skip to content

Commit 32c4520

Browse files
authored
Merge pull request #1044 from Nothing4You/local-infile
Fix arbitrary file access vulnerability when connecting to malicious servers
2 parents fc1203e + e2e895d commit 32c4520

File tree

4 files changed

+51
-4
lines changed

4 files changed

+51
-4
lines changed

CHANGES.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@ next (unreleased)
1919
* | Bump setuptools to >=80, setuptools-scm to >=7, <10.
2020
| setuptools-scm must be at least 9.2.0 for consistent hash lengths of non-release builds.
2121

22+
* | Properly check whether loading of local files is enabled #1044
23+
| Loading local data now requires using the `local_infile` parameter, passing just the client flag through `client_flag` is no longer supported.
24+
| Fixes `GHSA-r397-ff8c-wv2g <https://github.com/aio-libs/aiomysql/security/advisories/GHSA-r397-ff8c-wv2g>`_
25+
| Thanks to @KonstantAnxiety for reporting this.
26+
2227
0.2.0 (2023-06-11)
2328
^^^^^^^^^^^^^^^^^^
2429

aiomysql/connection.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -245,7 +245,8 @@ def __init__(self, host="localhost", user=None, password="",
245245

246246
self._encoding = charset_by_name(self._charset).encoding
247247

248-
if local_infile:
248+
self._local_infile = bool(local_infile)
249+
if self._local_infile:
249250
client_flag |= CLIENT.LOCAL_FILES
250251

251252
client_flag |= CLIENT.CAPABILITIES
@@ -1203,6 +1204,10 @@ def _read_ok_packet(self, first_packet):
12031204
self.has_next = ok_packet.has_next
12041205

12051206
async def _read_load_local_packet(self, first_packet):
1207+
if not self.connection._local_infile:
1208+
raise RuntimeError(
1209+
"**WARN**: Received LOAD_LOCAL packet but local_infile option is false."
1210+
)
12061211
load_packet = LoadLocalPacketWrapper(first_packet)
12071212
sender = LoadLocalFile(load_packet.filename, self.connection)
12081213
try:

docs/connection.rst

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,8 @@ Example::
4646
client_flag=0, cursorclass=Cursor, init_command=None,
4747
connect_timeout=None, read_default_group=None,
4848
autocommit=False, echo=False
49-
ssl=None, auth_plugin='', program_name='',
50-
server_public_key=None, loop=None)
49+
local_infile=False, loop=None, ssl=None, auth_plugin='',
50+
program_name='', server_public_key=None)
5151

5252
A :ref:`coroutine <coroutine>` that connects to MySQL.
5353

@@ -71,7 +71,8 @@ Example::
7171
See `pymysql.converters`.
7272
:param use_unicode: whether or not to default to unicode strings.
7373
:param client_flag: custom flags to send to MySQL. Find
74-
potential values in `pymysql.constants.CLIENT`.
74+
potential values in `pymysql.constants.CLIENT`. Refer to the
75+
`local_infile` parameter for enabling loading of local data.
7576
:param cursorclass: custom cursor class to use.
7677
:param str init_command: initial SQL statement to run when connection is
7778
established.
@@ -81,6 +82,10 @@ Example::
8182
file.
8283
:param autocommit: Autocommit mode. None means use server default.
8384
(default: ``False``)
85+
:param local_infile: Boolean to enable the use of `LOAD DATA LOCAL`
86+
command. This also enables the corresponding `client_flag`. aiomysql
87+
does not perform any validation of files requested by the server. Do
88+
not use this with untrusted servers. (default: ``False``)
8489
:param ssl: Optional SSL Context to force SSL
8590
:param auth_plugin: String to manually specify the authentication
8691
plugin to use, i.e you will want to use mysql_clear_password

tests/test_load_local.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22
import os
33
from unittest.mock import patch, MagicMock
44

5+
import aiomysql
56
import pytest
7+
from pymysql.constants import CLIENT
68
from pymysql.err import OperationalError
79

810

@@ -81,3 +83,33 @@ async def test_load_warnings(cursor, table_local_file):
8183
with warnings.catch_warnings(record=True) as w:
8284
await cursor.execute(sql)
8385
assert "Incorrect integer value" in str(w[-1].message)
86+
87+
88+
@pytest.mark.run_loop
89+
async def test_load_local_disabled(mysql_params, table_local_file):
90+
# By setting the client flag, the server will be informed that we support
91+
# loading local files. This validates that the client side check catches
92+
# the server attempting to read files from us without having this
93+
# explicitly enabled on the connection. The local_infile parameter sets
94+
# the client flag, but not the other way round.
95+
params = mysql_params.copy()
96+
params["local_infile"] = False
97+
if "client_flag" in params:
98+
params["client_flag"] |= CLIENT.LOCAL_FILES
99+
else:
100+
params["client_flag"] = CLIENT.LOCAL_FILES
101+
102+
async with aiomysql.connect(**params) as conn:
103+
async with conn.cursor() as cursor:
104+
# Test load local infile with a valid file
105+
filename = os.path.join(os.path.dirname(os.path.realpath(__file__)),
106+
'fixtures',
107+
'load_local_data.txt')
108+
with pytest.raises(
109+
RuntimeError,
110+
match="Received LOAD_LOCAL packet but local_infile option is false",
111+
):
112+
await cursor.execute(
113+
("LOAD DATA LOCAL INFILE '{0}' INTO TABLE " +
114+
"test_load_local FIELDS TERMINATED BY ','").format(filename)
115+
)

0 commit comments

Comments
 (0)