はじめに

前回の記事の続きです。
前回の記事はこちら

フォームの複数回送信の防止

前回はXSS 対策について勉強しました。
今回は フォームの複数回送信の防止 についてです。

フォーム送信した後にもう一度更新すると二重送信されたり、 送信ボタンをダブルクリックすると二重送信されることを防ぐ技術ですね。

どのようにして防ぐのか書いてありますので見ていきましょう。

解決方法はフォームの中にユニークな値を持ったhiddenフィールドを追加することです。
フォームを検証する際、このユニークな値を持ったフォームがすでに送信されているかどうか検証します。
もしすでに送信されていれば、二回目の送信を拒絶します。
そうでなければフォームに対して処理ロジックを行います。
また、もしAjax形式で送信するフォームだった場合、フォームが送信された後javascriptによってフォームの送信ボタンを禁止します。

バリデーションで使用したcheckboxの例を改良しているようです。

1
2
3
4
5
6
7
<input type="checkbox" name="interest" value="football">サッカー
<input type="checkbox" name="interest" value="basketball">バスケットボール
<input type="checkbox" name="interest" value="tennis">テニス
ユーザ名:<input type="text" name="username">
パスワード:<input type="password" name="password">
<input type="hidden" name="token" value="{{.}}">
<input type="submit" value="ログイン">

これを login.gtml に組み込んでみましょう。
form の中身はこのような感じになりました。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
<form action="/login" method="post">
    <input type="checkbox" name="interest" value="football">サッカー
    <input type="checkbox" name="interest" value="basketball">バスケットボール
    <input type="checkbox" name="interest" value="tennis">テニス
    ユーザ名:<input type="text" name="username">
    パスワード:<input type="password" name="password">
    年齢:<input type="text" name="age">
    <input type="hidden" name="token" value="{{.}}">
    <input type="submit" value="ログイン">
</form>

ログインのメソッドは下記のようになりました。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
func login(w http.ResponseWriter, r *http.Request) {
	fmt.Println("method:", r.Method) //リクエストを取得するメソッド
	if r.Method == "GET" {
		crutime := time.Now().Unix()
		h := md5.New()
		io.WriteString(h, strconv.FormatInt(crutime, 10))
		token := fmt.Sprintf("%x", h.Sum(nil))

		t, _ := template.ParseFiles("html/login.gtpl")
		t.Execute(w, token)
	} else {
		//ログインデータがリクエストされ、ログインのロジック判断が実行されます。
		r.ParseForm()
		token := r.Form.Get("token")
		if token != "" {
            //tokenの合法性を検証します。
		} else {
			//tokenが存在しなければエラーを出します。
		}
		fmt.Println("username:", r.Form["username"])
		fmt.Println("password:", r.Form["password"])
		fmt.Println("username:", template.HTMLEscapeString(r.Form.Get("username"))) //サーバ側に出力されます。
		fmt.Println("password:", template.HTMLEscapeString(r.Form.Get("password")))
		template.HTMLEscape(w, []byte(r.Form.Get("username"))) //クライアントに出力されます。
		t, err := template.New("foo").Parse(`{{define "T"}}Hello, {{.}}!{{end}}`)
		err = t.ExecuteTemplate(w, "T", template.HTML("<script>alert('you have been pwned')</script>"))
		if err != nil {
			fmt.Println("error")
		}
		fmt.Println("age: ", r.Form["age"])
		fmt.Println("GET query string", r.URL)
		// バリデーションしてみる
		errors := validate(r.Form)
		fmt.Println(errors)
	}
}

早速実行してみましょう。
下記のように tokenmd5 でありますね。
web-server-01

F5で更新してみると、下記のように変わっています。
web-server-02
ということはわかります通り、送信したときにユニークな値が保持されるわけですね。
このときに前回の値と比較して一致していない場合はエラー!のようになるわけです。

//tokenの合法性を検証します。//tokenが存在しなければエラーを出します。 を実装してみましょう。

一部ですが下記のようにしてみました。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
//ログインデータがリクエストされ、ログインのロジック判断が実行されます。
		r.ParseForm()
		token := r.Form.Get("token")
		if token != "" {
			//tokenの合法性を検証します。
			var cookie = getCookie(w, r, "token")
			if cookie == nil {
				setCookie(w, "token", token)
				fmt.Println("クッキーをセットしたよ! : ", token)
			} else if cookie != nil && cookie.Value != token {
				fmt.Println("2回更新しているよ!")
				fmt.Println("送信された値", token)
				fmt.Println("保持している値", cookie.Value)
			}
		} else {
			fmt.Println("tokenがないです!")
		}

動画でちょっと見てみましょう。 本来はセッションで実装すべきですが、クッキーでやってみました。
クッキーをクリアする処理はないので、一回パスを通ると二度と踏むことはできなくなってしまっています。

クッキーのセット, 2回更新の部分, tokenを送信しなかった場合 で検証しています。
ブラウザでどういう操作を行ったかを想像してみると面白いかもしれません。 (すみません、ブラウザの画面取れていなかったです。)

おわりに

この章はこれで終わりです。
次回はファイルアップロード処理ですね!
頑張っていきます、おやすみなさい。