這次我們將介紹怎么防止Python注入SQL攻擊。有上一篇的鋪墊,我們廢話不多說,開搞。。。
制作安全查詢參數
在上一篇中,我們看到了入侵者如何利用系統并通過使用 字符串獲得管理權限。問題是,我們允許直接執行從客戶端傳遞的值到數據庫,卻不執行任何類型的檢查或驗證,所以SQL注入就是依賴于這種類型的漏洞。
在數據庫查詢中使用用戶輸入時,可能存在SQL注入漏洞。防止PythonSQL注入的關鍵是確保該值是不是我們的意愿使用。在前面的示例中,我們打算username用作字符串。實際上,它被用作原始SQL語句。
為了防止入侵者將原始SQL注入字符串參數的位置,可以轉義引號:
>>> # BAD EXAMPLE. DON'T DO THIS!>>> username = username.replace("'", "''")
這只是一個例子。在試圖阻止Python SQL注入時,需要考慮許多特殊的字符和情況。還好,數據庫適配器提供了內置的工具,可以通過使用查詢參數來防止Python SQL注入。它們代替普通的字符串插值來組成一個帶有參數的查詢。
注意:不同的適配器、數據庫和編程語言以不同的名稱引用查詢參數。常見的名稱包括綁定變量、替換變量和替換變量。
現在我們對這個漏洞有了更好的理解,我們可以用查詢參數代替字符串插值來重寫函數了:
def is_admin(username: str) -> bool: with connection.cursor() as cursor: cursor.execute(""" SELECT admin FROM users WHERE username = %(username)s """, { 'username': username }) result = cursor.fetchone() if result is None: # User does not exist return False admin, = result return admin
在第9行,我們使用了一個命名參數username來指示用戶名應該放在哪里。注意,參數username不再被單引號包圍。
在第11行,我們將username的值作為第二個參數傳遞給了sor.execute()。在數據庫中執行查詢時,連接將使用username的類型和值。
測試這個函數,嘗試一些有效和無效的值,包括危險的字符串:
>>> is_admin('haki')False>>> is_admin('ran')True>>> is_admin('foo')False>>> is_admin("'; select true; --")False
該函數返回所有值都是預期的結果。更重要的是,無效的用戶名已經不再起作用了。可以通過檢查execute()生成的查詢來看原因:
>>> with connection.cursor() as cursor:... cursor.execute("""... SELECT... admin... FROM... users... WHERE... username = %(username)s... """, {... 'username': "'; select true; --"... })... print(cursor.query.decode('utf-8'))SELECT adminFROM usersWHERE username = '''; select true; --'
該連接將username的值視為字符串,并終止Python SQL注入的字符可能轉義該字符串的可能。
傳遞安全的查詢參數
數據庫適配器通常提供幾種傳遞查詢參數的方法。命名占位符通常是可讀性最好的,但是一些實現可能從使用其他選項中獲得。
讓我們快速查看一下使用查詢參數的一些正確和錯誤的方法。下面的代碼塊顯示了希望避免的查詢類型:
# BAD EXAMPLES. DON'T DO THIS!cursor.execute("SELECT admin FROM users WHERE username = '" + username + '");cursor.execute("SELECT admin FROM users WHERE username = '%s' % username);cursor.execute("SELECT admin FROM users WHERE username = '{}'".format(username));cursor.execute(f"SELECT admin FROM users WHERE username = '{username}'");
這些每一條語句都將用戶名從客戶機直接傳遞到數據庫,而不執行任何檢查或驗證。這種代碼適合引入Python SQL注入。
相比之下,這些類型的查詢執行起來應該是安全的:
cursor.execute("SELECT admin FROM users WHERE username = %s'", (username, ));cursor.execute("SELECT admin FROM users WHERE username = %(username)s", {'username': username});
在這些語句中,用戶名作為命名參數傳遞。數據庫將在執行查詢時使用用戶名的指定類型和值,從而避免Python SQL注入。
使用SQL組成
到目前為止,我們已經將參數用于諸如數字、字符串和日期之類的值。但是,如果有一個需要組合不同查詢,比如表名或列名,該怎么辦呢?
受前一個示例的啟發,讓我們實現一個函數,該函數接受表的名稱并返回該表中的行數:
# BAD EXAMPLE. DON'T DO THIS!def count_rows(table_name: str) -> int: with connection.cursor() as cursor: cursor.execute(""" SELECT count(*) FROM %(table_name)s """, { 'table_name': table_name, }) result = cursor.fetchone() rowcount, = result return rowcount
執行用戶表:
Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 9, in count_rowspsycopg2.errors.SyntaxError: syntax error at or near "'users'"LINE 5: 'users'
該命令無法生成SQL。數據庫適配器將變量視為字符串或文字,但是表名不是普通的字符串。所以這就是SQL組合的用武之地。
現在已經知道使用字符串插值表達式來編寫SQL是不安全的。幸好,Psycopg提供了一個名為Psycopg的模塊。幫助我們安全地編寫sql查詢。讓我們使用psycopg.sql()重寫這個函數:
from psycopg2 import sql def count_rows(table_name: str) -> int: with connection.cursor() as cursor: stmt = sql.SQL(""" SELECT count(*) FROM {table_name} """).format( table_name = sql.Identifier(table_name), ) cursor.execute(stmt) result = cursor.fetchone() rowcount, = result return rowcount
現在有兩個不同之處。首先,使用sql()來組合查詢。然后,使用sql.Identifier()來注釋參數值table_name。(標識符是列或表名。)
現在,嘗試執行用戶表上的函數:
>>> count_rows('users')2
接下來,讓我們看看當表不存在時會發生什么:
>> count_rows('foo')Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 11, in count_rowspsycopg2.errors.UndefinedTable: relation "foo" does not existLINE 5: "foo"
該函數拋出UndefinedTable異常。在接下來的步驟中,我們將使用這個異常來表明函數不會受到Python SQL注入攻擊。
為了將它們放在一起,添加一個選項來將表中的行數計數到一定的限制, 這個特性對非常大的表很有用。要實現這一點,在查詢中添加一個LIMIT子句,以及LIMIT值的查詢參數:
from psycopg2 import sql def count_rows(table_name: str, limit: int) -> int: with connection.cursor() as cursor: stmt = sql.SQL(""" SELECT COUNT(*) FROM ( SELECT 1 FROM {table_name} LIMIT {limit} ) AS limit_query """).format( table_name = sql.Identifier(table_name), limit = sql.Literal(limit), ) cursor.execute(stmt) result = cursor.fetchone() rowcount, = result return rowcount
在這個代碼塊中,使用sql.Literal()注釋了limit。與前面的示例一樣,psycopg在使用時將所有查詢參數綁定為文字。但是,在使用sql()時,需要使用sql.Identifier()或sql.Literal()顯式地注釋每個參數。
執行該功能,以確保運行正常:
>> count_rows('users', 1)1>>> count_rows('users', 10)2
運行正常,確保也是安全的:
>> count_rows("(select 1) as foo; update users set admin = true where name = 'haki'; --", 1)Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 18, in count_rowspsycopg2.errors.UndefinedTable: relation "(select 1) as foo; update users set admin = true where name = '" does not existLINE 8: "(select 1) as foo; update users set adm...
此返回顯示psycopg轉義了該值,并且數據庫將其視為表名。由于不存在具有此名稱的表,因此引發了UndefinedTable異常,攻擊失敗了
私信小編01 領取完整項目代碼
結論
我們已經成功地實現了一個組成動態SQL的函數,系統面臨Python SQL注入的風險也沒有了!
我們在查詢中既使用了字面值,又使用了標識符,沒有影響安全性。