Is it safe to use Python % string operator to compose a Psycopg SQL query as long as values are passed separately to cursor.execute()

129 Views Asked by At

There is plenty of material, most notably the "Passing parameters to SQL queries" guide on psycopg.org, covering the security problems with using Python's + or % string operators to merge values into a Psycopg2 SQL query.

What I've found unclear is whether or not it is safe to use % to insert the *field names* in to the query prior to running cursor.execute(query, values) with the values dictionary properly passed in as the second argument. My original need was to dynamically compose queries from variable length dictionaries of field/value pairs.

Following a different question/answer on Stack Overflow and other resources, I have my program working as expected with trusted input. This example code demonstrates what I'm doing, but I want to understand why the Python % operator is or is not safe in this context. Put another way, could a SQL injection attack be carried out by modifying only the two dictionaries (keys or values)?

from psycopg2 import sql

values = {
    "field1": "value1",
    "field2": "value2",
    "field3": "value3",
}

conditions = {
    "field4": "value4",
    "field5": "value5",
}

select_fields = [
    "field1",
    "field3",
]

# INSERT a record using fields/values from the values dictionary
insert_query = sql.SQL(
    r"INSERT INTO tablename (%s) VALUES (%s)"
    % (
        ",".join(values),
        ",".join(r"%%(%s)s" % key for key in values)
    )
)
# equivalent to:
insert_query = sql.SQL(
    r"INSERT INTO tablename (field1,field2,field3) VALUES (%(field1)s,%(field2)s,%(field3)s)"
)


# DELETE all records that match on ALL field/value pairs in the conditions dictionary
delete_query = sql.SQL(
    r"DELETE FROM tablename WHERE (%s) = (%s)"
    % (
        ",".join(conditions),
        ",".join(r"%%(%s)s" % key for key in conditions),
    )
)
# equivalent to:
delete_query = sql.SQL(
    r"DELETE FROM tablename WHERE (field4,field5) = (%(field4)s,%(field5)s)"
)


# SELECT all records that match on ALL field/value pairs in the conditions dictionary
select_query = sql.SQL(
    r"SELECT %s FROM tablename WHERE (%s) = (%s)"
    % (
        ",".join(select_fields),
        ",".join(conditions),
        ",".join(r"%%(%s)s" % key for key in conditions),
    )
)
# equivalent to:
select_query = sql.SQL(
    r"SELECT field1,field3 FROM tablename WHERE (field4,field5) = (%(field4)s,%(field5)s)")

# cursor.execute(a_query, values) # would come next, but that's beyond the scope of my question

I spun up a test database and tried injecting SQL into the keys and values of the two dictionaries. I had the query object looking like a ready-to-go injection attack, but Psycopg kept raising errors when I later passed that query and the values to cursor.execute()

If anyone has recommendations on how this could be better accomplished, I'd be very interested in hearing them as well.

0

There are 0 best solutions below