My External Storage

Dec 4, 2019 - 6 minute read - Comments - Go

OWASP/Go-SCPを読んでセキュアプログラミングとGoを学ぶ

この記事はGo Advent Calendar 2019の4日目の記事になる。
3日目は@ikawahaさんの「Goa v3 のテストをシュッとする]」だった。

本記事ではOpen Web Application Security Project(OWASP)が公開しているGo-SCPリポジトリを紹介する。
Webアプリケーションにはクロスサイトスクリプティング(XSS)クロスサイトリクエストフォージェリ(CSRF)など、様々な脆弱性が潜む可能性がある。 脆弱性対策の書籍としては、体系的に学ぶ 安全なWebアプリケーションの作り方(徳丸本)などが有名だろう。
Go-SCPリポジトリにはWebアプリケーションを実装する際に必要な脆弱性の知識と、Goを使った脆弱性対策の実装方法が含まれている。

TL;DR

なお、Go-SCPリポジトリはCreative Commons Attribution-ShareAlike 4.0 International license (CC BY-SA 4.0)のライセンスで公開されている。 そのため、本記事で引用しているサンプルコードなども同等のライセンスになる。 また、本記事で参照しているGo-SCPリポジトリのバージョンは2019/12/04時点最新のv2.5.6になる。

OWASP(Open Web Application Security Project)とは

OWASPという団体が存在するのはご存知だろうか。

OWASPは、Webをはじめとするソフトウェアのセキュリティ環境の現状、またセキュアなソフトウェア開発を促進する技術・プロセスに関する情報共有と普及啓発を目的としたプロフェッショナルの集まる、オープンソース・ソフトウェアコミュニティだ(OSWAP Japan TOPページより)。 OWASPと書いて、「オワスプ」と読む。 ソフトウェア全体の脆弱性やセキュリティに関する話題を取り扱っているため、Go以外のガイドも多数作成されている。

OWASP Go Secure Coding Practices Guide

OWASP Go Secure Coding Practices GuideはGoを使ってWeb開発を行なう際のセキュアコーディングガイドだ。

PDF、Mobi、ePub形式でも発行されている。PDF換算だと74ページになり、そこそこのボリュームがある。更新も活発で、約2年間で25回リリースされている。

内容自体はOWASP Secure Coding Practices Quick Reference Guideの内容をカバーしているらしく、その内容をGoで書き直したガイドとなる。 対象は他言語経験済レベルの開発者を想定しているらしいが、「Tour Of Goの次に読む学習素材としても良いだろう」と書いてある。

コンテンツの内容

コンテンツの内容はsrc/SUMMARY.mdに記載の通り以下の話題が網羅されている。

中身については実際に読んでいただくとして、どんな特徴があるかを簡単に紹介する。

Goで特有に発生する問題、Webアプリケーション一般で発生する問題両方を学習することができる

実際に読んでみると、「サンプルコードがGoなだけ」ではなかった。 Goの言語仕様や標準パッケージだからこそ起きる問題にも言及してあり、たしかにGoの文法を覚えた次に読む資料として良さそうだった。

例えば、疑似乱数生成の章では、Go特有のSeed問題に触れられている。 ご存知の通り、GoはSeedを設定せずにmath/randパッケージにある一連の乱数生成関数を使うと毎回同じ値が生成される。

// OWASP/Go-SCP/src/cryptographic-practices/pseudo-random-generators.md より
package main

import "fmt"
import "math/rand"

func main() {
    fmt.Println("Random Number: ", rand.Intn(1984))
}

// 実行結果は毎回同じになる
// $ for i in {1..5}; do go run rand.go; done
// Random Number:  1825
// Random Number:  1825
// Random Number:  1825
// Random Number:  1825
// Random Number:  1825

この章では「math/randパッケージではなくcrypto/randパッケージを使うべき理由」がサンプルコードと一緒にまとめられている。

// OWASP/Go-SCP/src/cryptographic-practices/pseudo-random-generators.md より
package main

import "fmt"
import "math/big"
import "crypto/rand"

