溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

如何使用Python防止SQL注入攻擊

發(fā)布時間:2020-07-23 09:45:15 來源:億速云 閱讀:315 作者:小豬 欄目:開發(fā)技術(shù)

這篇文章主要為大家展示了如何使用Python防止SQL注入攻擊,內(nèi)容簡而易懂,希望大家可以學習一下,學習完之后肯定會有收獲的,下面讓小編帶大家一起來看看吧。

文章背景

每隔幾年,開放式Web應用程序安全項目就會對最關(guān)鍵的Web應用程序安全風險進行排名。自第一次報告以來,注入風險高居其位!在所有注入類型中,SQL注入是最常見的攻擊手段之一,而且是最危險的。由于Python是世界上最流行的編程語言之一,因此了解如何防止Python SQL注入對于我們來說還是比較重要的

那么在寫這篇文章的時候我也是查詢了國內(nèi)外很多資料,最后帶著問題去完善總結(jié):

  • 什么是Python SQL注入以及如何防止注入
  • 如何使用文字和標識符作為參數(shù)組合查詢
  • 如何安全地執(zhí)行數(shù)據(jù)庫中的查詢

文章演示的操作適用于所有數(shù)據(jù)庫,這里的示例使用的是PG,但是效果跟過程可以在其他數(shù)據(jù)庫(例如SQLite,MySQL,Oracle等等系統(tǒng)中)重現(xiàn)

1. 了解Python SQL注入

  SQL注入攻擊是一種常見的安全漏洞。在我們?nèi)粘9ぷ髦猩珊蛨?zhí)行SQL查詢也同樣是一項常見的任務。但是,有時候在編寫SQL語句時常常會犯下可怕錯誤

當我們使用Python將這些查詢直接執(zhí)行到數(shù)據(jù)庫中時,很可能會損害到系統(tǒng)。所以如何成功實現(xiàn)組成動態(tài)SQL查詢的函數(shù),而又不會使系統(tǒng)遭受Python SQL注入的威脅呢?

如何使用Python防止SQL注入攻擊

2. 設(shè)置數(shù)據(jù)庫

首先,建立一個新的PostgreSQL數(shù)據(jù)庫并用數(shù)據(jù)填充它。在文章中,將使用該數(shù)據(jù)庫直接見證Python SQL注入的工作方式及基本操作

2.1 創(chuàng)建數(shù)據(jù)庫

打開你的shell工具并創(chuàng)建一個用戶擁有的新PostgreSQL數(shù)據(jù)庫:

$ createdb -O postgres psycopgtest

在這里,使用了命令行選項-O將數(shù)據(jù)庫的所有者設(shè)置為用戶postgres。還指定了數(shù)據(jù)庫的名稱psycopgtest

postgres是一個特殊用戶,通常將保留該用戶用于管理任務,但是對于本文章而言,可以使用postgres。但是,在實際系統(tǒng)中,應該創(chuàng)建一個單獨的用戶作為數(shù)據(jù)庫的所有者

新數(shù)據(jù)庫已準備就緒!現(xiàn)在我們連接它:

$ psql -U postgres -d psycopgtest
psql (11.2, server 10.5)
Type "help" for help.

現(xiàn)在,可以看到以psycopgtest用戶身份連接到數(shù)據(jù)庫postgres。該用戶也是數(shù)據(jù)庫所有者,因此將具有數(shù)據(jù)庫中每個表的讀取權(quán)限

2.2 構(gòu)造數(shù)據(jù)創(chuàng)建表

這里我們需要創(chuàng)建一個包含一些用戶信息的表,并向其中添加一些數(shù)據(jù):

psycopgtest=# CREATE TABLE users (
  username varchar(30),
  admin boolean
);
CREATE TABLE

psycopgtest=# INSERT INTO users
  (username, admin)
VALUES
  ('zhangsan', true),
  ('lisi', false);
INSERT 0 2

psycopgtest=# SELECT * FROM users;
 username | admin
----------+-------
 zhangsan   | t
 lisi   | f
(2 rows)

我們添加了username和admin兩個列。該admin列指示用戶是否具有管理特權(quán)。我們的目標是瞄準該admin領(lǐng)域并嘗試濫用它

2.3 設(shè)置Python虛擬環(huán)境

現(xiàn)在我們已經(jīng)有了一個數(shù)據(jù)庫,是時候設(shè)置Python環(huán)境。在新目錄中創(chuàng)建虛擬環(huán)境:

(~/src) $ mkdir psycopgtest
(~/src) $ cd psycopgtest
(~/src/psycopgtest) $ python3 -m venv venv

運行此命令后,venv將創(chuàng)建一個名為的新目錄。該目錄將存儲在虛擬環(huán)境中安裝的所有軟件包

2.4 使用Python連接數(shù)據(jù)庫

