ファイルアップロード・ディレクトリトラバーサル対策 ファイル形式・サイズ検証、保存場所の分離、パス名の制限。
ファイルアップロード対策の要点
ファイルアップロード機能は、Webアプリケーションにおける最も危険な攻撃経路の一つです。攻撃者が悪意のあるコード(例:Webシェル)をサーバーに送り込み、実行させる(Remote Code Execution, RCE)リスクを回避することが最優先です。
1. ファイル形式・拡張子の厳格な検証
クライアントからの申告は一切信用せず、サーバー側で厳格にチェックします。
許可リスト(ホワイトリスト)方式の採用:
許可する拡張子(例: .jpg, .png, .pdf, .zip)を明示的に定義し、それ以外のファイルをすべて拒否します。
.php, .jsp, .asp, .exe, .sh などの実行可能な拡張子を個別にブラックリストに入れるのではなく、ホワイトリスト外として拒否することで、未知の拡張子による攻撃を防ぎます。
マジックナンバー検証:
HTTPヘッダーの Content-Type は簡単に偽装されます。これに依存せず、ファイルの先頭にある固有のバイト列であるマジックナンバーをサーバー側で確認し、ファイルタイプを確実に特定します。
例: ファイルが .jpg 拡張子であっても、内部構造がPHPスクリプトではないことを確認します。
ファイル名の無害化:
ユーザーが指定したファイル名をそのまま使用せず、サーバー側で生成したランダムな一意なID(UUID) に変更します。これにより、ファイル名に含まれる特殊文字(例: ;, |)によるコマンド実行や、パス推測を防ぎます。
2. 保存場所の分離(隔離)
アップロードされたファイルが、仮に悪意のあるコードを含んでいたとしても、実行されない環境を構築します。
Webルート外への配置:
アップロードファイルを、Webサーバーが直接アクセスして実行できるWebルートディレクトリ(例: /var/www/html)の外に保存します。
公開が必要な場合は、専用のURLを通じてアプリケーション経由でのみアクセスできるようにします。
専用ストレージの利用:
AWS S3やGoogle Cloud Storageなどのオブジェクトストレージに保存することで、Webサーバーのファイルシステムから完全に分離します。
実行権限の削除:
ファイルが保存されたディレクトリに対して、Webサーバープロセスから実行(Execute)権限を削除し、誤ってコードが実行されるのを防ぎます。
3. ファイルサイズの検証
アップロードできるファイルサイズに厳格な上限を設定し、巨大なファイルによるディスク容量の圧迫や、処理遅延によるサービス妨害(DoS攻撃) を防ぎます。
package main
import (
"fmt"
"mime/multipart"
"path/filepath"
"strings"
"crypto/rand"
"encoding/hex"
)
// ★ 許可リスト(ホワイトリスト): 許可する拡張子のみ定義 ★
var allowedExtensions = map[string]bool{
".jpg": true,
".png": true,
".pdf": true,
}
// アップロード処理の関数 (簡略版)
func processUpload(fileHeader *multipart.FileHeader) (string, error) {
originalFilename := fileHeader.Filename
// 1. 拡張子の検証 (ホワイトリストチェック)
ext := strings.ToLower(filepath.Ext(originalFilename))
if !allowedExtensions[ext] {
// 許可されていない拡張子は拒否
return "", fmt.Errorf("許可されていないファイル形式: %s", ext)
}
// 2. ファイル名の無害化 (UUID/ランダム名生成)
b := make([]byte, 16)
rand.Read(b)
newFilename := hex.EncodeToString(b) + ext
// 3. (本来はここでファイルを開き、マジックナンバー検証後に安全な場所に保存する)
fmt.Printf("ファイル名: %s を %s として保存します\n", originalFilename, newFilename)
return newFilename, nil
}
ホワイトリスト: 危険な拡張子を一つずつ除外するのではなく、安全な拡張子のみを許可しています。
無害化: newFilename := hex.EncodeToString(b) + ext で、ユーザー入力のファイル名を完全に無視し、サーバー側でランダムな名前を生成するのは、ファイルアップロード時に、ユーザー入力のファイル名を完全に無視してサーバー側でランダムな名前(UUIDやハッシュ値)を生成するのは、主に以下のセキュリティリスクを排除するため。
- ディレクトリトラバーサル攻撃の防止 (Path Traversal)
- コマンド実行(RCE)の防止
- ファイル名の重複や文字コード問題の回避
ディレクトリトラバーサル対策(パス名の制限)
ファイルダウンロードやファイル操作の機能で、ユーザー入力のパスを扱う際に、サーバー上の機密情報にアクセスを防ぎます。
- パス名の制限と正規化
ディレクトリトラバーサル攻撃は、ユーザーがパスに .. (親ディレクトリ)を含めることで、アプリケーションが意図しないサーバー上のファイル(例: /etc/passwd、設定ファイルなど)にアクセスしようとする攻撃。
・入力の制限
ユーザー入力に含まれるすべての .. やシンボリックリンクの参照を厳しくチェックし、拒否します。
・パスの正規化(絶対必須)
ユーザー入力を受け取ったら、OSやフレームワークの機能を使ってパスを正規化(Clean処理) します。
例: /var/www/uploads/../conf/db.cnf は正規化され /var/www/conf/db.cnf となります。
・ベースディレクトリチェック
正規化された最終的なパスが、アプリケーションが許可したベースディレクトリ(例: /data/uploads/)の配下にあるかを厳密に確認します。配下にない場合は即座にアクセスを拒否します。
処理フロー: if !strings.HasPrefix(正規化後のパス, ベースパス) { 拒否 }
このセクションは、防御ロジックをテストするための実行例を提供しています。
func main() {
// 成功例: 許可されたディレクトリ内のファイル
safeFile, err := safePathCheck("report/Q4_data.pdf")
fmt.Printf("安全なパス: %s, エラー: %v\n", safeFile, err)
// 失敗例: ディレクトリトラバーサル攻撃の試行
attackFile, err := safePathCheck("../../../etc/passwd")
fmt.Printf("攻撃パス: %s, エラー: %v\n", attackFile, err)
// 出力: エラー: アクセスは許可されたディレクトリ外に出ています: /etc/passwd
}
テストケースの実行: 許可されたパスと、攻撃的なパス(../../../etc/passwd)の2つのシナリオで、メインの防御関数 safePathCheck を呼び出し、その結果(エラーの有無)を出力します。
次の関数は、ユーザー入力のパスを検証し、許可されたベースディレクトリの外へのアクセスをブロックするセキュリティゲートです。
// ★ ファイルが格納されているベースディレクトリ ★
const uploadDir = "/data/safe_uploads/"
func safePathCheck(userPath string) (string, error) {
// 1. ベースパスとユーザー入力を結合
combinedPath := filepath.Join(uploadDir, userPath)
ステップ 1: 結合 (filepath.Join)
処理: サーバーがファイルを保存している安全なベースディレクトリ (uploadDir) と、ユーザーからの入力 (userPath) を単純に結合し、一つのパス (combinedPath) を作ります。
例(攻撃時): /data/safe_uploads/ と ../../../etc/passwd が結合されます。
// 2. パスの正規化 (トラバーサル攻撃の要素 '..' を解決)
// /data/safe_uploads/../conf/db.cnf -> /data/conf/db.cnf に解決される
finalPath := filepath.Clean(combinedPath)
ステップ 2: 正規化 (filepath.Clean)
処理: filepath.Clean を使用し、結合されたパスに含まれる ..(親ディレクトリ)や冗長なスラッシュを解決し、OSが実際にアクセスしようとする最短の論理パス(finalPath)に変換します。
例(攻撃時): /data/safe_uploads/../../../etc/passwd は、/etc/passwd に解決されます。
// 3. 正規化後のパスが許可されたベースディレクトリ外に出ていないか最終チェック
// 攻撃者が '..' を使って上の階層に行こうとしていないか確認する
if !strings.HasPrefix(finalPath, uploadDir) {
// 正規化の結果、uploadDirの外に出ていたら拒否
return "", fmt.Errorf("アクセスは許可されたディレクトリ外に出ています: %s", finalPath)
}
ステップ 3: 最終チェック(防御の核)
処理: 正規化されたパス (finalPath) が、あらかじめ設定した安全なベースディレクトリ (uploadDir) で始まっているか(strings.HasPrefix)を確認します。
目的: パスがベースディレクトリの外に出てしまっている場合(例: /etc/passwd は /data/safe_uploads/ で始まらない)、アクセスを拒否します。これが、ディレクトリトラバーサル攻撃を防ぐ最も確実な防御手法です。
// 4. ベースディレクトリと最終パスが同一でないかチェック (単に /data/safe_uploads/ へのアクセスを防ぐ)
if finalPath == uploadDir {
return "", fmt.Errorf("ファイル名が指定されていません")
}
// 検証を通過した安全なパスを返す
return finalPath, nil
}
ステップ 4: ディレクトリ名チェック
処理: ユーザーがファイル名を指定せず、ベースディレクトリ自体にアクセスしようとした場合(例: userPathが空)、そのアクセスを拒否します。
目的: ファイルダウンロード機能の文脈で、ディレクトリリストの表示(Directory Listing)などの予期せぬアクセスを防ぎます。
リターン: すべてのチェックを通過した場合にのみ、検証済みの安全なパスを返します。