func main() {
    rand, err := rand.Int(rand.Reader, big.NewInt(1984))
    if err != nil {
        panic(err)
    }

    fmt.Printf("Random Number: %d\n", rand)
}

// 毎回異なる結果になる
// $ for i in {1..5}; do go run rand-safe.go; done
// Random Number: 277
// Random Number: 1572
// Random Number: 1793
// Random Number: 1328
// Random Number: 1378

また、クロスサイトスクリプティング(XSS)の章は次のようなシンプルなWebサーバのコードから始まる。

package main

import "net/http"
import "io"

func handler (w http.ResponseWriter, r *http.Request) {
    io.WriteString(w, r.URL.Query().Get("param1"))
}

func main () {
    http.HandleFunc("/", handler)
    http.ListenAndServe(":8080", nil)
}

このサンプルコードを以下の順に改修していき、セキュアな実装を学ぶ構成になっている。

  1. ioパッケージのまま悪意のあるリクエストを受け取ってみる
  2. text/templateパッケージを使う実装にしてみる
  3. html/templateパッケージを使う実装にしてみる

XSS自体の危険性を知りながら、Goの標準パッケージを使ってどのようにセキュアなコードを書けるのかが学べる。

なお、クロスサイトリクエストフォージェリ(CSRF)の説明ではgorilla/csrfパッケージが使われており、 標準パッケージだけでは対策の実装が難しい問題についてはOSSを利用したベターな解決策も提示されている。

// OWASP/Go-SCP/src/general-coding-practices/cross-site-request-forgery.md より
// 日本語コメントは私が追加したものである。
package main

import (
    "net/http"

    "github.com/gorilla/csrf"
    "github.com/gorilla/mux"
)

func main() {
    r := mux.NewRouter()
    r.HandleFunc("/signup", ShowSignupForm)
    // All POST requests without a valid token will return HTTP 403 Forbidden.
    r.HandleFunc("/signup/post", SubmitSignupForm)

    // 生成した暗号キーをセットしておくと、gorilla/csrfがPOSTメソッドなどの際にいい感じに検証してくれる。
    // Add the middleware to your router by wrapping it.
    http.ListenAndServe(":8000",
        csrf.Protect([]byte("32-byte-long-auth-key"))(r))
    // PS: Don't forget to pass csrf.Secure(false) if you're developing locally
    // over plain HTTP (just don't leave it on in production).
}

func ShowSignupForm(w http.ResponseWriter, r *http.Request) {
    // Formの中にトークンを埋め込んでおく。
    // signup_form.tmpl just needs a {{ .csrfField }} template tag for
    // csrf.TemplateField to inject the CSRF token into. Easy!
    t.ExecuteTemplate(w, "signup_form.tmpl", map[string]interface{}{
        csrf.TemplateTag: csrf.TemplateField(r),
    })
    // We could also retrieve the token directly from csrf.Token(r) and
    // set it in the request header - w.Header.Set("X-CSRF-Token", token)
    // This is useful if you're sending JSON to clients or a front-end JavaScript
    // framework.
}

func SubmitSignupForm(w http.ResponseWriter, r *http.Request) {
    // We can trust that requests making it this far have satisfied
    // our CSRF protection requirements.
}

これら以外の章も英語が得意ならばそれぞれ10分もかからず読める量なので、スキマ時間に少しずつ読むこともできるだろう (私は英語が苦手なので、得意ならばもっと早く読めるかもしれない)。

終わりに

簡単ではあるが、セキュアプログラミングの方法が記載されているGo-SCPの紹介をした。
ウェブアプリケーションフレームワーク(WAF)を使っていれば、気づかずに避けられる問題も多いかもしれない。 が、Goの場合はWAFに頼らずシンプルな実装をしているところも多いだろうし、「XXXという処理にはこういう脆弱性が潜む可能性がある」と知っておくのも大事だろう。
Go-SCPリポジトリは英語だが文量もそこまで多くないので、Go・セキュアプログラミング・英語の勉強にぜひ一度目を通してみると良いだろう。

Go Advent Calendar 2019 5日目はちまきんさんが担当される。お楽しみに! (今年はGoだけで毎日7記事読めるのですごい…)

参考

関連記事