121

I know how to map a list to a string:

foostring = ",".join( map(str, list_of_ids) )

And I know that I can use the following to get that string into an IN clause:

cursor.execute("DELETE FROM foo.bar WHERE baz IN ('%s')" % (foostring))

What I need is to accomplish the same thing SAFELY (avoiding SQL injection) using MySQL database. In the above example because foostring is not passed as an argument to execute, it is vulnerable. I also have to quote and escape outside of the MySQL library.

(There is a related SO question, but the answers listed there either do not work for MySQL database or are vulnerable to SQL injection.)

informatik01
  • 16,038
  • 10
  • 74
  • 104
mluebke
  • 8,588
  • 7
  • 35
  • 31
  • You might be able to get some inspiration from a similar question that is done in php http://stackoverflow.com/questions/327274/mysql-prepared-statements-with-a-variable-size-variable-list/327384 – Zoredache Feb 26 '09 at 08:46
  • Possible duplicate of [python list in sql query as parameter](http://stackoverflow.com/questions/283645/python-list-in-sql-query-as-parameter) – Kamil Sindi Feb 21 '16 at 21:57
  • @mluebke Any idea about passing multiple lists in query? – Dipen Dedania Aug 26 '16 at 06:31

9 Answers9

222

Use the list_of_ids directly:

format_strings = ','.join(['%s'] * len(list_of_ids))
cursor.execute("DELETE FROM foo.bar WHERE baz IN (%s)" % format_strings,
                tuple(list_of_ids))

That way you avoid having to quote yourself, and avoid all kinds of sql injection.

Note that the data (list_of_ids) is going directly to mysql's driver, as a parameter (not in the query text) so there is no injection. You can leave any chars you want in the string, no need to remove or quote chars.

nosklo
  • 217,122
  • 57
  • 293
  • 297
  • 2
    @heikogerlach: I am not quoting the %s... The first line creates a string of "%s,%s,%s"... the same size of list_of_ids length. – nosklo Feb 26 '09 at 11:22
  • Will this work in sqlite also? Cause I just tried it and it seems to point out syntax errors. – Sohaib Sep 16 '14 at 04:27
  • 1
    @Sohaib in sqlite the replacement char is `?` not `%s` so it would work if you change the first line to `format_strings = ','.join('?' * len(list_of_ids))`. – nosklo Oct 02 '14 at 20:17
  • Depending on how often such a statement is used with different number of arguments I like to group the argument number and execute multiple to make sure the database only sees a limited number of variations (for the sql cache).this also helps against too many arguments. – eckes Nov 20 '17 at 08:56
  • Could you give an example syntax for the same solution in Python3? – Nikolay Shindarov Feb 11 '19 at 13:01
  • @nosklo How'd you do the same if you are using the query object which has parameters for e.g `fname_list = ['item1', 'item2'] query = ("select distinct cln from vcf_commits where branch like %s and repository like %s and filename in (%s) and author not like %s" % format_strings,) cursor = churn_db_connection.get_connection_cursor() cursor.execute(query, (branch, repository, tuple(fname_list), invalid_author,))` this throws an error – kdas Mar 12 '19 at 19:37
  • 3
    @kdas in your case you don't want the `% format_strings` part to change the other `%s` placeholders in your query, only the `IN (%s)` placeholder - The way to achieve this is to double all `%` chars except the one you want to replace: `query = ("select distinct cln from vcf_commits where branch like %%s and repository like %%s and filename in (%s) and author not like %%s" % format_strings,); cursor.execute(query, (branch, repository) + tuple(fname_list) + (invalid_author,))` – nosklo Mar 12 '19 at 19:43
  • 1
    Aah this is brilliant @nosklo. The comma (,) after format_strings was causing error but after removing that it works flawlessly. Genius. Applause. `query = ("select distinct cln from vcf_commits where branch like %%s and repository like %%s and filename in (%s) and author not like %%s" % format_strings);` – kdas Mar 14 '19 at 05:42
  • Modifying @nosklo answer to use f strings. `format_strings = ','.join(['%s'] * len(list_of_ids)) cursor.execute(f"DELETE FROM foo.bar WHERE baz IN ({format_strings})", tuple(list_of_ids))` – R.S.K Mar 29 '23 at 04:40
13

Though this question is quite old, thought it would be better to leave a response in case someone else was looking for what I wanted

Accepted answer gets messy when we have a lot of the params or if we want to use named parameters

After some trials

ids = [5, 3, ...]  # list of ids
cursor.execute('''
SELECT 
...
WHERE
  id IN %(ids)s
  AND created_at > %(start_dt)s
''', {
  'ids': tuple(ids), 'start_dt': '2019-10-31 00:00:00'
})

Tested with python2.7, pymysql==0.7.11

markk
  • 607
  • 1
  • 7
  • 11
  • 22
    This does not work with python 3 and mysql-connector-python 8.0.21. An error "Python tuple cannot be converted to MySQL type" is returned. – Rubms Jul 20 '20 at 06:40
2

This appears to still be a problem with Python3 in 2021, as pointed out in the comment by Rubms to the answer by markk.

Adding about 9 lines of code to the method "_process_params_dict" in "cursor.py" in the mysql connector package to handle tuples solved the problem for me:

def _process_params_dict(self, params):
    """Process query parameters given as dictionary"""
    try:
        to_mysql = self._connection.converter.to_mysql
        escape = self._connection.converter.escape
        quote = self._connection.converter.quote
        res = {}
        for key, value in list(params.items()):
            if type(value) is tuple: ### BEGIN MY ADDITIONS
                res[key.encode()] = b''
                for subvalue in value:
                    conv = subvalue
                    conv = to_mysql(conv)
                    conv = escape(conv)
                    conv = quote(conv)
                    res[key.encode()] = res[key.encode()] + b',' + conv if len(res[key.encode()]) else conv
            else: ### END MY ADDITIONS
                conv = value
                conv = to_mysql(conv)
                conv = escape(conv)
                conv = quote(conv)
                res[key.encode()] = conv
    except Exception as err:
        raise errors.ProgrammingError(
            "Failed processing pyformat-parameters; %s" % err)
    else:
        return res
Philip
  • 157
  • 2
  • 8
0

Maybe a little late to the question, but I stumbled upon a similar problem, but I wanted to use a dict of named parameters instead of a tuple (because if I want to modify the parameters to add or remove some, I don't want to re-construct the tuple, messing the order can be very easy and bug-inducing...).

My solution was to format the query string to explode the parameter into several parameters, and then construct the parameter dict with these new params:

from typing import Iterable

query = """
SELECT *
FROM table
WHERE id IN (%(test_param)s)
"""

parameters = {"test_param": [1, 2, 3])

new_params = {}

for k, v in parameters.items():
    if isinstance(v, Iterable):
        iterable_params = {f"{k}_{i}": value for i, value in enumerate(v)}
        iterable_params_formatted = [f"%({k}_{i})s" for i in range(0, len(v))]
        query = query.replace(f"%({k})s", ", ".join(iterable_params_formatted))
        new_params.update(iterable_params)
    else:
        new_params[k] = v

print(query)
print(new_params)

Result:

> SELECT *
FROM table
WHERE id IN (%(test_param_0)s, %(test_param_1)s, %(test_param_2)s)

> {'test_param_0': 1, 'test_param_1': 2, 'test_param_2': 3}

Could be done better, but I couldn't find a solution using a dict of named parameters instead of an ordered tuple.

jcf
  • 602
  • 1
  • 6
  • 26
-2

If you use Django 2.0 or 2.1 and Python 3.6, this is the right way:

from django.db import connection
RESULT_COLS = ['col1', 'col2', 'col3']
RESULT_COLS_STR = ', '.join(['a.'+'`'+i+'`' for i in RESULT_COLS])
QUERY_INDEX = RESULT_COLS[0]

TABLE_NAME = 'test'
search_value = ['ab', 'cd', 'ef']  # <-- a list
query = (
    f'SELECT DISTINCT {RESULT_COLS_STR} FROM {TABLE_NAME} a '
    f'WHERE a.`{RESULT_COLS[0]}` IN %s '
    f'ORDER BY a.`{RESULT_COLS[0]}`;'
)  # <- 'SELECT DISTINCT a.`col1`, a.`col2`, a.`col3` FROM test a WHERE a.`col1` IN %s ORDER BY a.`col1`;'
with connection.cursor() as cursor:
    cursor.execute(query, params=[search_value])  # params is a list with a list as its element

ref: https://stackoverflow.com/a/23891759/2803344 https://docs.djangoproject.com/en/2.1/topics/db/sql/#passing-parameters-into-raw

Belter
  • 3,573
  • 5
  • 42
  • 58
-3
list_of_ids = [ 1, 2, 3]
query = "select * from table where x in %s" % str(tuple(list_of_ids))
print query

This could work for some use-cases if you don't wish to be concerned with the method in which you have to pass arguments to complete the query string and would like to invoke just cursror.execute(query).

Another way could be:

"select * from table where x in (%s)" % ', '.join(str(id) for id in list_of_ids)
prajmus
  • 3,171
  • 3
  • 31
  • 41
-3

Another simple solution using list comprehension:

# creating a new list of strings and convert to tuple
sql_list = tuple([ key.encode("UTF-8") for key in list_of_ids ])

# replace "{}" with "('id1','id2',...'idlast')"
cursor.execute("DELETE FROM foo.bar WHERE baz IN {}".format(sql_list))
chenchuk
  • 5,324
  • 4
  • 34
  • 41
-3

Though this question is quite old. I am sharing my solution if it can help someone.

list_to_check = ['A', 'B'] cursor.execute("DELETE FROM foo.bar WHERE baz IN ({})".format(str(list_to_check)[1:-1])

Tested with Python=3.6

Aditya Sahu
  • 65
  • 1
  • 8
  • 3
    I am afraid this solution is vulnerable to SQL injection attacks, as the provided `list_to_check` is not being SQL-escaped. This is why passing the values as parameters to `execute` is more appropriate. Use this solution very carefully (that is, the input IDs are not received as parameters from the outside of your application), as someone could use this to attack your system and access your database. – Rubms Jul 20 '20 at 05:43
-9

Very simple: Just use the below formation

rules_id = ["9","10"]

sql1 = "SELECT * FROM attendance_rules_staff WHERE id in("+", ".join(map(str, rules_id))+")"

", ".join(map(str, rules_id))

  • Where does it do sql quoting and isn’t this using a literal instead of bind variables? – eckes Nov 20 '17 at 08:54
  • Do not need , it simply working fine. You can test Because tuple formation directly converted as string with first braces ("9", "10") . Which adjust sql formation. So you do not need other formation to make is sql adjastable – Mizanur Rahman Nov 20 '17 at 16:50
  • 2
    and if an `rules_id` contains `"); DROP TABLES Bobby --`? – eckes Nov 20 '17 at 18:55
  • Already told "imploding a list" not ") ... so before query you need to validate – Mizanur Rahman Nov 21 '17 at 13:53
  • or use: sql1 = "SELECT * FROM attendance_rules_staff WHERE id in("+", ".join(map(str, rules_id))+")" – Mizanur Rahman Nov 21 '17 at 14:34
  • I guess its fine if you stick to numbers, but still it produces a SQL with literals, poor SQL parser... – eckes Nov 21 '17 at 19:12