{% raw %}
# 6.4 預防session劫持
session劫持是一種廣泛存在的比較嚴重的安全威脅,在session技術中,客戶端和服務端通過session的標識符來維護會話, 但這個標識符很容易就能被嗅探到,從而被其他人利用.它是中間人攻擊的一種類型。
本節將通過一個實例來演示會話劫持,希望通過這個實例,能讓讀者更好地理解session的本質。
## session劫持過程
我們寫了如下的代碼來展示一個count計數器:
func count(w http.ResponseWriter, r *http.Request) {
sess := globalSessions.SessionStart(w, r)
ct := sess.Get("countnum")
if ct == nil {
sess.Set("countnum", 1)
} else {
sess.Set("countnum", (ct.(int) + 1))
}
t, _ := template.ParseFiles("count.gtpl")
w.Header().Set("Content-Type", "text/html")
t.Execute(w, sess.Get("countnum"))
}
count.gtpl的代碼如下所示:
Hi. Now count:{{.}}
然后我們在瀏覽器里面刷新可以看到如下內容:

圖6.4 瀏覽器端顯示count數
隨著刷新,數字將不斷增長,當數字顯示為6的時候,打開瀏覽器(以chrome為例)的cookie管理器,可以看到類似如下的信息:

圖6.5 獲取瀏覽器端保存的cookie
下面這個步驟最為關鍵: 打開另一個瀏覽器(這里我打開了firefox瀏覽器),復制chrome地址欄里的地址到新打開的瀏覽器的地址欄中。然后打開firefox的cookie模擬插件,新建一個cookie,把按上圖中cookie內容原樣在firefox中重建一份:

圖6.6 模擬cookie
回車后,你將看到如下內容:

圖6.7 劫持session成功
可以看到雖然換了瀏覽器,但是我們卻獲得了sessionID,然后模擬了cookie存儲的過程。這個例子是在同一臺計算機上做的,不過即使換用兩臺來做,其結果仍然一樣。此時如果交替點擊兩個瀏覽器里的鏈接你會發現它們其實操縱的是同一個計數器。不必驚訝,此處firefox盜用了chrome和goserver之間的維持會話的鑰匙,即gosessionid,這是一種類型的“會話劫持”。在goserver看來,它從http請求中得到了一個gosessionid,由于HTTP協議的無狀態性,它無法得知這個gosessionid是從chrome那里“劫持”來的,它依然會去查找對應的session,并執行相關計算。與此同時 chrome也無法得知自己保持的會話已經被“劫持”。
## session劫持防范
### cookieonly和token
通過上面session劫持的簡單演示可以了解到session一旦被其他人劫持,就非常危險,劫持者可以假裝成被劫持者進行很多非法操作。那么如何有效的防止session劫持呢?
其中一個解決方案就是sessionID的值只允許cookie設置,而不是通過URL重置方式設置,同時設置cookie的httponly為true,這個屬性是設置是否可通過客戶端腳本訪問這個設置的cookie,第一這個可以防止這個cookie被XSS讀取從而引起session劫持,第二cookie設置不會像URL重置方式那么容易獲取sessionID。
第二步就是在每個請求里面加上token,實現類似前面章節里面講的防止form重復遞交類似的功能,我們在每個請求里面加上一個隱藏的token,然后每次驗證這個token,從而保證用戶的請求都是唯一性。
h := md5.New()
salt:="astaxie%^7&8888"
io.WriteString(h,salt+time.Now().String())
token:=fmt.Sprintf("%x",h.Sum(nil))
if r.Form["token"]!=token{
//提示登錄
}
sess.Set("token",token)
### 間隔生成新的SID
還有一個解決方案就是,我們給session額外設置一個創建時間的值,一旦過了一定的時間,我們銷毀這個sessionID,重新生成新的session,這樣可以一定程度上防止session劫持的問題。
createtime := sess.Get("createtime")
if createtime == nil {
sess.Set("createtime", time.Now().Unix())
} else if (createtime.(int64) + 60) < (time.Now().Unix()) {
globalSessions.SessionDestroy(w, r)
sess = globalSessions.SessionStart(w, r)
}
session啟動后,我們設置了一個值,用于記錄生成sessionID的時間。通過判斷每次請求是否過期(這里設置了60秒)定期生成新的ID,這樣使得攻擊者獲取有效sessionID的機會大大降低。
上面兩個手段的組合可以在實踐中消除session劫持的風險,一方面, 由于sessionID頻繁改變,使攻擊者難有機會獲取有效的sessionID;另一方面,因為sessionID只能在cookie中傳遞,然后設置了httponly,所以基于URL攻擊的可能性為零,同時被XSS獲取sessionID也不可能。最后,由于我們還設置了MaxAge=0,這樣就相當于session cookie不會留在瀏覽器的歷史記錄里面。
## links
* [目錄](<preface.md>)
* 上一節: [session存儲](<06.3.md>)
* 下一節: [小結](<06.5.md>)
{% endraw %}
- 目錄
- Go環境配置
- Go安裝
- GOPATH 與工作空間
- Go 命令
- Go開發工具
- 小結
- Go語言基礎
- 你好,Go
- Go基礎
- 流程和函數
- struct
- 面向對象
- interface
- 并發
- 小結
- Web基礎
- web工作方式
- Go搭建一個簡單的web服務
- Go如何使得web工作
- Go的http包詳解
- 小結
- 表單
- 處理表單的輸入
- 驗證表單的輸入
- 預防跨站腳本
- 防止多次遞交表單
- 處理文件上傳
- 小結
- 訪問數據庫
- database/sql接口
- 使用MySQL數據庫
- 使用SQLite數據庫
- 使用PostgreSQL數據庫
- 使用beedb庫進行ORM開發
- NOSQL數據庫操作
- 小結
- session和數據存儲
- session和cookie
- Go如何使用session
- session存儲
- 預防session劫持
- 小結
- 文本文件處理
- XML處理
- JSON處理
- 正則處理
- 模板處理
- 文件操作
- 字符串處理
- 小結
- Web服務
- Socket編程
- WebSocket
- REST
- RPC
- 小結
- 安全與加密
- 預防CSRF攻擊
- 確保輸入過濾
- 避免XSS攻擊
- 避免SQL注入
- 存儲密碼
- 加密和解密數據
- 小結
- 國際化和本地化
- 設置默認地區
- 本地化資源
- 國際化站點
- 小結
- 錯誤處理,調試和測試
- 錯誤處理
- 使用GDB調試
- Go怎么寫測試用例
- 小結
- 部署與維護
- 應用日志
- 網站錯誤處理
- 應用部署
- 備份和恢復
- 小結
- 如何設計一個Web框架
- 項目規劃
- 自定義路由器設計
- controller設計
- 日志和配置設計
- 實現博客的增刪改
- 小結
- 擴展Web框架
- 靜態文件支持
- Session支持
- 表單支持
- 用戶認證
- 多語言支持
- pprof支持
- 小結
- 參考資料