ファイルアップロードの検証不備とは?起きるパターンと対策法

ファイルアップロードの検証不備とは?起きるパターンと対策法

#セキュリティ#Web開発#脆弱性対策

ファイルアップロードの検証不備は、受け取ったファイルの種類や内容、保存方法を適切に制限せず、不正なファイルを保存、配信、処理させる脆弱性です。 対策の勘所は、拡張子だけに頼らず実体まで検証し、保存先で実行させない設計にできているかどうかです。

ファイルアップロードの検証不備とは

ファイルアップロードの検証不備:アップロードされたファイルの種類や内容を十分に確認せず、想定外のファイルを受け入れ、保存後の公開や解析によって攻撃を成立させる脆弱性です。 拡張子やクライアント申告の情報だけを信用することに加え、保存先や公開方法を安全に設計していないことが根本原因になります。

攻撃者は画像に見せかけたスクリプトや、解析処理を狙った細工済みファイルなどをアップロードします。 Webシェルの実行によるサーバの乗っ取りだけでなく、HTMLやSVGの配信によるStored XSS、解析ライブラリの脆弱性の悪用、既存ファイルの上書き、保存領域の圧迫に至ります。 アップロードは利用者が任意のデータをサーバへ送り込める数少ない経路であり、入口の検証の緩さがそのまま被害の大きさに直結します。

ファイルアップロードの検証不備が起きるパターン

典型的な弱点は次のとおりです。

  • 拡張子だけで種類を判定し、shell.php.jpgのような偽装や二重拡張子を通してしまう
  • クライアントが申告するMIMEタイプ(Content-Type)を信用し、実体を確認していない
  • アップロード先が公開ディレクトリで、かつスクリプトとして実行可能な設定になっている
  • 元のファイル名をそのまま使い、パスの操作や既存ファイルの上書きを許す
  • アップロードしたファイルのURLをそのまま公開し、推測でアクセスされる
  • ファイルサイズや枚数に上限がなく、大量アップロードでストレージを枯渇させられる

共通するのは「申告された情報を信じて受け入れ、保存後の公開、実行、解析までを安全に制御していない」という点です。

対策法

原則は「拡張子だけで判断せず実体まで検証し、保存先では実行させない」ことです。

  • 入口の検査として、拡張子、MIMEタイプ、ファイルの中身(シグネチャ)を照合し、許可した型だけを受け入れる
  • 保存先をスクリプトとして実行できない領域にし、公開ディレクトリに直接置かない
  • ファイル名はサーバが生成した安全な名前にリネームし、元の名前をパスに使わない
  • サイズや枚数の上限を設け、過大なアップロードを弾く
  • 画像はデコーダで読み込み、生成した画像へ再エンコードして、元のバイト列をそのまま配信しない

拡張子やContent-Typeは偽装できるため、本命は実体の検証と実行不可な保存です。 アップロード機能を洗い出し、型の検証と保存先の分離から着手するのが費用対効果の高い進め方になります。

Rails での事例

危険な実装は、クライアント申告の情報を信じて公開ディレクトリへそのまま書き込む形です。

# 危険:ファイル名をそのまま使い、public 配下に保存している
def create
  file = params[:file]
  path = Rails.root.join("public/uploads", file.original_filename)
  File.open(path, "wb") { |f| f.write(file.read) }
end

original_filename は利用者が自由に決められる値であり、パス操作や偽装拡張子の入り口になります。 また file.content_type もブラウザの申告値で、実体とは無関係に偽装できます。

直すときは、実体(マジックバイト)で型を判定し、画像としてデコードしてから再エンコードします。 名前はサーバが生成し、保存は Active Storage 経由にします。

require "image_processing/vips"

ALLOWED_TYPES = %w[image/png image/jpeg].freeze
MAX_FILE_SIZE = 5.megabytes
MAX_DIMENSION = 8_000
MAX_PIXELS = 40_000_000

def create
  file = params[:file]
  return head :bad_request unless file.respond_to?(:tempfile)
  return head :payload_too_large if file.size > MAX_FILE_SIZE

  # name を渡さず、ファイル本体だけで種類を判定する
  detected = Marcel::MimeType.for(file.tempfile)
  return head :unprocessable_entity if detected == "application/octet-stream"
  return head :unprocessable_entity unless ALLOWED_TYPES.include?(detected)

  file.tempfile.rewind
  image = Vips::Image.new_from_file(file.tempfile.path, access: :sequential)
  dimensions_allowed =
    image.width.between?(1, MAX_DIMENSION) &&
    image.height.between?(1, MAX_DIMENSION) &&
    image.width * image.height <= MAX_PIXELS
  return head :unprocessable_entity unless dimensions_allowed

  file.tempfile.rewind
  normalized = ImageProcessing::Vips
    .source(file.tempfile)
    .convert("png")
    .call

  blob = ActiveStorage::Blob.create_and_upload!(
    io: normalized,
    filename: "#{SecureRandom.uuid}.png",
    content_type: "image/png"
  )
  current_user.avatar.attach(blob)
