摘自《Flask Web開發實戰:入門、進階與原理解析(李輝著 )》

對於Web程序的安全問題,一個首要的原則是:永遠不要相信你的用戶。大部分Web安全問題都是因為沒有對用戶輸入的內容進行「消毒」造成的。

1.注入攻擊

注入攻擊包括系統命令(OS Command)注入、SQL注入(SQL Injection)、NoSQL注入、ORM注入等。

下面重點介紹SQL注入。

(1)攻擊原理

在編寫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

我們通過查詢字元串獲取用戶輸入的查詢參數,並且不經過任何處理就使用字元串格式化的方法拼接到SQL語句中。在這種情況下,如果攻擊者輸入的password參數值為「or 1=1--」,即http://example.com/students?password=or 1=1--,那麼最終視圖函數中被執行的SQL語句將變為:

SELECT * FROM students WHERE password= or 1=1 --;

在SQL中,「;」用來結束一行語句;「--」用來注釋後面的語句。這時會把students表中的所有記錄全部查詢並返回。

(3)主要防範方法

1)使用ORM可以一定程度上避免SQL注入問題。

2)驗證輸入類型。比如某個視圖函數接收整型id來查詢,那麼就在URL規則中限制URL變數為整型。

3)參數化查詢。在構造SQL語句時避免使用拼接字元串或字元串格式化(使用百分號或format()方法)的方式來構建SQL語句。而要使用各類介面庫提供的參數化查詢方法,以內置的sqlite3庫為例:

db.execute(SELECT * FROM students WHERE password=?, password)

4)轉義特殊字元,比如引號、分號和橫線等。使用參數化查詢時,各種介面庫會為我們做轉義工作。

2.XSS(Cross-Site Scripting)攻擊

(1)攻擊原理

XSS是注入攻擊的一種,攻擊者通過將代碼注入被攻擊者的網站中,用戶一旦訪問網頁便會執行被注入的惡意腳本。XSS攻擊主要分為反射型XSS攻擊(Reflected XSS Attack)和存儲型XSS攻擊(Stored XSS Attack)兩類。

(2)攻擊示例

反射型XSS又稱為非持久型XSS(Non-Persistent XSS)。當某個站點存在XSS漏洞時,這種攻擊會通過URL注入攻擊腳本,只有當用戶訪問這個URL時才會執行攻擊腳本。

@app.route(/hello)
def hello():
name = request.args.get(name)
response = <h1>Hello, %s!</h1> % name

這個視圖函數接收用戶通過查詢字元串傳入的數據,未做任何處理就把它直接插入到返回的響應主體中,返回給客戶端。如果某個用戶輸入了一段JavaScript代碼作為查詢參數name的值,如下所示:

http://example.com/hello?name=<script>alert(Bingo!);</script>

客戶端接收的響應將變為下面的代碼:

<h1>Hello, <script>alert(Bingo!);</script>!</h1>

當客戶端接收到響應後,瀏覽器解析這行代碼就會打開一個彈窗。

攻擊者通過JavaScript幾乎能夠做任何事情:竊取用戶的cookie和其他敏感數據,重定向到釣魚網站,發送其他請求,執行諸如轉賬、發布廣告信息、在社交網站關注某個用戶等。

即使不插入JavaScript代碼,通過HTML和CSS(CSS注入)也可以影響頁面正常的輸出,篡改頁面樣式,插入圖片等。

XSS攻擊流程:網站A存在XSS漏洞,攻擊者將包含攻擊代碼的網站A的鏈接發送給用戶Foo,當Foo訪問這個鏈接,伺服器就會產生帶有攻擊代碼的響應給用戶Foo,用戶Foo的瀏覽器就會執行攻擊代碼。

存儲型XSS也被稱為持久型XSS(persistent XSS)。它和反射型XSS類似,不過會把攻擊代碼儲存到資料庫中,任何用戶訪問包含攻擊代碼的頁面都會被殃及。

比如,某個網站通過表單接收用戶的留言,如果伺服器接收數據後未經處理就存儲到資料庫中,那麼用戶可以在留言中插入任意JavaScript代碼。

比如,攻擊者在留言中加入一行重定向代碼:

<script>window.location.href="http://attacker.com";</script>

其他任意用戶一旦訪問留言板頁面,就會執行其中的JavaScript腳本。

(3)主要防範措施

a.HTML轉義

防範XSS攻擊最主要的方法是對用戶輸入的內容進行HTML轉義,轉義後可以確保用戶輸入的內容在瀏覽器中作為文本顯示,而不是作為代碼解析。就是把變數標記的內容標記為文本,而不是HTML代碼。

我們可以使用Jinja2提供的escape()函數對用戶傳入的數據進行轉義:

from jinja2 import escape
@app.route(/hello)
def hello():
name = request.args.get(name)
response = <h1>Hello, %s!</h1> % escape(name)

前面的示例中,用戶輸入的JavaScript代碼將被轉義為:

