來源:jizhi.im/blog/post/sql_injection_intro
先來看一副很有意思的漫畫:
相信大家對于學(xué)校們糟糕的網(wǎng)絡(luò)環(huán)境和運維手段都早有體會,在此就不多做吐槽了。今天我們來聊一聊SQL注入相關(guān)的內(nèi)容。
何謂SQL注入?
是一種非常常見的數(shù)據(jù)庫攻擊手段,漏洞也是網(wǎng)絡(luò)世界中最普遍的漏洞之一。大家也許都聽過某某學(xué)長通過攻擊學(xué)校數(shù)據(jù)庫修改自己成績的事情,這些學(xué)長們一般用的就是方法。
SQL注入 其實就是惡意用戶通過在表單中填寫包含SQL關(guān)鍵字的數(shù)據(jù)來使數(shù)據(jù)庫執(zhí)行非常規(guī)代碼的過程。簡單來說,就是數(shù)據(jù)「越俎代庖」做了代碼才能干的事情。
這個問題的來源是,SQL數(shù)據(jù)庫的操作是通過SQL語句來執(zhí)行的,而無論是執(zhí)行代碼還是數(shù)據(jù)項都必須寫在SQL語句之中,這就導(dǎo)致如果我們在數(shù)據(jù)項中加入了某些SQL語句關(guān)鍵字(比如說SELECT、DROP等等),這些關(guān)鍵字就很可能在數(shù)據(jù)庫寫入或讀取數(shù)據(jù)時得到執(zhí)行。
多言無益,我們拿真實的案例來說話。下面我們先使用SQLite建立一個學(xué)生檔案表。
SQL數(shù)據(jù)庫操作示例:
importsqlite3
連接數(shù)據(jù)庫:
conn= sqlite3.connect( 'test.db')
建立新的數(shù)據(jù)表:
conn.execute(''' DROPTABLEIFEXISTSstudents;
CREATETABLEstudents
( idINTEGERPRIMARY KEYAUTOINCREMENT,
nameTEXTNOTNULL);''')
插入學(xué)生信息:
students = [ 'Paul', 'Tom', 'Tracy', 'Lily']
forname instudents:
query = "INSERT INTO students (name) VALUES ('%s')"% (name)
conn.execute(query);
檢視已有的學(xué)生信息:
cursor= conn. execute( "SELECT id, name from students")
print( 'IDName')
forrow in cursor:
print( '{0}{1}'.format(row[ 0], row[ 1]))
conn. close
點擊運行按鈕將會打印目前表中的內(nèi)容。上述程序中我們建立了一個test.db數(shù)據(jù)庫以及一個students數(shù)據(jù)表,并向表中寫入了四條學(xué)生信息。
那么SQL注入 又是怎么一回事呢?我們嘗試再插入一條惡意數(shù)據(jù),數(shù)據(jù)內(nèi)容就是漫畫中的"Robert');DROP TABLE students;--",看看會發(fā)生什么情況。
SQL數(shù)據(jù)庫注入示例:
conn= sqlite3.connect( 'test.db')
插入包含注入代碼的信息:
name = "Robert'); DROPTABLEstudents; --"
query = " INSERTINTOstudents ( name) VALUES( '%s') " % (name)
conn.execute(query)
檢視已有的學(xué)生信息:
cursor= conn. execute( "SELECT id, name from students")
print( 'IDName')
forrow in cursor:
print( '{0}{1}'.format(row[ 0], row[ 1]))
conn. close
你將會發(fā)現(xiàn),運行后,程序沒有輸出任何數(shù)據(jù)內(nèi)容,而是返回一條錯誤信息:表單students無法找到!
這是為什么呢?問題就在于我們所插入的數(shù)據(jù)項中包含SQL關(guān)鍵字DROP TABLE,這兩個關(guān)鍵字的意義是從數(shù)據(jù)庫中清除一個表單。
而關(guān)鍵字之前的Robert');使得SQL執(zhí)行器認為上一命令已經(jīng)結(jié)束,從而使得危險指令DROP TABLE得到執(zhí)行。
也就是說,這段包含DROP TABLE關(guān)鍵字的數(shù)據(jù)項使得原有的簡單的插入姓名信息的SQL語句:
INSERTINTOstudents ( name) VALUES( 'Robert')
變?yōu)榱送瑫r包含另外一條清除表單命令的語句:
INSERTINTOstudents ( name) VALUES( 'Robert'); DROPTABLEstudents;
而SQL數(shù)據(jù)庫執(zhí)行上述操作后,students表單被清除,因而表單無法找到,所有數(shù)據(jù)項丟失。
如何防止SQL注入問題呢?
大家也許都想到了,注入問題都是因為執(zhí)行了數(shù)據(jù)項中的SQL關(guān)鍵字,那么,只要檢查數(shù)據(jù)項中是否存在SQL關(guān)鍵字不就可以了么?
的確是這樣,很多數(shù)據(jù)庫管理系統(tǒng)都是采取了這種看似『方便快捷』的過濾手法,但是這并不是一種根本上的解決辦法,如果有個美國人真的就叫做『Drop Table』呢? 你總不能逼人家改名字吧。
合理的防護辦法有很多。首先,盡量避免使用常見的數(shù)據(jù)庫名和數(shù)據(jù)庫結(jié)構(gòu)。在上面的案例中,如果表單名字并不是students,則注入代碼將會在執(zhí)行過程中報錯,也就不會發(fā)生數(shù)據(jù)丟失的情況—— SQL注入 并不像大家想象得那么簡單,它需要攻擊者本身對于數(shù)據(jù)庫的結(jié)構(gòu)有足夠的了解才能成功,因而在構(gòu)建數(shù)據(jù)庫時盡量使用較為復(fù)雜的結(jié)構(gòu)和命名方式將會極大地減少被成功攻擊的概率。
使用正則表達式等字符串過濾手段限制數(shù)據(jù)項的格式、字符數(shù)目等也是一種很好的防護措施。理論上,只要避免數(shù)據(jù)項中存在引號、分號等特殊字符就能很大程度上避免 SQL注入 的發(fā)生。
另外,就是使用各類程序文檔所推薦的數(shù)據(jù)庫操作方式來執(zhí)行數(shù)據(jù)項的查詢與寫入操作,比如在上述的案例中,如果我們稍加修改,首先使用execute方法來保證每次執(zhí)行僅能執(zhí)行一條語句,然后將數(shù)據(jù)項以參數(shù)的方式與SQL執(zhí)行語句分離開來,就可以完全避免 SQL注入 的問題,如下SQL數(shù)據(jù)庫反注入示例。
conn= sqlite3.connect( 'test.db')
以安全方式插入包含注入代碼的信息:
name = "Robert'); DROPTABLEstudents; --"
query = " INSERTINTOstudents ( name) VALUES(?) "
conn.execute(query, [name])
檢視已有的學(xué)生信息:
cursor= conn. execute( "SELECT id, name from students")
print( 'IDName')
forrow in cursor:
print( '{0}{1}'.format(row[ 0], row[ 1]))
conn. close
而對于php而言,則可以通過MySQL_real_escape_string等方法對SQL關(guān)鍵字進行轉(zhuǎn)義,必要時審查數(shù)據(jù)項目是否安全來防治 SQL注入 。
當(dāng)然,做好數(shù)據(jù)庫的備份,同時對敏感內(nèi)容進行加密永遠是最重要的。某些安全性問題可能永遠不會有完美的解決方案,只有我們做好最基本的防護措施,才能在發(fā)生問題的時候亡羊補牢,保證最小程度的損失。