再使用Python連接PostgreSQL數(shù)據(jù)庫時需要確保我們的環(huán)境是否安裝了psycopg2,如果沒有使用pip安裝psycopg2:

pip install psycopg2

安裝完之后,我們編寫創(chuàng)建與數(shù)據(jù)庫連接的代碼:

import psycopg2

connection = psycopg2.connect(
  host="127.0.0.1",
  database="psycopgtest",
  user="postgres",
  password="",
)
connection.set_session(autocommit=True)

psycopg2.connect()函數(shù)用來創(chuàng)建與數(shù)據(jù)庫的連接且接受以下參數(shù):

  • host是數(shù)據(jù)庫所在服務器的IP地址
  • database是要連接的數(shù)據(jù)庫的名稱
  • user是具有數(shù)據(jù)庫權(quán)限的用戶
  • password連接數(shù)據(jù)庫的密碼

我們設(shè)置完連接后,使用配置了會話autocommit=True。激活autocommit意味著不必通過發(fā)出commit或來手動管理rollback。這是 大多數(shù)ORM中的默認 行為。也可以在這里使用此行為,以便可以專注于編寫SQL查詢而不是管理事務

2.5 執(zhí)行查詢

現(xiàn)在我們已經(jīng)連接到了數(shù)據(jù)庫,開始執(zhí)行我們的查詢:

>>> with connection.cursor() as cursor:
...   cursor.execute('SELECT COUNT(*) FROM users')
...   result = cursor.fetchone()
... print(result)
(2,)

使用該connection對象創(chuàng)建了一個cursor。就像Python中的文件操作一樣,cursor是作為上下文管理器實現(xiàn)的。創(chuàng)建上下文時,將cursor打開一個供使用以將命令發(fā)送到數(shù)據(jù)庫。當上下文退出時,將cursor關(guān)閉,將無法再使用它

Python with語句的實現(xiàn)感興趣的朋友可以自己查詢一下

在上下文中時,曾經(jīng)cursor執(zhí)行查詢并獲取結(jié)果。在這種情況下,發(fā)出查詢以對users表中的行進行計數(shù)。要從查詢中獲取結(jié)果,執(zhí)行cursor.fetchone()并接收了一個元組。由于查詢只能返回一個結(jié)果,因此使用fetchone()。如果查詢返回的結(jié)果不止一個,那么我們就需要迭代cursor

3. 在SQL中使用查詢參數(shù)

現(xiàn)在我們創(chuàng)建了數(shù)據(jù)庫并且建立了與數(shù)據(jù)庫的連接,并執(zhí)行了查詢。但是我們使用的查詢是靜態(tài)的。換句話說,它沒有參數(shù)?,F(xiàn)在,將開始在查詢中使用參數(shù)

首先,將實現(xiàn)一個檢查用戶是否為管理員的功能。is_admin()接受用戶名并返回該用戶的管理員狀態(tài):

def is_admin(username: str) -> bool:
  with connection.cursor() as cursor:
    cursor.execute("""
      SELECT
        admin
      FROM
        users
      WHERE
        username = '%s'
    """ % username)
    result = cursor.fetchone()
  admin, = result
  return admin

此函數(shù)執(zhí)行查詢以獲取admin給定用戶名的列的值。曾經(jīng)fetchone()返回一個具有單個結(jié)果的元組。然后,將此元組解壓縮到變量中admin。要測試的功能,請檢查用戶名:

>>> is_admin('lisi')
False
>>> is_admin('zhangsan')
True

到目前為止,一切都是正常的。該函數(shù)返回了兩個用戶的預期結(jié)果。但是我們?nèi)绻榭床淮嬖诘挠脩裟??看下會怎樣?/p>

>>> is_admin('wangwu')
Traceback (most recent call last):
 File "<stdin>", line 1, in <module>
 File "<stdin>", line 12, in is_admin
TypeError: cannot unpack non-iterable NoneType object

當用戶不存在時可以看到出現(xiàn)了異常,這是因為如果找不到結(jié)果,則.fetchone()返回None,導致引發(fā)TypeError

要處理不存在的用戶,我們可以創(chuàng)建一個特例None:

def is_admin(username: str) -> bool:
  with connection.cursor() as cursor:
    cursor.execute("""
      SELECT
        admin
      FROM
        users
      WHERE
        username = '%s'
    """ % username)
    result = cursor.fetchone()

  if result is None:
    return False

  admin, = result
  return admin

在這里,添加了處理的特殊情況None。如果username不存在,則該函數(shù)應返回False。再次在某些用戶上測試該功能:

>>> is_admin('lisi')
False
>>> is_admin('zhangsan')
True
>>> is_admin('wangwu')
False

可以發(fā)現(xiàn)這個函數(shù)現(xiàn)在已經(jīng)可以處理不存在的用戶名

