beego AjaxでCSRF対策を独自実装する　と思ったらしなくても良かった件

# はじめに
go言語のwebframework beegoにはCSRF対策が実装されています。

https://beego.me/docs/mvc/controller/xsrf.md

<details><summary>CSRFとは</summary><div>

https://www.trendmicro.com/ja_jp/security-intelligence/research-reports/threat-solution/csrf.html

説明は不要かと思いますが、CSRFとは他サイトを経由した
不正なリクエストによってサイトを攻撃する手法です。

CSRF対策の一つとしてbeegoでも採用されているのがトークンを使った対策です。
サーバに保存されたトークンとリクエストにあるトークンが一致しているかを
判定することによって不正なリクエストでないことを証明します。

</div></details>

公式サイトを読むと・・・
POSTの場合、configにパラメータを記載すればcookieに_xsrfをセットするよ
PUTとDELETEの場合、cookieに含めないのでcontrollerにXSRFFormHTML()を記載すれば_xsrfフィールドを全てのformに追加するよ
とありますね。

ですがAjaxリクエスト(POST)を利用する場合、常にエラーを返してしまい正常に動作しません。

公式サイトを見ても情報がないため、Ajaxリクエストの場合のCSRF対策は少し工夫してやる必要があります。

**・・・とそんなふうに考えていた時期が俺にもありました。**

# しなくても良かった

結論から言います！
AjaxリクエストでもCSRF対策は正常に動作します！
追加でヘッダーに_xsrfを含めてやればよいです。

```html:test.tpl
 {{.xsrfdata}}
```

```js:test.js
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対策処理を通らないんだろうし、と。**

なので

- ヘッダーの適当な値にトークンを含めて送信
- サーバに保存されたトークンとリクエストにあるトークンの一致確認

を勘違いして実装しちゃいました。


# しなくても良かった実装箇所

- ヘッダーの適当な値にトークンを含めて送信

```js:test.js
let config = {
  headers: {
    "X-CSRF-Token": document.getElementsByName("_xsrf")[0].value
    //もしくはcookieからBASE64デコードした値をセット
  }
}

let data = {
}

axios.post(URL, data, config).then(...)
```

- サーバに保存されたトークンとリクエストにあるトークンの一致確認

```go:test.go
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リクエストだとエラーになると思い込んでしまったのか？

- ヘッダーの適当な値にトークンを含めて送信

ヘッダーにトークンを含めて送信という考え方は正しかったんですが、
ヘッダーのどの値に含めるかが間違ってました。

```diff:test.js
let config = {
  headers: {
-    "X-CSRF-Token": document.getElementsByName("_xsrf")[0].value
+    "X-Xsrftoken": document.getElementsByName("_xsrf")[0].value
    //もしくはcookieからBASE64デコードした値をセット
  }
}
```

"X-Xsrftoken"にトークンをセットするのが正解なんです。
そんなのどこにも書いてないじゃん！と思いましたが
正しい値はbeegoのソースにちゃんと書いてあったんです・・・

```go:context.go
// 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
}
```

1. inputの"_xsrf"
1. ヘッダーの"X-Xsrftoken"
1. ヘッダーの"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