Skip to content

Commit a849fb1

Browse files
Copilotmethane
andauthored
Quote procname and user variables with backticks in callproc (#789)
`callproc` passed the procedure name and user variable names unquoted into SQL, making it unsafe for names containing reserved words or special characters. Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Inada Naoki <songofacandy@gmail.com>
1 parent 88a834f commit a849fb1

2 files changed

Lines changed: 33 additions & 6 deletions

File tree

src/MySQLdb/cursors.py

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@
2323
)
2424

2525

26+
def _backquote_escape(s):
27+
return s.replace(b"`", b"``")
28+
29+
2630
class BaseCursor:
2731
"""A base for Cursor classes. Useful attributes:
2832
@@ -279,10 +283,10 @@ def callproc(self, procname, args=()):
279283
variable and then retrieved by a query. Since stored
280284
procedures return zero or more result sets, there is no
281285
reliable way to get at OUT or INOUT parameters via callproc.
282-
The server variables are named @_procname_n, where procname
286+
The server variables are named @`_procname_n`, where procname
283287
is the parameter above and n is the position of the parameter
284288
(from zero). Once all result sets generated by the procedure
285-
have been fetched, you can issue a SELECT @_procname_0, ...
289+
have been fetched, you can issue a SELECT @`_procname_0`, ...
286290
query using .execute() to get any OUT or INOUT values.
287291
288292
Compatibility warning: The act of calling a stored procedure
@@ -295,17 +299,18 @@ def callproc(self, procname, args=()):
295299
db = self._get_db()
296300
if isinstance(procname, str):
297301
procname = procname.encode(db.encoding)
302+
procname_escaped = _backquote_escape(procname)
298303
if args:
299-
fmt = b"@_" + procname + b"_%d=%s"
304+
fmt = b"@`_" + procname_escaped + b"_%d`=%s"
300305
q = b"SET %s" % b",".join(
301306
fmt % (index, db.literal(arg)) for index, arg in enumerate(args)
302307
)
303308
self._query(q)
304309
self.nextset()
305310

306-
q = b"CALL %s(%s)" % (
307-
procname,
308-
b",".join([b"@_%s_%d" % (procname, i) for i in range(len(args))]),
311+
q = b"CALL `%s`(%s)" % (
312+
procname_escaped,
313+
b",".join([b"@`_%s_%d`" % (procname_escaped, i) for i in range(len(args))]),
309314
)
310315
self._query(q)
311316
return args

tests/test_cursor.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import MySQLdb.cursors
33
from MySQLdb.constants import ER
44
from configdb import connection_factory
5+
from textwrap import dedent
56

67

78
_conns = []
@@ -308,3 +309,24 @@ def test_cursor_is_iterator(Cursor):
308309
assert next(cursor) == ("c",)
309310
with pytest.raises(StopIteration):
310311
next(cursor)
312+
313+
314+
def test_callproc_escaping():
315+
conn = connect()
316+
cur = conn.cursor()
317+
318+
cur.execute("DROP PROCEDURE IF EXISTS `foo.bar`")
319+
try:
320+
cur.execute(
321+
dedent("""\
322+
create procedure `foo.bar` (arg1 int)
323+
begin
324+
select arg1*2;
325+
end
326+
""")
327+
)
328+
329+
cur.callproc("foo.bar", args=(123,))
330+
assert cur.fetchone()[0] == 246
331+
finally:
332+
cur.execute("DROP PROCEDURE IF EXISTS `foo.bar`")

0 commit comments

Comments
 (0)