rescue Vips::Error
  head :unprocessable_entity
ensure
  normalized&.close!
end

Marcel は Active Storage が内部で使う判定ライブラリで、Rails に同梱されています。 この例ではimage_processing gemと、そのVipsバックエンドが利用するruby-vipsおよびlibvipsを前提にしています。 name: file.original_filenameを渡すと、実体から判定できないファイルについて拡張子へフォールバックするため、厳密な実体検証にはなりません。 マジックバイトの一致だけでは安全性を保証できないため、画像はデコーダで読み込み、再エンコード後の出力だけを保存します。 デコーダ自体も攻撃対象になるため、ライブラリを更新し、ファイルサイズだけでなく画像の縦横と総画素数にも上限を設けます。 Active Storage の標準的な保存先は Web サーバのドキュメントルートの外にあるため、「アップロード先でスクリプトが実行される」型の事故を避けやすくなります。 ただし、ストレージを公開設定にした場合のアクセス制御やレスポンスのContent-Typeまでは別途確認が必要です。 サイズ上限は file.size の検査とプロキシ側のボディサイズ制限で重ねます。

まとめ

ファイルアップロードの検証不備対策の要点を整理します。

  • 根本原因は「申告された情報を信じて受け入れ、保存後の公開、実行、解析を安全に制御していないこと」であり、拡張子もcontent_typeも実体とは無関係に偽装できる
  • Marcelにファイル名を渡すと拡張子へフォールバックするため、実体(マジックバイト)で厳密に型を判定するときはファイル本体だけを渡し、application/octet-streamを拒否する
  • マジックバイトだけでは安全性を保証できないため、画像はデコードして再エンコードし、生成したファイルだけを実行されない場所へ保存する
  • original_filenameをそのまま使うとパス操作や二重拡張子(shell.php.jpg)の入り口になるため、ファイル名はサーバが生成する
  • サイズ、枚数、画像の縦横、総画素数に上限を設け、アプリ側の検査とプロキシのボディサイズ制限を重ねる

アップロードは、利用者が任意のデータをサーバへ送り込める数少ない経路です。入口の検証の緩さがそのまま被害の大きさに直結するため、型の検証と保存先の分離という2点を最初に固めてください。

関連して読める記事

保存先のファイル名に外部入力を使うと、../で保存位置をずらされるパストラバーサルになります。受け取ったファイルの中身がSVGやdocxであれば、解析ライブラリが内部でXMLパーサを呼ぶためXXEの経路にもなります。アップロードされた画像を外部ツールで変換している場合は、ファイル名がそのままOSコマンドインジェクションの入力になり得ます。

パストラバーサルとは?起きるパターンと対策法

パストラバーサル(ディレクトリトラバーサル)の仕組みと起きるパターン、正規化後の範囲チェックと許可リストを軸にした対策を整理します。Railsで send_file にファイル名パラメータをそのまま渡してしまう危険例と、File.expand_path による正規化+前方一致の検証、入力を「パス」ではなく「ID」にする設計まで具体的なコードで示します。

2026年7月24日

XML外部実体参照(XXE)とは?起きるパターンと対策法

XML外部実体参照(XXE)の仕組みと起きるパターン、XMLパーサの外部実体解決とDTD処理の無効化を軸にした対策を整理します。Nokogiri の noent オプションが名前に反して実体を展開してしまう危険例と、安全な既定設定を外さない運用、SVGやOfficeファイル経由の見落としやすい経路まで具体的に示します。

2026年7月24日

OSコマンドインジェクションとは?起きるパターンと対策法

OSコマンドインジェクションの仕組みと起きるパターン、シェルを介さず多引数形式でコマンドと引数を分離して渡す対策を整理します。Railsで system にユーザー入力を連結した文字列を渡してしまう危険例と、多引数形式での安全な実装、オプション解釈の残余リスクや Shellwords.escape の位置づけまで具体的なコードで示します。

2026年7月24日
この記事をシェア