第二十章: 安全

第二十章: 安全

Internet並不安全。html

現現在,天天都會出現新的安全問題。 咱們目擊過病毒飛速地蔓延,大量被控制的肉雞做爲武器來攻擊其餘人,與垃圾郵件的永無止境的軍備競賽,以及許許多多站點被黑的報告。python

做爲Web開發人員,咱們有責任來對抗這些黑暗的力量。 每個Web開發者都應該把安全當作是Web編程中的基礎部分。 不幸的是,要實現安全是困難的。web

Django試圖減輕這種難度。 它被設計爲自動幫你避免一些web開發新手(甚至是老手)常常會犯的錯誤。 儘管如此,須要弄清楚,Django如何保護咱們,以及咱們能夠採起哪些重要的方法來使得咱們的代碼更加安全。sql

首先,一個重要的前提: 咱們並不打算給出web安全的一個詳盡的說明,所以咱們也不會詳細地解釋每個薄弱環節。 在這裏,咱們會給出Django所面臨的安全問題的一個大概。數據庫

Web安全現狀

若是你從這章中只學到了一件事情,那麼它會是:django

在任何條件下都不要相信瀏覽器端提交的數據。編程

你從不會知道HTTP鏈接的另外一端會是誰。 多是一個正常的用戶,可是一樣多是一個尋找漏洞的邪惡的駭客。瀏覽器

從瀏覽器傳過來的任何性質的數據,都須要近乎狂熱地接受檢查。 這包括用戶數據(好比Web表單提交的內容)和帶外數據(好比,HTTP頭、cookies以及其餘信息)。 要修改那些瀏覽器自動添加的元數據,是一件很容易的事。安全

在這一章所提到的全部的安全隱患都直接源自對傳入數據的信任,而且在使用前不加處理。 你須要不斷地問本身,這些數據從何而來。服務器

SQL注入

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。

  • 使用底層數據庫API的查詢。 (詳見第十章)

以上列舉的每個示例都可以很容易的讓您的應用獲得保護。 在每個示例中,爲了不字符串被篡改而使用 綁定參數 來代替。這樣,本節開始的例子應該寫成這樣:

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 ,這個函數可以根據當前數據庫引用結構對這些標識符進行轉義。

跨站點腳本 (XSS)

在Web應用中, 跨站點腳本 (XSS)有時在被渲染成HTML以前,不能恰當地對用戶提交的內容進行轉義。 這使得攻擊者可以向你的網站頁面插入一般以 <script> 標籤形式的任意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=<i>Jacob</i> 時又會發生什麼呢?

<h1>Hello, <i>Jacob</i>!</h1>

固然,一個攻擊者不會使用<i>標籤開始的相似代碼,他可能會用任意內容去包含一個完整的 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, &lt;i&gt;Jacob&lt;/i&gt;!</h1>

咱們在第四章涵蓋了Django的自動轉義,一塊兒想辦法將其關閉。 甚至,若是Django真的新增了這些特性,您也應該習慣性的問本身,一直以來,這些數據都來自於哪裏呢? 沒有哪一個自動解決方案可以永遠保護您的站點百分之百的不會受到XSS攻擊。

僞造跨站點請求

僞造跨站點請求(CSRF)發生在當某個惡意Web站點誘騙用戶不知不覺的從一個信任站點下載某個URL之時,這個信任站點已經被經過信任驗證,所以惡意站點就利用了這個被信任狀態。

Django擁有內建工具來防止這種攻擊。 包括攻擊自己及其使用的工具都在有詳細介紹。16章

會話僞造/劫持

這不是某個特定的攻擊,而是對用戶會話數據的通用類攻擊。 這種攻擊能夠採起多種形式:

中間人 攻擊:檢索所在有線(無線)網絡,監聽會話數據。

僞造會話 :攻擊者利用會話ID(多是經過中間人攻擊來得到)將本身假裝成另外一個用戶。

這兩種攻擊的一個例子能夠是在一間咖啡店裏的某個攻擊者利用店內的無線網絡來捕獲某個會話cookie,而後她就能夠利用那個cookie來假冒原始用戶。 她即可以使該cookie來模擬原始用戶。

僞造cookie :就是指某個攻擊者覆蓋了在某個cookie中本應該是隻讀的數據。 ` 第十四章 <../chapter14/>`__ 詳細介紹了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框架(參見` 第十四章 <../chapter14/>`__ )根本不會允許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,咱們祝你好運。

相關文章
相關標籤/搜索