あぼぼーぼ・ぼーぼぼ

のんびり生きたい

Go 1.19.3 and 1.18.8 security fix の内容を読んだ

今更ながら Go 1.93 及び 1.8.8 のマイナーリリース、security fix の内容について。

リリース内容はこちら

groups.google.com

どういう問題があったか

github.com

Windowsにおいて、環境変数をサブプロセスに渡す際にヌル文字(NUL)が含まれていると別の環境変数を設定できてしまう。例えば “A=B\x00C=D” という文字列を解釈すると環境変数として A=B, C=D と設定できるということ。

修正内容

github.com

diff の上から見ていくと、まず env_test.go は今回の脆弱性がテストケースとして分かりやすく表現されていて良い。

次に os/exec/exec.go が修正されているが、コミットメッセージを見ると、一応冗長なチェックを追加しといた(「Add a redundant check to os/exec as extra insurance.」)と書いてあるため、一旦スルー。

本丸は syscall/exec_windows.go の修正で、createEnvBlock 関数に変更が加わっている。

中身を見る前に、syscall パッケージについておさらいすると、syscall パッケージは OS のシステムコールを担当するパッケージで、syscall パッケージ内にある各 OS 専用のファイルでシステムコールの実装が管理されている。例えば、syscall パッケージの StartProcess 関数の GoDoc には「StartProcess wraps ForkExec for package os.」と書いてある。これは Windows なら exec_windows.go の処理が呼ばれ、UNIX なら exec_unix.go の処理が呼ばれるように、実行環境に合わせた形でビルドされる。ちなみに、Go 1.4 からは syscall パッケージは基本フリーズされ、OS 固有の実装は golang.org/x/sys で管理されている(参考:https://go.googlesource.com/proposal/+/refs/heads/master/design/freeze-syscall.md)。

createdEnvBlock 関数に戻ると、この関数はざっくり言うと環境変数を受け取って、その後の処理に必要な形に変換している。今回の修正では、error を返すようになった。どういうときに error を返すかというと、if bytealg.IndexByteString(s, 0) != -1 { のときに error を返すようになった。

この bytealg パッケージの IndexByteString 関数が何をやってるか調べる。まず bytealg パッケージは internal なパッケージだった。IndexByteString 関数は、pkg.go.dev を見ても特にコメントが無かったため、実装を見にいく。

pkg.go.dev

bytealg.IndexByteString 関数の実装は多分こちら。

go.dev

実装を見ると、文字列とバイトを受け取って、文字列を一文字ずつ for で回してバイトと同じ要素が見つかったらそのインデックスを返し、見つからなかったら -1 を返すということをしている。

(internal パッケージなのでコピペして) playground で動かしてみると、実際にヌル文字がインデックス3つ目にあることを検知できた。

package main

import "fmt"

func main() {
    fmt.Println(IndexByteString("A=B\x00C=D", 0)) // 3
}

func IndexByteString(s string, c byte) int {
    for i := 0; i < len(s); i++ {
        if s[i] == c {
            return i
        }
    }
    return -1
}

よって、Windows において環境変数の文字列を解釈する処理中に、ヌル文字を見つけて error を返すような変更が加わったということが確認できた。

Go 1.19.2 and 1.18.7 security fix の内容を読んだ

https://groups.google.com/g/golang-announce/c/xtuG5faxtaU

3つの security fix が含まれている。

  • archive/tar : ヘッダー読み込み時のメモリ消費量に制限をかけた
  • net/http/httputil : ReverseProxy でパース不可能なクエリパラメータを転送しないようにした
  • regexp/syntax : 正規表現をパースする際のメモリ使用量の制限をかけた

archive/tar : ヘッダー読み込み時のメモリ消費量に制限をかけた

Issue https://github.com/golang/go/issues/54853

Reader.Read はファイルヘッダの最大サイズに制限を設けていなかった。悪意をもって作られたアーカイブは Read に無制限のメモリを割り当て、リソースの枯渇やパニックを引き起こす可能性があった。

Reader.Read はヘッダーブロックの最大サイズを 1MiB に制限するようになった。1MiB は libarchive の仕様に合わせている

コードを見ると、1MiB を maxSpecialFileSize = 1 << 20 と定義している。 << は Arithmetic operators (算術演算子) の中の left shift と呼ばれる。左シフトは、左辺の値を右辺の値だけ左へシフトする(例 0001 => 0010)。MiB はメビ・バイトという単位。コンピューターの容量や記憶装置の大きさを表す単位のひとつとして利用される。。MB(メガバイト)は10の6乗バイト(1MB = 100万B)となるが、1MiB は2の20乗バイト(1MiB = 104万8576B)となる。

net/http/httputil : ReverseProxy でパース不可能なクエリパラメータを転送しないようにした

issue https://github.com/golang/go/issues/54663

ReverseProxy でリクエストを転送するときに、パースできないクエリパラメータも含まれていた。クエリパラメータの密輸が可能ということが脆弱性なのかセキュリティ強化策なのかは微妙なところだが、CVE を割り当て脆弱性として扱うことにした。

下位互換性を保ちつつ修正する方法として、条件付きでパースできないクエリパラメータを削除するようにした。

regexp/syntax : 正規表現をパースする際のメモリ使用量の制限をかけた

issue https://github.com/golang/go/issues/55949

解析される各正規表現は 256MB のメモリフットプリントに制限されるようになり、ネストが深い正規表現などそれ以上の容量を必要とする正規表現が拒否されるようになった。

この修正ではサイズが大きすぎる正規表現に対して syntax.ErrInternalError を返すようにしたが、Go 1.20 でこのエラーを変えるプロポーザルが出されていた。すでに Go 1.19 ではこのケースに利用できそうな syntax.ErrNestingEpth が追加されている。

http.NewRequestがerrorを返すパターン

以下のPRをマージする過程で調べる必要があったのでメモしておく。

github.com

結論http.NewRequestがerrorを返すパターンは、HTTPメソッドが不正な場合と、URLのパースに失敗した場合の2パターンある。

http.NewRequestはgodocにもあるとおりhttp.NewRequestWithContextcontext.Backgroundを指定して呼び出すラッパーになっている。

// NewRequest wraps NewRequestWithContext using context.Background.
func NewRequest(method, url string, body io.Reader) (*Request, error) {
    return NewRequestWithContext(context.Background(), method, url, body)
}

https://github.com/golang/go/blob/master/src/net/http/request.go#L835-L838

NewRequestWithContextがerrorを返すパターンは、validMethodでHTTPメソッドが不正な値だった場合と、contextがnilだった場合と、urlpkg.ParseでURLのパースに失敗した場合の3パターンある。このうち、contextがnilであるパターンはhttp.NewRequestでは起こらないので、残りの2パターンとなる。

func NewRequestWithContext(ctx context.Context, method, url string, body io.Reader) (*Request, error) {
    if method == "" {
        // We document that "" means "GET" for Request.Method, and people have
        // relied on that from NewRequest, so keep that working.
        // We still enforce validMethod for non-empty methods.
        method = "GET"
    }
    if !validMethod(method) {
        return nil, fmt.Errorf("net/http: invalid method %q", method)
    }
    if ctx == nil {
        return nil, errors.New("net/http: nil Context")
    }
    u, err := urlpkg.Parse(url)
    if err != nil {
        return nil, err
    }
// 省略

https://github.com/golang/go/blob/f64f12f0b32eba7d49c259480e0fa0c79eb47600/src/net/http/request.go#L862-L878

今回google/go-githubに出したPRで追加したメソッドは、内部でhttp.NewRequestにPOSTを指定しているためHTTPメソッドが不正にはならないし、http.NewRequest以前の処理でベースとなるGitHub APIのURLと結合させる処理の中でURLパースをしているため、errorを返すケースは存在しないと思うが、テストカバレッジ上はカバーされていないためどうしようか?となった。起こり得なさそうとはいえ、プロダクションコードでブランク識別子でerrorを握りつぶすのは悩ましいし、errcheckのような静的解析ツールを使っている場合警告が出る。今回は、テストケースとしてはカバーしないことになったが、設計に改善の余地があるのだと思う。

『詳解Go言語Webアプリケーション開発』を読んだ

本の約半分がハンズオン形式になっていて、実際にWeb APIを開発しながら必要となる知識をインストールできる本でした。ハンズオンで作るWeb APIはBeyond the Twelve-Factor Appに準拠した形で、お題もMySQLを使ったデータ永続化、RedisとJWTを使った認証・認可といった機能を持つTODOアプリのREST APIなので、昨今のWeb開発において実用的なお題で良かったです。

ハンズオンじゃない各セクションは簡潔に短くまとまっていて、「なぜ?」に答える詳細な説明はGoの公式サイトなど外部のリンクが紹介されているので、わりとサクサク読めます。

ハンズオンは、チーム開発するうえでも役立つことが書いてあるなと思いました。例えば https://github.com/cosmtrek/air を使ってホットリロードできる構成にしたり、Makefileを用意したり、GitHub Actionsでテストや静的解析をCIしたり。ホットリロードについてはテストコードによるフィードバックサイクルとの比較から実務においては必須というわけでもない、みたいな解説がされていて結構信頼できました。ハンズオン中心の構成にするにあたっての落としどころだったのかなと想像します。

ただハンズオン部分は、結構解説が飛んでいるところも散見されました。手を動かしながら読むので気付きやすいのかもしれません。例えばディレクトリ構造とファイル名が明示されていない場合「今どのパッケージ(ディクレトリ)のどのファイル触っているんだ...?」となったり、新しいパッケージのgo getが無かったり、あとは単純に誤植というかコードがコンパイルできなかったりといった感じです。この辺りは、著者がGitHubでサンプルコード(https://github.com/budougumi0617/go_todo_app)を公開しているのでそれと照らし合わせながら読むのがいいです。正誤表もあるみたいです。

パフォーマンスチューニングや可観測性のためのロギングといった、運用していくうえで必要性が高まる領域についてはカバーしていないので、もし本書から入門した場合は『実用Go言語』や、最近発売された『Go言語による分散サービス』(まだ読んでない)などを次に読んでいくと良さそうです。

ハンズオン楽しかったです!

詳解Go言語Webアプリケーション開発 | 清水陽一郎 | 工学 | Kindleストア | Amazon

『達人が教えるWebパフォーマンスチューニング』を読んだ

明日ISUCON予選なので読みました(白目

https://github.com/catatsuy/private-isuを題材に実際にチューニングをしながら理解できる構成になっています。実際のISUCONではベンチマーカーはクリックひとつで実行するだけだと思いますが、本書ではベンチマーカーのApache Benchを使う方の説明があったり、ベンチマーカーの実装についての付録があったりします。

サンプルコードが Go で書かれているのでぼくにとっては読みやすかったです。

ISUCON、ボトルネックのみ解決するという鉄則を守りたい...けど何もできないで終わるよりは出来ることを何かして終わりたいですね。

https://www.amazon.co.jp/dp/B0B1Z9ZMY6

『UNIXという考え方』を読んだ

名著なのは知ってましたが読めておらず、ようやく読めました。

スモール・イズ・ビューティフル、できるだけ早く試作をつくる、梃子を有効に活用する、一つのことをうまくやる、あたりの考え方がとくに刺さりました。そしてUNIXコマンドはすごく考えられてるなぁと。

自分で何かツールを作ったり、課題解決のためのプログラムを書いたりする際に今後めちゃくちゃ意識しそうです。

www.amazon.co.jp

NotionのページアイコンのURLをコピーするChrome拡張をつくった

chrome.google.com

自前でアップロードした画像を色々なページのアイコンに設定するのをラクにするChrome拡張です。

Notionではアップロードした画像をページアイコンに設定できます。その画像を他のページのアイコンにも設定したければ、ページ自体を複製するしかありません。なぜかというとNotionにアップロードした画像は、はてなブログの画像とか、Slackの絵文字のように簡単に取り出せないからです。

ページ自体を複製するとプロパティや本文も全て複製されるのが厄介です。あとすでにあるページのアイコンを変えるというケースには使えません。

このChrome拡張は、アップロードした画像が保存されているS3のURLをクリップボードにコピーします。ページアイコンにはURLを設定できるので、他のページのアイコンに簡単に設定できるようになります。

コードはGitHubに置いてあります。

github.com

Chrome拡張の初歩的な実装という感じですが強いて言えばTypeScript 100%になってて気持ちいいですね ( ˙σー˙ )