はじめに
go言語のwebframework beegoにはCSRF対策が実装されています。
CSRFとは
https://www.trendmicro.com/ja_jp/security-intelligence/research-reports/threat-solution/csrf.html
説明は不要かと思いますが、CSRFとは他サイトを経由した
不正なリクエストによってサイトを攻撃する手法です。
CSRF対策の一つとしてbeegoでも採用されているのがトークンを使った対策です。
サーバに保存されたトークンとリクエストにあるトークンが一致しているかを
判定することによって不正なリクエストでないことを証明します。
公式サイトを読むと・・・
POSTの場合、configにパラメータを記載すればcookieに_xsrfをセットするよ
PUTとDELETEの場合、cookieに含めないのでcontrollerにXSRFFormHTML()を記載すれば_xsrfフィールドを全てのformに追加するよ
とありますね。
ですがAjaxリクエスト(POST)を利用する場合、常にエラーを返してしまい正常に動作しません。
公式サイトを見ても情報がないため、Ajaxリクエストの場合のCSRF対策は少し工夫してやる必要があります。
・・・とそんなふうに考えていた時期が俺にもありました。
しなくても良かった
結論から言います!
AjaxリクエストでもCSRF対策は正常に動作します!
追加でヘッダーに_xsrfを含めてやればよいです。
{{.xsrfdata}}
let config = {
headers: {
"X-Xsrftoken": document.getElementsByName("_xsrf")[0].value
//もしくはcookieからBASE64デコードした値をセット
}
}
let data = {
}
axios.post(URL, data, config).then(...)
これだけでbeegoがよしなにしてくれます!
AjaxでCSRF対策をしたい方はここでページを閉じてもらってOKです。
筆者はひどく迷走しCSRF対策を独自実装しないといけないと思ってしまいました。
以下はその迷走の軌跡です。
迷走
↑の正解を見ると明白ですがヘッダーにトークンが含まれていないからですよね。
そこで筆者は勘違いをしてしまいました。
Ajaxリクエストだとヘッダーにトークンを含めてやる必要があるけど
ヘッダーのどこに含めるかはどこでもいいだろう。
AjaxリクエストだとbeegoのCSRF対策処理を通らないんだろうし、と。
なので
- ヘッダーの適当な値にトークンを含めて送信
- サーバに保存されたトークンとリクエストにあるトークンの一致確認
を勘違いして実装しちゃいました。
しなくても良かった実装箇所
- ヘッダーの適当な値にトークンを含めて送信
let config = {
headers: {
"X-CSRF-Token": document.getElementsByName("_xsrf")[0].value
//もしくはcookieからBASE64デコードした値をセット
}
}
let data = {
}
axios.post(URL, data, config).then(...)
- サーバに保存されたトークンとリクエストにあるトークンの一致確認
func (c *TestController) Prepare() {
enableXSRF, _ := beego.AppConfig.Bool("EnableXSRF")
if enableXSRF == true {
if c.IsAjax() && c.Ctx.Request.Method == "POST" {
//beegoの通常機能でajaxがエラーになるため一旦false
//falseにした設定はこのリクエスト内だけ有効なので戻さなくてOK
c.EnableXSRF = false
valid := validation.Validation{}
if c.Ctx.Request.Header["X-Csrf-Token"] != nil {
token := c.Ctx.Request.Header["X-Csrf-Token"][0]
if token != c.XSRFToken() {
//トークン不一致エラー
valid.SetError("CSRF", "トークン不一致です")
}
} else {
//トークンがセットされてないエラー
valid.SetError("CSRF", "トークンがセットされてないです")
}
if valid.HasErrors() {
data := make(map[string]string)
for _, err := range valid.Errors {
data[err.Key] = err.Message
}
mapData := map[string]interface{}{
"isValid": false,
"validationSummary": data,
}
c.Data["json"] = mapData
c.Ctx.Output.SetStatus(http.StatusOK)
c.ServeJSON()
return
}
}
}
}
こんな実装しなくても標準機能が対応していたんですねぇ・・・
うわー恥ずかしい!
惜しかった実装箇所
なぜAjaxリクエストだとエラーになると思い込んでしまったのか?
- ヘッダーの適当な値にトークンを含めて送信
ヘッダーにトークンを含めて送信という考え方は正しかったんですが、
ヘッダーのどの値に含めるかが間違ってました。
let config = {
headers: {
- "X-CSRF-Token": document.getElementsByName("_xsrf")[0].value
+ "X-Xsrftoken": document.getElementsByName("_xsrf")[0].value
//もしくはcookieからBASE64デコードした値をセット
}
}
"X-Xsrftoken"にトークンをセットするのが正解なんです。
そんなのどこにも書いてないじゃん!と思いましたが
正しい値はbeegoのソースにちゃんと書いてあったんです・・・
// CheckXSRFCookie checks if the XSRF token in this request is valid or not.
// The token can be provided in the request header in the form "X-Xsrftoken" or "X-CsrfToken"
// or in form field value named as "_xsrf".
func (ctx *Context) CheckXSRFCookie() bool {
token := ctx.Input.Query("_xsrf")
if token == "" {
token = ctx.Request.Header.Get("X-Xsrftoken")
}
if token == "" {
token = ctx.Request.Header.Get("X-Csrftoken")
}
if token == "" {
ctx.Abort(422, "422")
return false
}
if ctx._xsrfToken != token {
ctx.Abort(417, "417")
return false
}
return true
}
- inputの"_xsrf"
- ヘッダーの"X-Xsrftoken"
- ヘッダーの"X-Csrftoken"
の順番でトークンが確認されることがちゃんと書いてあります。
実装する前にソースをちゃんと見ろって話しですね。
あ、ただしAjaxだと1.が認識されないので2.3.の方法でやるのが正しいです。
おわりに
AjaxリクエストのCSRF対策についての参考情報
beegoのgithub issueにヘッダについてのやり取りがありました。
https://github.com/beego/beego/issues/2426
中国語で言及されているサイトがありました。
https://www.kancloud.cn/hello123/beego/126124
https://cloud.tencent.com/developer/article/1067750