??無論是一個簡單的博客,還是大型的社交網站,Web安全都應該放在首位。Web安全問題涉及廣泛,在這里介紹其中常見的幾種攻擊 (attack)和其他常見的漏洞(vulnerability。
??對于Web程序的安全問題,一個首要的原則是:永遠不要相信你的用戶。大部分Web安全問題都是因為沒有對用戶輸入的內容進行“消毒”造成的。
1.注入攻擊
??在OWASP(Open Web Application Security Project,開放式Web程序安全項目)發布的最危險的Web程序安全風險Top 10中,無論是最新的2017年的排名,2013年的排名還是最早的2010年,注入攻擊 (Injection)都位列第一。注入攻擊包括系統命令(OS Command)注入、SQL(Structured Query Language,結構化查詢語言)注入(SQL Injection)、NoSQL注入、ORM(Object Relational Mapper,對象關系
映射)注入等。我們這里重點介紹的是SQL注入。
(1)攻擊原理
??在編寫SQL語句時,如果直接將用戶傳入的數據作為參數使用字符串拼接的方式插入到SQL查詢中,那么攻擊者可以通過注入其他語句來 執行攻擊操作,這些攻擊操作包括可以通過SQL語句做的任何事:獲取敏感數據、修改數據、刪除數據庫表……
(2)攻擊示例
??假設我們的程序是一個學生信息查詢程序,其中的某個視圖函數接收用戶輸入的密碼,返回根據密碼查詢對應的數據。我們的數據庫由一 個db對象表示,SQL語句通過execute()方法執行:
@app.route('/students')
def bobby_table():
password = request.args.get('password')
cur = db.execute("SELECT * FROM students WHERE password='%s';" % password)
results = cur.fetchall()
return results
注:在實際應用中,敏感數據需要通過表單提交的POST請求接收,這 里為了便于演示,我們通過查詢參數接收。
??我們通過查詢字符串獲取用戶輸入的查詢參數,并且不經過任何處理就使用字符串格式化的方法拼接到SQL語句中。在這種情況下,如果 攻擊者輸入的password參數值為“'or 1=1--”, 那么最終視圖函數中 被執行的SQL語句將變為:
SELECT * FROM students WHERE password='' or 1=1 --;'
這時會把students表中的所有記錄全部查詢并返回,也就意味著所有的記錄都被攻擊者竊取了。更可怕的是,如果攻擊者將password參數 的值設為“';drop table users;--”,那么查詢語句就會變成:
SELECT * FROM students WHERE password=''; drop table students; --;
執行這個語句會把students表中的所有記錄全部刪除掉。(“--”用來注釋后面的語句)
(3)主要防范方法
1)使用ORM可以一定程度上避免SQL注入問題。
2)驗證輸入類型。比如某個視圖函數接收整型id來查詢,那么就 在URL規則中限制URL變量為整型。
3)參數化查詢。在構造SQL語句時避免使用拼接字符串或字符串
格式化(使用百分號或format()方法)的方式來構建SQL語句。而要使用各類接口庫提供的參數化查詢方法。
db.execute('SELECT * FROM students WHERE password=?, password)
4)轉義特殊字符,比如引號、分號和橫線等。
2 XSS攻擊
XSS(Cross-Site Scripting,跨站腳本)攻擊
(1)攻擊原理
??XSS是注入攻擊的一種,攻擊者通過將代碼注入被攻擊者的網站 中,用戶一旦訪問網頁便會執行被注入的惡意腳本。XSS攻擊主要分為 反射型XSS攻擊(Reflected XSS Attack)和存儲型XSS攻擊(Stored XSS Attack)兩類。
??反射型XSS又稱為非持久型XSS(Non-Persistent XSS)。當某個站點存在XSS漏洞時,這種攻擊會通過URL注入攻擊腳本,只有當用戶訪問這個URL時才會執行攻擊腳本。我們在本章前面介紹查詢字符串和 cookie時引入的示例就包含反射型XSS漏洞,如下所示:
@app.route('/hello')
def hello():
name = request.args.get('name')
response = '<h1>Hello, %s!</h1>' % name
??這個視圖函數接收用戶通過查詢字符串傳入的數據,未做任何處理 就把它直接插入到返回的響應主體中,返回給客戶端。如果某個用戶輸入了一段JavaScript代碼作為查詢參數name的值,如下所示:
name=<script>alert('Bingo!');</script>
??當客戶端接收到響應后,瀏覽器解析這行代碼就會打開一個彈窗,你覺得一個小彈窗不會造成什么危害?那你就完全錯了,能夠執行 alert()函數就意味著通過這種方式可以執行任意JavaScript代碼。即攻擊者通過JavaScript幾乎能夠做任何事情:竊取用戶的cookie和其他敏感 數據,重定向到釣魚網站,發送其他請求,執行諸如轉賬、發布廣告信 息、在社交網站關注某個用戶等。即使不插入JavaScript代碼,通過HTML和CSS(CSS注入)也可以 影響頁面正常的輸出,篡改頁面樣式,插入圖片等。
??存儲型XSS也被稱為持久型XSS(persistent XSS),這種類型的 XSS攻擊更常見,危害也更大。它和反射型XSS類似,不過會把攻擊代碼儲存到數據庫中,任何用戶訪問包含攻擊代碼的頁面都會被殃及。比如,某個網站通過表單接收用戶的留言,如果服務器接收數據后未經處 理就存儲到數據庫中,那么用戶可以在留言中插入任意JavaScript代碼。 比如,攻擊者在留言中加入一行重定向代碼:
<script>window.location.;</script>
其他任意用戶一旦訪問留言板頁面,就會執行其中的JavaScript腳本。就會被重定向到攻擊者寫入的站點。
(3)主要防范措施
a.HTML轉義
??防范XSS攻擊最主要的方法是對用戶輸入的內容進行HTML轉義, 轉義后可以確保用戶輸入的內容在瀏覽器中作為文本顯示,而不是作為代碼解析。這里的轉義和Python中的概念相同,即消除代碼執行時的歧義,也就是把變量標記的內容標記為文本,而不是HTML代碼。具體來說,這會把變量中與HTML相關的符號轉換為安全字符,以避免變量中包含影響頁面輸出的HTML標簽或惡意的JavaScript代碼。
比如,我們可以使用Jinja2提供的escape()函數對用戶傳入的數據 進行轉義:
from jinja2 import escape
@app.route('/hello')
def hello():
name = request.args.get('name')
response = '<h1>Hello, %s!</h1>' % escape(name)
b.驗證用戶輸入
??XSS攻擊可以在任何用戶可定制內容的地方進行,例如圖片引用、自定義鏈接。僅僅轉義HTML中的特殊字符并不能完全規避XSS攻擊,因為在某些HTML屬性中,使用普通的字符也可以插入JavaScript代碼。 除了轉義用戶輸入外,我們還需要對用戶的輸入數據進行類型驗證。在 所有接收用戶輸入的地方做好驗證工作。
以某個程序的用戶資料頁面為例,我們來演示一下轉義無法完全避免的XSS攻擊。。程序允許用戶輸入個人資料中的個人網站地址,通過下面的方式顯示在資料頁面中:
<a href="{{ url }}">Website</a>
其中{{url}}部分表示會被替換為用戶輸入的url變量值。如果不對 URL進行驗證,那么用戶就可以寫入JavaScript代碼,比如“javascript: alert('Bingo!');”。因為這個值并不包含會被轉義的<和>。最終頁面 上的鏈接代碼會變為:
<a href="javascript:alert('Bingo!');">Website</a>
類似的,{{url}}部分表示會被替換為用戶輸入的url變量值。如果不對輸入的URL進行驗證,那么用戶可以將url設為“123"onerror="alert('Bingo!')”,最終的<img>標簽就會變為:
<img src="123" onerror="alert('Bingo!')">
在這里因為src中傳入了一個錯誤的URL,瀏覽器便會執行onerror屬 性中設置的JavaScript代碼。
所以需要對用戶輸入進行驗證
3.CSRF攻擊
CSRF(Cross Site Request Forgery,跨站請求偽造)是一種近年來 才逐漸被大眾了解的網絡攻擊方式,又被稱為One-Click Attack或Session Riding。
(1)攻擊原理
??CSRF攻擊的大致方式如下:某用戶登錄了A網站,認證信息保存在 cookie中。當用戶訪問攻擊者創建的B網站時,攻擊者通過在B網站發送 一個偽造的請求提交到A網站服務器上,讓A網站服務器誤以為請求來自于自己的網站,于是執行相應的操作,該用戶的信息便遭到了篡改。總結起來就是,攻擊者利用用戶在瀏覽器中保存的認證信息,向對應的站點發送偽造請求。在前面學習cookie時,我們介紹過用戶認證通過保存在cookie中的數據實現。在發送請求時,只要瀏覽器中保存了對應的 cookie,服務器端就會認為用戶已經處于登錄狀態,而攻擊者正是利用了這一機制。
(2)攻擊示例
??假設我們網站是一個社交網站簡稱網站A;攻擊者的網站可以是任意類型的網站,簡稱網站B。在我們的網站中,刪除賬戶的操作通過GET請求執行,由使用下面的delete_account視圖處理:
@app.route('/account/delete')
def delete_account():
if not current_user.authenticated:
abort(401)
current_user.delete()
return 'Deleted!'
當用戶登錄后,只要訪問http://A.com/account/delete就會刪 賬戶。那么在攻擊者的網站上,只需要創建一個顯示圖片的img標簽, 其中的src屬性加入刪除賬戶的URL:
<img src="http://A.com/account/delete">
當用戶訪問B網站時,瀏覽器在解析網頁時會自動向img標簽的src 屬性中的地址發起請求。此時你在A網站的登錄信息保存在cookie中, 因此,僅僅是訪問B網站的頁面就會讓你的賬戶被刪除掉。
??當然,現實中很少有網站會使用GET請求來執行包含數據更改的敏感操作,這里只是一個示例。現在,假設我們吸取了教訓,改用POST 請求提交刪除賬戶的請求。盡管如此,攻擊者只需要在B網站中內嵌一 個隱藏表單,然后設置在頁面加載后執行提交表單的JavaScript函數,攻 擊仍然會在用戶訪問B網站時發起。
(3)主要防范措施
a.正確使用HTTP方法
??防范CSRF的基礎就是正確使用HTTP方法。在前面我們介紹過 HTTP中的常用方法。在普通的Web程序中,一般只會使用到GET和 POST方法。而且,目前在HTML中僅支持GET和POST方法(借助 AJAX則可以使用其他方法)。在使用HTTP方法時,通常應該遵循下面 的原則:
-GET方法屬于安全方法,不會改變資源狀態,僅用于獲取資源, 因此又被稱為冪等方法(idempotent method)。頁面中所有可以通過鏈接發起的請求都屬于GET請求。
-POST方法用于創建、修改和刪除資源。在HTML中使用form標簽 創建表單并設置提交方法為POST,在提交時會創建POST請求。
b.CSRF令牌校驗
當處理非GET請求時,要想避免CSRF攻擊,關鍵在于判斷請求是否來自自己的網站。在前面我們曾經介紹過使用HTTP referer獲取請求來源,理論上說,通過referer可以判斷源站點從而避免CSRF攻擊,但因 為referer很容易被修改和偽造,所以不能作為主要的防御措施。
除了在表單中加入驗證碼外,一般的做法是通過在客戶端頁面中加入偽隨機數來防御CSRF攻擊,這個偽隨機數通常被稱為CSRF令牌 (token)。
在HTML中,POST方法的請求通過表單創建。我們把在服務器端創建的偽隨機數(CSRF令牌)添加到表單中的隱藏字段里和session變量(即簽名cookie)中,當用戶提交表單時,這個令牌會和表單數據一起提交。在服務器端處理POST請求時,我們會對表單中的令牌值進 驗證,如果表單中的令牌值和session中的令牌值相同,那么就說明請求發自自己的網站。因為CSRF令牌在用戶向包含表單的頁面發起GET請求時創建,并且在一定時間內過期,一般情況下攻擊者無法獲取到這個令牌值,所以我們可以有效地區分出請求的來源是否安全。
除了這幾個攻擊方式外,我們還有很多安全問題要注意。比如文件上傳漏洞、敏感數據存儲、用戶認證(authentication)與權限管理等。
這些內容我們將在后面的陸續介紹。