説明
アーカイブのエントリ名を検証せずにファイルパスへ使うと、../ などを通じて展開先ディレクトリの外へ書き込まれるおそれがあります。絶対パスやリンクも、使用するAPIとOSに応じて制限する必要があります。
想定される影響
- Webコンテンツ、設定ファイル、実行ファイルを上書きされる可能性があります。
- 上書きしたファイルの利用方法とプロセス権限によっては、コード実行やサービス停止につながるおそれがあります。
対処方法
- 新しい非公開の展開用ディレクトリを作り、実際のファイルアクセスをその内部に限定してください。
- 単一のファイル名だけが必要なら、絶対パス、親への移動、入れ子のパス、シンボリックリンクなどの通常ファイル以外のエントリを拒否してください。
- 排他的な作成で上書きを防ぎ、開く処理、コピー、クローズのエラーを処理してください。
- ファイル数と実際の展開サイズも制限してください。パス検証だけでは圧縮爆弾を防げません。
例
変更前は dest とZIPエントリ file が存在する前提の抜粋です。変更後はサブディレクトリを作らず通常ファイルだけを展開し、同じアカウントの別のコードが一時ディレクトリを操作しない前提です。展開サイズの制限は省略しています。
変更前
go
_, _ = os.Create(filepath.Join(dest, file.Name))
変更後
go
package example
import (
"archive/zip"
"fmt"
"io"
"os"
"path/filepath"
)
func extractFlat(r *zip.Reader) (root string, err error) {
root, err = os.MkdirTemp("", "xeize-unzip-")
if err != nil {
return "", err
}
createdRoot := root
defer func() {
if err != nil {
_ = os.RemoveAll(createdRoot)
}
}()
for _, file := range r.File {
if !file.Mode().IsRegular() {
return "", fmt.Errorf("unsupported archive entry: %q", file.Name)
}
name := filepath.Clean(file.Name)
if name != file.Name || filepath.IsAbs(name) || name == "." || name == ".." ||
name != filepath.Base(name) {
return "", fmt.Errorf("invalid archive path: %q", file.Name)
}
in, err := file.Open()
if err != nil {
return "", err
}
out, err := os.OpenFile(filepath.Join(root, name),
os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0600)
if err != nil {
in.Close()
return "", err
}
_, copyErr := io.Copy(out, in)
closeOutErr := out.Close()
closeInErr := in.Close()
if copyErr != nil {
return "", copyErr
}
if closeOutErr != nil {
return "", closeOutErr
}
if closeInErr != nil {
return "", closeInErr
}
}
return root, nil
}
解説:
- 変更前: 結合したパスが展開先の内部に収まるか確認せずにファイルを作成します。
- 変更後: 正規化で変化する名前と複数の構成要素を持つ名前を拒否し、
O_EXCLで重複作成を防ぎます。エラー時には、別に保持したディレクトリのパスで削除を試みます。成功後の削除は呼び出し側で行う必要があります。