使用Python防止SQL注入攻擊的實現示例
文章背景
每隔幾年,開放式Web應用程序安全項目就會對最關鍵的Web應用程序安全風險進行排名。自第一次報告以來,注入風險高居其位!在所有注入類型中,SQL注入是最常見的攻擊手段之一,而且是最危險的。由于Python是世界上最流行的編程語言之一,因此了解如何防止Python SQL注入對于我們來說還是比較重要的
那么在寫這篇文章的時候我也是查詢了國內外很多資料,最后帶著問題去完善總結:
什么是Python SQL注入以及如何防止注入 如何使用文字和標識符作為參數組合查詢 如何安全地執行數據庫中的查詢文章演示的操作適用于所有數據庫,這里的示例使用的是PG,但是效果跟過程可以在其他數據庫(例如SQLite,MySQL,Oracle等等系統中)重現
1. 了解Python SQL注入
SQL注入攻擊是一種常見的安全漏洞。在我們日常工作中生成和執行SQL查詢也同樣是一項常見的任務。但是,有時候在編寫SQL語句時常常會犯下可怕錯誤
當我們使用Python將這些查詢直接執行到數據庫中時,很可能會損害到系統。所以如何成功實現組成動態SQL查詢的函數,而又不會使系統遭受Python SQL注入的威脅呢?
2. 設置數據庫
首先,建立一個新的PostgreSQL數據庫并用數據填充它。在文章中,將使用該數據庫直接見證Python SQL注入的工作方式及基本操作
2.1 創建數據庫
打開你的shell工具并創建一個用戶擁有的新PostgreSQL數據庫:
$ createdb -O postgres psycopgtest
在這里,使用了命令行選項-O將數據庫的所有者設置為用戶postgres。還指定了數據庫的名稱psycopgtest
postgres是一個特殊用戶,通常將保留該用戶用于管理任務,但是對于本文章而言,可以使用postgres。但是,在實際系統中,應該創建一個單獨的用戶作為數據庫的所有者
新數據庫已準備就緒!現在我們連接它:
$ psql -U postgres -d psycopgtestpsql (11.2, server 10.5)Type 'help' for help.
現在,可以看到以psycopgtest用戶身份連接到數據庫postgres。該用戶也是數據庫所有者,因此將具有數據庫中每個表的讀取權限
2.2 構造數據創建表
這里我們需要創建一個包含一些用戶信息的表,并向其中添加一些數據:
psycopgtest=# CREATE TABLE users ( username varchar(30), admin boolean);CREATE TABLEpsycopgtest=# INSERT INTO users (username, admin)VALUES (’zhangsan’, true), (’lisi’, false);INSERT 0 2psycopgtest=# SELECT * FROM users; username | admin----------+------- zhangsan | t lisi | f(2 rows)
我們添加了username和admin兩個列。該admin列指示用戶是否具有管理特權。我們的目標是瞄準該admin領域并嘗試濫用它
2.3 設置Python虛擬環境
現在我們已經有了一個數據庫,是時候設置Python環境。在新目錄中創建虛擬環境:
(~/src) $ mkdir psycopgtest(~/src) $ cd psycopgtest(~/src/psycopgtest) $ python3 -m venv venv
運行此命令后,venv將創建一個名為的新目錄。該目錄將存儲在虛擬環境中安裝的所有軟件包
2.4 使用Python連接數據庫
再使用Python連接PostgreSQL數據庫時需要確保我們的環境是否安裝了psycopg2,如果沒有使用pip安裝psycopg2:
pip install psycopg2
安裝完之后,我們編寫創建與數據庫連接的代碼:
import psycopg2connection = psycopg2.connect( host='127.0.0.1', database='psycopgtest', user='postgres', password='',)connection.set_session(autocommit=True)
psycopg2.connect()函數用來創建與數據庫的連接且接受以下參數:
host是數據庫所在服務器的IP地址 database是要連接的數據庫的名稱 user是具有數據庫權限的用戶 password連接數據庫的密碼我們設置完連接后,使用配置了會話autocommit=True。激活autocommit意味著不必通過發出commit或來手動管理rollback。這是 大多數ORM中的默認 行為。也可以在這里使用此行為,以便可以專注于編寫SQL查詢而不是管理事務
2.5 執行查詢
現在我們已經連接到了數據庫,開始執行我們的查詢:
>>> with connection.cursor() as cursor:... cursor.execute(’SELECT COUNT(*) FROM users’)... result = cursor.fetchone()... print(result)(2,)
使用該connection對象創建了一個cursor。就像Python中的文件操作一樣,cursor是作為上下文管理器實現的。創建上下文時,將cursor打開一個供使用以將命令發送到數據庫。當上下文退出時,將cursor關閉,將無法再使用它
Python with語句的實現感興趣的朋友可以自己查詢一下
在上下文中時,曾經cursor執行查詢并獲取結果。在這種情況下,發出查詢以對users表中的行進行計數。要從查詢中獲取結果,執行cursor.fetchone()并接收了一個元組。由于查詢只能返回一個結果,因此使用fetchone()。如果查詢返回的結果不止一個,那么我們就需要迭代cursor
3. 在SQL中使用查詢參數
現在我們創建了數據庫并且建立了與數據庫的連接,并執行了查詢。但是我們使用的查詢是靜態的。換句話說,它沒有參數。現在,將開始在查詢中使用參數
首先,將實現一個檢查用戶是否為管理員的功能。is_admin()接受用戶名并返回該用戶的管理員狀態:
def is_admin(username: str) -> bool: with connection.cursor() as cursor: cursor.execute(''' SELECTadmin FROMusers WHEREusername = ’%s’ ''' % username) result = cursor.fetchone() admin, = result return admin
此函數執行查詢以獲取admin給定用戶名的列的值。曾經fetchone()返回一個具有單個結果的元組。然后,將此元組解壓縮到變量中admin。要測試的功能,請檢查用戶名:
>>> is_admin(’lisi’)False>>> is_admin(’zhangsan’)True
到目前為止,一切都是正常的。該函數返回了兩個用戶的預期結果。但是我們如果查看不存在的用戶呢?看下會怎樣:
>>> is_admin(’wangwu’)Traceback (most recent call last): File '<stdin>', line 1, in <module> File '<stdin>', line 12, in is_adminTypeError: cannot unpack non-iterable NoneType object
當用戶不存在時可以看到出現了異常,這是因為如果找不到結果,則.fetchone()返回None,導致引發TypeError
要處理不存在的用戶,我們可以創建一個特例None:
def is_admin(username: str) -> bool: with connection.cursor() as cursor: cursor.execute(''' SELECTadmin FROMusers WHEREusername = ’%s’ ''' % username) result = cursor.fetchone() if result is None: return False admin, = result return admin
在這里,添加了處理的特殊情況None。如果username不存在,則該函數應返回False。再次在某些用戶上測試該功能:
>>> is_admin(’lisi’)False>>> is_admin(’zhangsan’)True>>> is_admin(’wangwu’)False
可以發現這個函數現在已經可以處理不存在的用戶名
4. 使用Python SQL注入利用查詢參數
在上一個示例中,使用了字符串插值來生成查詢。然后,執行查詢并將結果字符串直接發送到數據庫。但是,在此過程中可能會忽略一些事情
回想一下username傳遞給is_admin()。這個變量究竟代表什么?我們可能會認為這username只是代表實際用戶名的字符串。但是,正如我們將要看到的,入侵者可以通過執行Python SQL注入輕松利用這種監督并造成破壞
嘗試檢查以下用戶是否是管理員:
>>> is_admin('’; select true; --')True
等等…發生了什么事?
讓我們再看一下實現。打印出數據庫中正在執行的實際查詢:
>>> print('select admin from users where username = ’%s’' % '’; select true; --')select admin from users where username = ’’; select true; --’
結果文本包含三個語句。為了確切地了解Python SQL注入的工作原理,需要單獨檢查每個部分。第一條語句如下:
select admin from users where username = ’’;
這是我們想要的查詢。分號(;)終止查詢,因此該查詢的結果無關緊要。接下來是第二個語句:
select true;
這是入侵者構造的。它旨在始終返回True。
最后,我們會看到這段簡短的代碼:
--’
該代碼片段可消除其后的所有內容。入侵者添加了注釋符號(?),以將我們可能在最后一個占位符之后輸入的所有內容轉換為注釋
使用此參數執行函數時,它將始終返回True。例如,如果我們在登錄頁面中使用此功能,則入侵者可以使用用戶名登錄’; select true; --,并將被授予訪問權限。
如果我們認為這很難受,則可能會變得更難受!了解表結構的入侵者可以使用Python SQL注入造成永久性破壞。例如,入侵者可以注入一條更新語句來更改數據庫中的信息:
>>> is_admin(’lisi’)False>>> is_admin('’; update users set admin = ’true’ where username = ’lisi’; select true; --')True>>> is_admin(’lisi’)True
讓我們再次分解:
’;
就像之前的注入一樣,此代碼段終止了查詢。下一條語句如下:
update users set admin = ’true’ where username = ’lisi’;
更新admin到true用戶lisi
最后,有以下代碼片段:
select true; --
與前面的示例一樣,該片段返回true并注釋掉其后的所有內容。
如果入侵者設法使用此輸入執行功能,則用戶lisi將成為管理員:
psycopgtest=# select * from users; username | admin----------+------- zhangsan | t lisi | t(2 rows)
入侵者可以使用用戶名登錄lisi。(如果入侵者確實想破壞,那么可以使用DROP DATABASE命令)
現在我們恢復lisi的原始狀態:
psycopgtest=# update users set admin = false where username = ’lisi’;UPDATE 1
4.1 制作安全查詢參數
了解了入侵者如何通過使用精心設計的字符串來利用系統并獲得管理員權限。問題是我們允許從客戶端傳遞的值直接執行到數據庫,而無需執行任何類型的檢查或驗證。SQL注入依賴于這種類型的漏洞
每當在數據庫查詢中使用用戶輸入時,SQL注入就可能存在漏洞。防止Python SQL注入的關鍵是確保該值已按我們開發的預期使用。在上一個示例中,username用作了字符串。實際上,它被用作原始SQL語句
為了確保我們按預期使用值,需要對值進行轉義。例如,為防止入侵者將原始SQL替換為字符串參數,可以對引號進行轉義:
>>> username = username.replace('’', '’’')
這只是一個例子。嘗試防止Python SQL注入時,有很多特殊字符和場景需要考慮。現代的數據庫適配器隨附了一些內置工具,這些工具可通過使用查詢參數來防止Python SQL注入。使用這些參數代替普通字符串插值可組成帶有參數的查詢
現在,我們已經對該漏洞有了一個明確的知曉,可以使用查詢參數而不是字符串插值來重寫該函數:
def is_admin(username: str) -> bool: with connection.cursor() as cursor: cursor.execute(''' SELECTadmin FROMusers WHEREusername = %(username)s ''', { ’username’: username }) result = cursor.fetchone() if result is None: return False admin, = result return admin
我們使用了一個命名參數username來指示用戶名應該去哪里
將值username作為第二個參數傳遞給cursor.execute()。username在數據庫中執行查詢時,連接將使用的類型和值要測試此功能,我們先嘗試一些有效以及無效的值跟一些有隱患的字符串:
>>> is_admin(’lisi’)False>>> is_admin(’zhangsan’)True>>> is_admin(’wangwu’)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注入
4.2 傳遞安全查詢參數
數據庫適配器通常提供幾種傳遞查詢參數的方法。命名占位符通常是可讀性最好的,但是某些實現可能會受益于使用其他選項
讓我們快速看一下使用查詢參數的一些對與錯方法。以下代碼塊顯示了我們需要避免的查詢類型:
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}’');
這些語句中的每條語句都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});
在這些語句中,username作為命名參數傳遞。現在,數據庫將username在執行查詢時使用指定的類型和值,從而提供針對Python SQL注入的保護
5. 使用SQL組合
但是,如果我們有一個用例需要編寫一個不同的查詢(該參數是其他參數,例如表或列名),該怎么辦?
繼上一個列子,我們實現一個函數,該函數接受表的名稱并返回該表中的行數:
def count_rows(table_name: str) -> int: with connection.cursor() as cursor: cursor.execute(''' SELECTcount(*) 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,可以幫助我們安全地編寫SQL查詢。讓我們使用psycopg.sql.SQL()以下代碼重寫該函數:
from psycopg2 import sqldef count_rows(table_name: str) -> int: with connection.cursor() as cursor: stmt = sql.SQL(''' SELECTcount(*) FROM{table_name} ''').format( table_name = sql.Identifier(table_name), ) cursor.execute(stmt) result = cursor.fetchone() rowcount, = result return rowcount
此實現有兩個區別。sql.SQL()組成查詢。sql.Identifier()對參數值進行注釋table_name(標識符是列或表的名稱)
現在,我們嘗試在users表上執行該函數:
>>> count_rows(’users’)2
接下來,讓我們看看表不存在時會發生什么:
>>> count_rows(’wangwu’)Traceback (most recent call last): File '<stdin>', line 1, in <module> File '<stdin>', line 11, in count_rowspsycopg2.errors.UndefinedTable: relation 'wangwu' does not existLINE 5: 'wangwu'^
該函數引發UndefinedTable異常。將使用此異常來表明我們的函數可以安全地免受Python SQL注入攻擊
要將所有內容放在一起,添加一個選項以對表中的行進行計數,直到達到特定限制。對于非常大的表,這個功能很有用。要實現這個操作,LIMIT在查詢中添加一個子句,以及該限制值的查詢參數:
from psycopg2 import sqldef count_rows(table_name: str, limit: int) -> int: with connection.cursor() as cursor: stmt = sql.SQL(''' SELECTCOUNT(*) FROM (SELECT 1FROM {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
在上面的代碼中,limit使用注釋了sql.Literal()。與前面的列子一樣,psycopg使用簡單方法時,會將所有查詢參數綁定為文字。但是,使用時sql.SQL(),需要使用sql.Identifier()或顯式注釋每個參數sql.Literal()
不幸的是,Python API規范不解決標識符的綁定,僅處理文字。Psycopg是唯一流行的適配器,它添加了使用文字和標識符安全地組合SQL的功能。這個事實使得在綁定標識符時要特別注意
執行該函數以確保其起作用:
>>> count_rows(’users’, 1)1>>> count_rows(’users’, 10)2
現在我們已經看到該函數正在運行,檢查它是否安全:
>>> count_rows('(select 1) as wangwu; update users set admin = true where name = ’lisi’; --', 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 wangwu; update users set admin = true where name = ’' does not existLINE 8: '(select 1) as wangwu; update users set adm... ^
異常顯示psycopg轉義了該值,并且數據庫將其視為表名。由于不存在具有該名稱的表,因此UndefinedTable引發了異常所以是安全的!
6. 結論
通過實現組成動態SQL,可與你使我們有效的規避系統遭受Python SQL注入的威脅!在查詢過程中同時使用文字和標識符,并不會影響安全性
7. 致謝
到此這篇關于使用Python防止SQL注入攻擊的實現示例的文章就介紹到這了,更多相關Python防止SQL注入攻擊內容請搜索好吧啦網以前的文章或繼續瀏覽下面的相關文章希望大家以后多多支持好吧啦網!
相關文章: