Internet并不安全。
現如今,每天都會出現新的安全問題。 我們目睹過病毒飛速地蔓延,大量被控制的肉雞作為武器來攻擊其他人,與垃圾郵件的永無止境的軍備競賽,以及許許多多站點被黑的報告。
作為Web開發人員,我們有責任來對抗這些黑暗的力量。 每一個Web開發者都應該把安全看成是Web編程中的基礎部分。 不幸的是,要實現安全是困難的。
Django試圖減輕這種難度。 它被設計為自動幫你避免一些web開發新手(甚至是老手)經常會犯的錯誤。 盡管如此,需要弄清楚,Django如何保護我們,以及我們可以采取哪些重要的方法來使得我們的代碼更加安全。
首先,一個重要的前提: 我們并不打算給出web安全的一個詳盡的說明,因此我們也不會詳細地解釋每一個薄弱環節。 在這里,我們會給出Django所面臨的安全問題的一個大概。
如果你從這章中只學到了一件事情,那么它會是:
在任何條件下都不要相信瀏覽器端提交的數據。
你從不會知道HTTP連接的另一端會是誰。 可能是一個正常的用戶,但是同樣可能是一個尋找漏洞的邪惡的駭客。
從瀏覽器傳過來的任何性質的數據,都需要近乎狂熱地接受檢查。 這包括用戶數據(比如Web表單提交的內容)和帶外數據(比如,HTTP頭、cookies以及其他信息)。 要修改那些瀏覽器自動添加的元數據,是一件很容易的事。
在這一章所提到的所有的安全隱患都直接源自對傳入數據的信任,并且在使用前不加處理。 你需要不斷地問自己,這些數據從何而來。
SQL注入 是一個很常見的形式,在SQL注入中,攻擊者改變web網頁的參數(例如 GET /POST 數據或者URL地址),加入一些其他的SQL片段。 未加處理的網站會將這些信息在后臺數據庫直接運行。
這種危險通常在由用戶輸入構造SQL語句時產生。 例如,假設我們要寫一個函數,用來從通信錄搜索頁面收集一系列的聯系信息。 為防止垃圾郵件發送器閱讀系統中的email,我們將在提供email地址以前,首先強制用戶輸入用戶名。
def user_contacts(request):
user = request.GET['username']
sql = "SELECT * FROM user_contacts WHERE username = '%s';" % username
# execute the SQL here...
備注
在這個例子中,以及在以下所有的“不要這樣做”的例子里,我們都去除了大量的代碼,避免這些函數可以正常工作。 我們可不想這些例子被拿出去使用。
盡管,一眼看上去,這一點都不危險,實際上卻不盡然。
首先,我們對于保護email列表所采取的措施,遇到精心構造的查詢語句就會失效。 想象一下,如果攻擊者在查詢框中輸入 "' OR 'a'='a"
。 此時,查詢的字符串會構造如下:
SELECT * FROM user_contacts WHERE username = '' OR 'a' = 'a';
由于我們允許不安全的SQL語句出現在字符串中,攻擊者加入 OR 子句,使得每一行數據都被返回。
事實上,這是最溫和的攻擊方式。 如果攻擊者提交了 "'; DELETE FROM user_contacts WHERE 'a' = 'a'"
,我們最終將得到這樣的查詢:
SELECT * FROM user_contacts WHERE username = ''; DELETE FROM user_contacts WHERE 'a' = 'a';
哦!我們整個通信錄名單去哪兒了? 我們整個通訊錄會被立即刪除
盡管這個問題很陰險,并且有時很難發現,解決方法卻很簡單: 絕不信任用戶提交的數據,并且在傳遞給SQL語句時,總是轉義它。
Django的數據庫API幫你做了。 它會根據你所使用的數據庫服務器(例如PostSQL或者MySQL)的轉換規則,自動轉義特殊的SQL參數。
舉個例子,在下面這個API調用中:
foo.get_list(bar__exact="' OR 1=1")
Django會自動進行轉義,得到如下表達:
SELECT * FROM foos WHERE bar = '\' OR 1=1'
完全無害。
這被運用到了整個Django的數據庫API中,只有一些例外:
傳給 extra() 方法的 where 參數。 (參考 附錄 C。) 這個參數故意設計成可以接受原始的SQL。
以上列舉的每一個示例都能夠很容易的讓您的應用得到保護。 在每一個示例中,為了避免字符串被篡改而使用綁定參數 來代替。這樣,本節開始的例子應該寫成這樣:
from django.db import connection
def user_contacts(request):
user = request.GET['username']
sql = "SELECT * FROM user_contacts WHERE username = %s"
cursor = connection.cursor()
cursor.execute(sql, [user])
# ... do something with the results
底層 execute 方法采用了一個SQL字符串作為其第二個參數,這個SQL字符串包含若干’%s’占位符,execute方法能夠自動對傳入列表中的參數進行轉義和插入。 你應該用 always 這種方式構造自定義的SQL。
不幸的是,您并不是在SQL中能夠處處都使用綁定參數,綁定參數不能夠作為標識符(如表或列名等)。 因此,如果您需要這樣做—我是說—動態構建 POST 變量中的數據庫表的列表的話,您需要在您的代碼中來對這些數據庫表的名字進行轉義。 Django提供了一個函數, django.db.backend.quote_name ,這個函數能夠根據當前數據庫引用結構對這些標識符進行轉義。
在Web應用中, 跨站點腳本 (XSS)有時在被渲染成HTML之前,不能恰當地對用戶提交的內容進行轉義。 這使得攻擊者能夠向你的網站頁面插入通常以 標簽形式的任意HTML代碼。
攻擊者通常利用XSS攻擊來竊取cookie和會話信息,或者誘騙用戶將其私密信息透漏給被人(又稱 釣魚 )。
這種類型的攻擊能夠采用多種不同的方式,并且擁有幾乎無限的變體,因此我們還是只關注某個典型的例子吧。 讓我們來想想這樣一個極度簡單的Hello World視圖:
from django.http import HttpResponse
def say_hello(request):
name = request.GET.get('name', 'world')
return HttpResponse('<h1>Hello, %s!</h1>' % name)
這個視圖只是簡單的從GET參數中讀取姓名然后將姓名傳遞給hello.html模板。 因此,如果我們訪問http://example.com/hello/?name=Jacob
,被呈現的頁面將會包含一以下這些:
<h1>Hello, Jacob!</h1>
但是,等等,如果我們訪問 http://example.com/hello/?name=Jacob
時又會發生什么呢?
<h1>Hello, <i>Jacob</i>!</h1>
當然,一個攻擊者不會使用標簽開始的類似代碼,他可能會用任意內容去包含一個完整的HTML集來劫持您的頁面。 這種類型的攻擊已經運用于虛假銀行站點以誘騙用戶輸入個人信息,事實上這就是一種劫持XSS的形式,用以使用戶向攻擊者提供他們的銀行帳戶信息。
如果您將這些數據保存在數據庫中,然后將其顯示在您的站點上,那么問題就變得更嚴重了。 例如,一旦MySpace被發現這樣的特點而能夠輕易的被XSS攻擊,后果不堪設想。 某個用戶向他的簡介中插入JavaScript,使得您在訪問他的簡介頁面時自動將其加為您的好友,這樣在幾天之內,這個人就能擁有上百萬的好友。 在幾天的時間里,他擁有了數以百萬的朋友。
現在,這種后果聽起來還不那么惡劣,但是您要清楚——這個攻擊者正設法將 他 的代碼而不是MySpace的代碼運行在 您 的計算機上。 這顯然違背了假定信任——所有運行在MySpace上的代碼應該都是MySpace編寫的,而事實上卻不如此。
MySpace是極度幸運的,因為這些惡意代碼并沒有自動刪除訪問者的帳戶,沒有修改他們的密碼,也并沒有使整個站點一團糟,或者出現其他因為這個弱點而導致的其他噩夢。
解決方案是簡單的: 總是轉義可能來自某個用戶的任何內容。
為了防止這種情況,Django的模板系統自動轉義所有的變量值。 讓我們來看看如果我們使用模板系統重寫我們的例子會發生什么
# views.py
from django.shortcuts import render_to_response
def say_hello(request):
name = request.GET.get('name', 'world')
return render_to_response('hello.html', {'name': name})
# hello.html
<h1>Hello, {{ name }}!</h1>
這樣,一個到http://example.com/hello/name=Jacob
的請求將導致下面的頁面:
<h1>Hello, <i>Jacob</i>!</h1>
我們在第四章涵蓋了Django的自動轉義,一起想辦法將其關閉。 甚至,如果Django真的新增了這些特性,您也應該習慣性的問自己,一直以來,這些數據都來自于哪里呢? 沒有哪個自動解決方案能夠永遠保護您的站點百分之百的不會受到XSS攻擊。
偽造跨站點請求(CSRF)發生在當某個惡意Web站點誘騙用戶不知不覺的從一個信任站點下載某個URL之時,這個信任站點已經被通過信任驗證,因此惡意站點就利用了這個被信任狀態。
Django擁有內建工具來防止這種攻擊。 包括攻擊本身及其使用的工具都在有詳細介紹。16章
這不是某個特定的攻擊,而是對用戶會話數據的通用類攻擊。 這種攻擊可以采取多種形式:
中間人 攻擊:檢索所在有線(無線)網絡,監聽會話數據。
偽造會話 :攻擊者利用會話ID(可能是通過中間人攻擊來獲得)將自己偽裝成另一個用戶。
這兩種攻擊的一個例子可以是在一間咖啡店里的某個攻擊者利用店內的無線網絡來捕獲某個會話cookie,然后她就可以利用那個cookie來假冒原始用戶。 她便可以使該cookie來模擬原始用戶。
偽造cookie :就是指某個攻擊者覆蓋了在某個cookie中本應該是只讀的數據。
第十四章
__ 詳細介紹了cookies如何工作,以及要點之一的是,它在你不知道的情況下無視瀏覽器和惡意用戶私自改變cookies。Web站點以 IsLoggedIn=1 或者 LoggedInAsUser=jacob 這樣的方式來保存cookie由來已久,使用這樣的cookie是再簡單不過的了。
一個更微妙的層面上,然而,相信在cookies中存儲的任意信息絕對不是一個好主意。 你永遠不知道誰一直在作怪。
會話滯留 :攻擊者誘騙用戶設置或者重設置該用戶的會話ID。
例如,PHP允許在URL(如 http://example.com/?PHPSESSID=fa90197ca25f6ab40bb1374c510d7a32 等)中傳遞會話標識符。攻擊者欺騙用戶點擊一個硬編碼會話ID的鏈接,這回導致用戶轉到那個會話。
會話滯留已經運用在釣魚攻擊中,以誘騙用戶在攻擊者擁有的賬號里輸入其個人信息。 他可以稍后登陸賬戶并且檢索數據。
會話中毒 :攻擊者通過用戶提交設置會話數據的Web表單向該用戶會話中注入潛在危險數據。
一個經典的例子就是一個站點在某個cookie中存儲了簡單的用戶偏好(比如一個頁面背景顏色)。 攻擊者可以誘騙用戶點擊一個鏈接來提交背景顏色,實際上包含了一個XSS攻擊。 如果顏色沒有轉義,那么就可以再把惡意代碼注入到用戶環境中。
有許多基本準則能夠保護您不受到這些攻擊:
不要在URL中包含任何session信息。
Django的session框架(參見
第十四章
__ )根本不會容許session包含在URL中。不要直接在cookie中保存數據。 相反,存儲一個在后臺映射到session數據存儲的session ID。
如果使用Django內置的session框架(即 request.session ),它會自動進行處理。 這個session框架僅在cookie中存儲一個session ID,所有的session數據將會被存儲在數據庫中。
如果需要在模板中顯示session數據,要記得對其進行轉義。 可參考之前的XSS部分,對所有用戶提交的數據和瀏覽器提交的數據進行轉義。 對于session信息,應該像用戶提交的數據一樣對其進行處理。
任何可能的地方都要防止攻擊者進行session欺騙。
盡管去探測究竟是誰劫持了會話ID是幾乎不可能的事兒,Django還是內置了保護措施來抵御暴力會話攻擊。 會話ID被存在哈希表里(取代了序列數字),這樣就阻止了暴力攻擊,并且如果一個用戶去嘗試一個不存在的會話那么她總是會得到一個新的會話ID,這樣就阻止了會話滯留。
請注意,以上沒有一種準則和工具能夠阻止中間人攻擊。 這些類型的攻擊是幾乎不可能被探測的。 如果你的站點允許登陸用戶去查看任意敏感數據的話,你應該 總是 通過HTTPS來提供網站服務。 此外,如果你的站點使用SSL,你應該將 SESSION_COOKIE_SECURE 設置為 True ,這樣就能夠使Django只通過HTTPS發送會話cookie。
郵件頭部注入 :SQL注入的兄弟,是一種通過劫持發送郵件的Web表單的攻擊方式。 攻擊者能夠利用這種技術來通過你的郵件服務器發送垃圾郵件。 在這種攻擊面前,任何方式的來自Web表單數據的郵件頭部構筑都是非常脆弱的。
讓我們看看在我們許多網站中發現的這種攻擊的形式。 通常這種攻擊會向硬編碼郵件地址發送一個消息,因此,第一眼看上去并不顯得像面對垃圾郵件那么脆弱。
但是,大多數表單都允許用戶輸入自己的郵件主題(同時還有from地址,郵件體,有時還有部分其他字段)。 這個主題字段被用來構建郵件消息的主題頭部。
如果那個郵件頭部在構建郵件信息時沒有被轉義,那么攻擊者可以提交類似"hello\ncc:spamvictim@example.com" (這里的 "\n" 是換行符)的東西。 這有可能使得所構建的郵件頭部變成:
To: hardcoded@example.com
Subject: hello
cc: spamvictim@example.com
就像SQL注入那樣,如果我們信任了用戶提供的主題行,那樣同樣也會允許他構建一個頭部惡意集,他也就能夠利用聯系人表單來發送垃圾郵件。
我們能夠采用與阻止SQL注入相同的方式來阻止這種攻擊: 總是校驗或者轉義用戶提交的內容。
Django內建郵件功能(在 django.core.mail 中)根本不允許在用來構建郵件頭部的字段中存在換行符(表單,收件地址,還有主題)。 如果您試圖使用 django.core.mail.send_mail 來處理包含換行符的主題時,Django將會拋出BadHeaderError異常。
如果你沒有使用Django內建郵件功能來發送郵件,那么你需要確保包含在郵件頭部的換行符能夠引發錯誤或者被去掉。 你或許想仔細閱讀 django.core.mail 中的 SateMIMEText 類來看看Django是如何做到這一點的。
目錄遍歷 :是另外一種注入方式的攻擊,在這種攻擊中,惡意用戶誘騙文件系統代碼對Web服務器不應該訪問的文件進行讀取和/或寫入操作。
例子可以是這樣的,某個視圖試圖在沒有仔細對文件進行防毒處理的情況下從磁盤上讀取文件:
def dump_file(request):
filename = request.GET["filename"]
filename = os.path.join(BASE_PATH, filename)
content = open(filename).read()
# ...
盡管一眼看上去,視圖通過 BASE_PATH (通過使用 os.path.join )限制了對于文件的訪問,但如果攻擊者使用了包含 .. (兩個句號,父目錄的一種簡寫形式)的文件名,她就能夠訪問到 BASE_PATH 目錄結構以上的文件。對她來說,發現究竟使用幾個點號只是時間問題,比如這樣:../../../../../etc/passwd
。
任何不做適當轉義地讀取文件操作,都可能導致這樣的問題。 允許 寫 操作的視圖同樣容易發生問題,而且結果往往更加可怕。
這個問題的另一種表現形式,出現在根據URL和其他的請求信息動態地加載模塊。 一個眾所周知的例子來自于Ruby on Rails。 在2006年上半年之前,Rails使用類似于 http://example.com/person/poke/1 這樣的URL直接加載模塊和調用函數。 結果是,精心構造的URL,可以自動地調用任意的代碼,包括數據庫的清空腳本。
如果你的代碼需要根據用戶的輸入來讀寫文件,你就需要確保,攻擊者不能訪問你所禁止訪問的目錄。
備注
不用多說,你 永遠 不要在編寫可以讀取任何位置上的文件的代碼!
Django內置的靜態內容視圖是做轉義的一個好的示例(在 django.views.static 中)。這是相關代碼:
import os
import posixpath
# ...
path = posixpath.normpath(urllib.unquote(path))
newpath = ''
for part in path.split('/'):
if not part:
# strip empty path components
continue
drive, part = os.path.splitdrive(part)
head, part = os.path.split(part)
if part in (os.curdir, os.pardir):
# strip '.' and '..' in path
continue
newpath = os.path.join(newpath, part).replace('\\', '/')
Django不讀取文件(除非你使用 static.serve 函數,但也受到了上面這段代碼的保護),因此這種危險對于核心代碼的影響就要小得多。
更進一步,URLconf抽象層的使用,意味著不經過你明確的指定,Django 決不會 裝載代碼。 通過創建一個URL來讓Django裝載沒有在URLconf中出現的東西,是不可能發生的。
在開發過程中,通過瀏覽器檢查錯誤和跟蹤異常是非常有用的。 Django提供了漂亮且詳細的debug信息,使得調試過程更加容易。
然而,一旦在站點上線以后,這些消息仍然被顯示,它們就可能暴露你的代碼或者是配置文件內容給攻擊者。
還有,錯誤和調試消息對于最終用戶而言是毫無用處的。 Django的理念是,站點的訪問者永遠不應該看到與應用相關的出錯消息。 如果你的代碼拋出了一個沒有處理的異常,網站訪問者不應該看到調試信息或者 _任何_代碼片段或者Python(面向開發者)出錯消息。 訪問者應該只看到友好的無法訪問的頁面。
當然,開發者需要在debug時看到調試信息。 因此,框架就要將這些出錯消息顯示給受信任的網站開發者,而要向公眾隱藏。
正如我們在第12章所提到的,Django的DEBUG
設置控制這些錯誤信息的顯示。 當你準備部署時請確認把這個設置為:False
。
在Apache和mod_python下開發的人員,還要保證在Apache的配置文件中關閉 PythonDebug Off 選項,這個會在Django被加載以前去除出錯消息。
我們希望關于安全問題的討論,不會太讓你感到恐慌。 Web是一個處處布滿陷阱的世界,但是只要有一些遠見,你就能擁有安全的站點。
永遠記住,Web安全是一個不斷發展的領域。如果你正在閱讀這本書的停止維護的那些版本,請閱讀最新版本的這個部分來檢查最新發現的漏洞。 事實上,每周或者每月花點時間挖掘Web應用安全,并且跟上最新的動態是一個很好的主意。 花費很少,但是對你網站和用戶的保護確是無價的。
你已經完成了我們安排的程序。 以下的附錄內容中包含了可能在你的Djang項目中用得上的引用資源.
在運行你的Django網站時,無論是為你或幾個朋友的小網站,或者是下一個google,我們祝你好運。