&lt;script&gt;alert(&#34;Bingo!&#34;)&lt;/sript&gt;

轉義後,文本中的特殊字元(比如「>」和「<」)都將被轉義為HTML實體(character entitiy),這行文本最終在瀏覽器中會被顯示為文本形式的<script>alert(Bingo!)</script>。

在Python中,如果你想在單引號標記的字元串中顯示一個單引號,那麼你需要在單引號前添加一個反斜線來轉義它,也就是把它標記為普通文本,而不是作為特殊字元解釋。

在HTML中,也存在許多保留的特殊字元,比如大於小於號。如果你想以文本顯示這些字元,也需要對其進行轉義。

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。這個圖片通過下面的方式顯示:

<img src="{{ url }}">

類似的,{{url}}部分表示會被替換為用戶輸入的url變數值。如果不對輸入的URL進行驗證,那麼用戶可以將url設為「123"onerror="alert(Bingo!)」,最終的<img>標籤就會變為:

<img src="123" onerror="alert(Bingo!)">

在這裡因為src中傳入了一個錯誤的URL,瀏覽器便會執行onerror屬性中設置的JavaScript代碼。

3.CSRF(Cross Site Request Forgery,跨站請求偽造)攻擊

(1)攻擊原理

CSRF攻擊的大致方式如下:某用戶登錄了A網站,認證信息保存在cookie中。當用戶訪問攻擊者創建的B網站時,攻擊者通過在B網站發送一個偽造的請求提交到A網站伺服器上,讓A網站伺服器執行相應的操作。

(2)攻擊示例

假設我們網站是一個社交網站(Example Domain),簡稱網站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://example.com/account/delete就會刪除賬戶。那麼在攻擊者的網站上,只需要創建一個顯示圖片的img標籤,其中的src屬性加入刪除賬戶的URL:

<img src="http://example.com/account/delete">

當用戶訪問B網站時,瀏覽器在解析網頁時會自動向img標籤的src屬性中的地址發起請求。你的賬戶就會被刪除掉。

當然,現實中很少有網站會使用GET請求來執行包含數據更改的敏感操作,這裡只是一個示例。

現在,假設我們吸取了教訓,改用POST請求提交刪除賬戶的請求。儘管如此,攻擊者只需要在B網站中內嵌一個隱藏表單,然後設置在頁面載入後執行提交表單的JavaScript函數,攻擊仍然會在用戶訪問B網站時發起。

(3)主要防範措施

a.正確使用HTTP方法

在使用HTTP方法時,通常應該遵循下面的原則:

  • GET方法屬於安全方法,不會改變資源狀態,僅用於獲取資源。
  • POST方法用於創建、修改和刪除資源。在HTML中使用form標籤創建表單並設置提交方法為POST,在提交時會創建POST請求。

正確使用HTTP方法後,攻擊者就無法通過GET請求來修改用戶的數據,下面我們介紹如何保護GET之外的請求。

b.CSRF令牌校驗

要想避免CSRF攻擊,關鍵在於判斷請求是否來自自己的網站。

在前面我們曾經介紹過使用HTTP referer獲取請求來源,理論上說,通過referer可以判斷源站點從而避免CSRF攻擊,但因為referer很容易被修改和偽造,所以不能作為主要的防禦措施。

除了在表單中加入驗證碼外,一般的做法是通過在客戶端頁面中加入偽隨機數來防禦CSRF攻擊,這個偽隨機數通常被稱為CSRF令牌(token)。

在HTML中,POST方法的請求通過表單創建。我們把在伺服器端創建的偽隨機數(CSRF令牌)添加到表單中的隱藏欄位里和session變數(即簽名cookie)中,當用戶提交表單時,這個令牌會和表單數據一起提交。在伺服器端處理POST請求時,我們會對錶單中的令牌值進行驗證,如果表單中的令牌值和session中的令牌值相同,那麼就說明請求發自自己的網站。

因為CSRF令牌在用戶向包含表單的頁面發起GET請求時創建,並且在一定時間內過期,一般情況下攻擊者無法獲取到這個令牌值,所以我們可以有效地區分出請求的來源是否安全。

對於AJAX請求,我們可以在XMLHttpRequest請求首部添加一個自定義欄位X-CSRFToken來保存CSRF令牌。

如果程序包含XSS漏洞,那麼攻擊者可以使用跨站腳本攻破可能使用的任何跨站請求偽造(CSRF)防禦機制,比如使用JavaScript竊取cookie內容,進而獲取CSRF令牌。

你應該列出一個程序安全項目檢查清單,可以參考OWASP Top 10或是CWE(Common Weakness Enumeration,一般弱點列舉)提供的Top 25(https://cwe.mitre.org/top25/)。確保你的程序所有的安全項目檢查,也可以使用漏洞檢查工具來,比如OWASP提供的WebScarab(https://github.com/OWASP/OWASP-WebScarab)。

推薦閱讀:

相关文章