io.Copy を上限付きで実行する
「もうlintに怒られない!Goでより安全にzipを扱うために」を読んで「なるほど!」と思ったが,(おそらくコードをかなり端折ってるんだと思うが)サンプル・コードがツッコミどころ満載なので,うちのブログでも覚え書として記しておく。 自作ツールでも圧縮データの解凍処理を使ってるしね。
gosec について
ところで,上述のリンク先で使われている gosec は統合 linter である golangci-lint から呼び出すことができるが,既定では無効になっている。
有効にするには --enable
オプションで gosec を指定すればよい。
$ golangci-lint run --enable gosec ./...
gosec が既定で有効になっていないのは,特に unsafe
標準パッケージを使っている場合の誤検知が多すぎるかららしい。
私の環境で試してみたが処理速度的には問題ないようなので,一度は試してみてもいいかもしれない。
最初のサンプル・コード
まずは起点となるサンプル・コードを挙げておこう。 こんな感じでどうだろう。
package main
import (
"bytes"
"compress/zlib"
"fmt"
"io"
"os"
"strings"
)
func compress(dst io.Writer, src io.Reader) error {
zw := zlib.NewWriter(dst)
defer zw.Close()
if _, err := io.Copy(zw, src); err != nil {
return err
}
return nil
}
func extract(dst io.Writer, src io.Reader) error {
r, err := zlib.NewReader(src)
if err != nil {
return err
}
if _, err := io.Copy(dst, r); err != nil {
return err
}
return nil
}
func main() {
content := "Hello world\n" //raw data
zbuf := &bytes.Buffer{}
//compress raw data
if err := compress(zbuf, strings.NewReader(content)); err != nil {
fmt.Fprintln(os.Stderr, err)
return
}
//display compressed data
b := zbuf.Bytes()
fmt.Printf("%d bytes: %v\n", len(b), b)
//extract from compressed data
buf := &bytes.Buffer{}
err := extract(buf, bytes.NewReader(b))
if err != nil {
fmt.Fprintln(os.Stderr, err)
return
}
//display extracted data
if _, err := io.Copy(os.Stdout, buf); err != nil {
fmt.Fprintln(os.Stderr, err)
return
}
}
最初に紹介した記事で使っている archive/zip
パッケージはファイルシステムを含んでいるので,今回はもっとシンプルに compress/zlib
パッケージ(RFC 1950 準拠)を使ってみた。
これならデータの圧縮と解凍のみ注視できるだろう。
このコードを実行すると
$ go run sample.go
24 bytes: [120 156 242 72 205 201 201 87 40 207 47 202 73 225 2 4 0 0 255 255 28 242 4 71]
Hello world
と一応は問題なく動く。 これで準備OK。
Decompression Bomb Vulnerability
では,このコードに対して lint をかけてみる。
$ golangci-lint run --enable gosec ./...
sample1.go:27:15: G110: Potential DoS vulnerability via decompression bomb (gosec)
if _, err := io.Copy(dst, r); err != nil {
^
おー,出た出た。
“Decompression bomb” というのは
A zip bomb, also known as a zip of death or decompression bomb, is a malicious archive file designed to crash or render useless the program or system reading it. It is often employed to disable antivirus software, in order to create an opening for more traditional viruses.
ということで,主にウイルス対策ツールをターゲットにした攻撃手段らしい。
まぁ 42.zip
みたいなファイルを解凍したら普通にシステムが死ぬと思うけど(笑)
解凍処理の改修
“Decompression bomb” を緩和する方法はいくつかあるようだが,いちばん安直なのは io
.Copy()
関数に上限を設けることだろう。
そのための関数として io
.CopyN()
が用意されている。
func io.CopyN(dst io.Writer, src io.Reader, n int64) (written int64, err error)
機能としては
.CopyN
copiesn
bytes (or until an error) fromsrc
todst
. It returns the number of bytes copied and the earliest error encountered while copying. On return, written== n
if and only iferr == nil
ということで
と見なすことができそうだ。
そこで最初のサンプルコードの extract()
関数を以下のように書き直す。
const maxSize = 1024 * 1024 * 1024 //1GB
var ErrTooLarge = errors.New("too laege decompressed data")
func extract(dst io.Writer, src io.Reader) error {
r, err := zlib.NewReader(src)
if err != nil {
return err
}
if _, err := io.CopyN(dst, r, maxSize); err != nil {
if errors.Is(err, io.EOF) {
return nil
}
return err
}
return ErrTooLarge
}
とりあえずコピーの上限は1GBとした(深い意味はない)。 これで少なくとも gosec に怒られることはなくなる。
これ仕込むのかぁ。 頑張ろう。
ブックマーク
参考図書
- プログラミング言語Go (ADDISON-WESLEY PROFESSIONAL COMPUTING SERIES)
- Alan A.A. Donovan (著), Brian W. Kernighan (著), 柴田 芳樹 (翻訳)
- 丸善出版 2016-06-20
- 単行本(ソフトカバー)
- 4621300253 (ASIN), 9784621300251 (EAN), 4621300253 (ISBN)
- 評価
著者のひとりは(あの「バイブル」とも呼ばれる)通称 “K&R” の K のほうである。この本は Go 言語の教科書と言ってもいいだろう。と思ったら絶版状態らしい(2025-01 現在)。復刊を望む!