4. 使用Python SQL注入利用查詢參數(shù)

在上一個示例中,使用了字符串插值來生成查詢。然后,執(zhí)行查詢并將結(jié)果字符串直接發(fā)送到數(shù)據(jù)庫。但是,在此過程中可能會忽略一些事情

回想一下username傳遞給is_admin()。這個變量究竟代表什么?我們可能會認為這username只是代表實際用戶名的字符串。但是,正如我們將要看到的,入侵者可以通過執(zhí)行Python SQL注入輕松利用這種監(jiān)督并造成破壞

嘗試檢查以下用戶是否是管理員:

>>> is_admin("'; select true; --")
True

等等…發(fā)生了什么事?

讓我們再看一下實現(xiàn)。打印出數(shù)據(jù)庫中正在執(zhí)行的實際查詢:

>>> print("select admin from users where username = '%s'" % "'; select true; --")
select admin from users where username = ''; select true; --'

結(jié)果文本包含三個語句。為了確切地了解Python SQL注入的工作原理,需要單獨檢查每個部分。第一條語句如下:

select admin from users where username = '';

這是我們想要的查詢。分號(;)終止查詢,因此該查詢的結(jié)果無關(guān)緊要。接下來是第二個語句:

select true;

這是入侵者構(gòu)造的。它旨在始終返回True。

最后,我們會看到這段簡短的代碼:

--'

該代碼片段可消除其后的所有內(nèi)容。入侵者添加了注釋符號(–),以將我們可能在最后一個占位符之后輸入的所有內(nèi)容轉(zhuǎn)換為注釋

使用此參數(shù)執(zhí)行函數(shù)時,它將始終返回True。例如,如果我們在登錄頁面中使用此功能,則入侵者可以使用用戶名登錄'; select true; --,并將被授予訪問權(quán)限。

如果我們認為這很難受,則可能會變得更難受!了解表結(jié)構(gòu)的入侵者可以使用Python SQL注入造成永久性破壞。例如,入侵者可以注入一條更新語句來更改數(shù)據(jù)庫中的信息:

>>> 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并注釋掉其后的所有內(nèi)容。

如果入侵者設(shè)法使用此輸入執(zhí)行功能,則用戶lisi將成為管理員:

psycopgtest=# select * from users;
 username | admin
----------+-------
 zhangsan   | t
 lisi   | t
(2 rows)

入侵者可以使用用戶名登錄lisi。(如果入侵者確實想破壞,那么可以使用DROP DATABASE命令)

現(xiàn)在我們恢復lisi的原始狀態(tài):

psycopgtest=# update users set admin = false where username = 'lisi';
UPDATE 1

4.1 制作安全查詢參數(shù)

了解了入侵者如何通過使用精心設(shè)計的字符串來利用系統(tǒng)并獲得管理員權(quán)限。問題是我們允許從客戶端傳遞的值直接執(zhí)行到數(shù)據(jù)庫,而無需執(zhí)行任何類型的檢查或驗證。SQL注入依賴于這種類型的漏洞

每當在數(shù)據(jù)庫查詢中使用用戶輸入時,SQL注入就可能存在漏洞。防止Python SQL注入的關(guān)鍵是確保該值已按我們開發(fā)的預期使用。在上一個示例中,username用作了字符串。實際上,它被用作原始SQL語句

為了確保我們按預期使用值,需要對值進行轉(zhuǎn)義。例如,為防止入侵者將原始SQL替換為字符串參數(shù),可以對引號進行轉(zhuǎn)義:

>>> username = username.replace("'", "''")

這只是一個例子。嘗試防止Python SQL注入時,有很多特殊字符和場景需要考慮。現(xiàn)代的數(shù)據(jù)庫適配器隨附了一些內(nèi)置工具,這些工具可通過使用查詢參數(shù)來防止Python SQL注入。使用這些參數(shù)代替普通字符串插值可組成帶有參數(shù)的查詢

現(xiàn)在,我們已經(jīng)對該漏洞有了一個明確的知曉,可以使用查詢參數(shù)而不是字符串插值來重寫該函數(shù):

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:
    return False

  admin, = result
  return admin

我們使用了一個命名參數(shù)username來指示用戶名應該去哪里

將值username作為第二個參數(shù)傳遞給cursor.execute()。username在數(shù)據(jù)庫中執(zhí)行查詢時,連接將使用的類型和值
要測試此功能,我們先嘗試一些有效以及無效的值跟一些有隱患的字符串:

>>> is_admin('lisi')
False
>>> is_admin('zhangsan')
True
>>> is_admin('wangwu')
False
>>> is_admin("'; select true; --")
False

跟我們想象的一毛一樣!該函數(shù)返回所有值的預期結(jié)果。并且,隱患的字符串不再起作用。要了解原因,可以檢查由生成的查詢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
  admin
FROM
  users
WHERE
  username = '''; select true; --'

該連接將值username視為字符串,并轉(zhuǎn)義了可能終止該字符串的所有字符并引入了Python SQL注入

4.2 傳遞安全查詢參數(shù)

數(shù)據(jù)庫適配器通常提供幾種傳遞查詢參數(shù)的方法。命名占位符通常是可讀性最好的,但是某些實現(xiàn)可能會受益于使用其他選項

讓我們快速看一下使用查詢參數(shù)的一些對與錯方法。以下代碼塊顯示了我們需要避免的查詢類型:

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直接從客戶端傳遞到數(shù)據(jù)庫,而無需執(zhí)行任何類型的檢查或驗證。這類代碼已經(jīng)可以達到Python SQL注入

相比上面,以下類型的查詢可以安全地執(zhí)行:

cursor.execute("SELECT admin FROM users WHERE username = %s'", (username, ));
cursor.execute("SELECT admin FROM users WHERE username = %(username)s", {'username': username});

在這些語句中,username作為命名參數(shù)傳遞?,F(xiàn)在,數(shù)據(jù)庫將username在執(zhí)行查詢時使用指定的類型和值,從而提供針對Python SQL注入的保護

5. 使用SQL組合

但是,如果我們有一個用例需要編寫一個不同的查詢(該參數(shù)是其他參數(shù),例如表或列名),該怎么辦?

繼上一個列子,我們實現(xiàn)一個函數(shù),該函數(shù)接受表的名稱并返回該表中的行數(shù):

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

嘗試在用戶表上執(zhí)行該功能:

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 9, in count_rows
psycopg2.errors.SyntaxError: syntax error at or near "'users'"
LINE 5:                 'users'
                        ^

該命令無法生成SQL。數(shù)據(jù)庫適配器將變量視為字符串或文字。但是,表名不是純字符串。這就是SQL組合的用武之地

我們已經(jīng)知道使用字符串插值來編寫SQL是不安全的。psycopg提供了一個名為的模塊psycopg.sql,可以幫助我們安全地編寫SQL查詢。讓我們使用psycopg.sql.SQL()以下代碼重寫該函數(shù):

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

此實現(xiàn)有兩個區(qū)別。sql.SQL()組成查詢。sql.Identifier()對參數(shù)值進行注釋table_name(標識符是列或表的名稱)

現(xiàn)在,我們嘗試在users表上執(zhí)行該函數(shù):

>>> count_rows('users')
2

接下來,讓我們看看表不存在時會發(fā)生什么:

>>> count_rows('wangwu')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 11, in count_rows
psycopg2.errors.UndefinedTable: relation "wangwu" does not exist
LINE 5:                 "wangwu"
                        ^

該函數(shù)引發(fā)UndefinedTable異常。將使用此異常來表明我們的函數(shù)可以安全地免受Python SQL注入攻擊

要將所有內(nèi)容放在一起,添加一個選項以對表中的行進行計數(shù),直到達到特定限制。對于非常大的表,這個功能很有用。要實現(xiàn)這個操作,LIMIT在查詢中添加一個子句,以及該限制值的查詢參數(shù):

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

在上面的代碼中,limit使用注釋了sql.Literal()。與前面的列子一樣,psycopg使用簡單方法時,會將所有查詢參數(shù)綁定為文字。但是,使用時sql.SQL(),需要使用sql.Identifier()或顯式注釋每個參數(shù)sql.Literal()

不幸的是,Python API規(guī)范不解決標識符的綁定,僅處理文字。Psycopg是唯一流行的適配器,它添加了使用文字和標識符安全地組合SQL的功能。這個事實使得在綁定標識符時要特別注意

執(zhí)行該函數(shù)以確保其起作用:

>>> count_rows('users', 1)
1
>>> count_rows('users', 10)
2

現(xiàn)在我們已經(jīng)看到該函數(shù)正在運行,檢查它是否安全:

>>> 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_rows
psycopg2.errors.UndefinedTable: relation "(select 1) as wangwu; update users set admin = true where name = '" does not exist
LINE 8:                     "(select 1) as wangwu; update users set adm...
                            ^                     

異常顯示psycopg轉(zhuǎn)義了該值,并且數(shù)據(jù)庫將其視為表名。由于不存在具有該名稱的表,因此UndefinedTable引發(fā)了異常所以是安全的!

6. 結(jié)論

通過實現(xiàn)組成動態(tài)SQL,可與你使我們有效的規(guī)避系統(tǒng)遭受Python SQL注入的威脅!在查詢過程中同時使用文字和標識符,并不會影響安全性

以上就是關(guān)于如何使用Python防止SQL注入攻擊的內(nèi)容,如果你們有學習到知識或者技能,可以把它分享出去讓更多的人看到。

向AI問一下細節(jié)

免責聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI