mcpField
S
sast-fileupload

sast-fileupload

github.com/utkusenセキュリティ1.3k70 回インストール5 日前に更新
リポジトリ

sast-fileupload について

sast-fileuploadは、コードベースの安全性評価で危険なファイルアップロードを検出するスキルです。アップロード箇所を特定し、回避手法を並列で確認して、結果をレポートにまとめます。

主な機能

  1. 01ファイル受信・保存箇所の特定
  2. 02回避手法の並列バッチ検証
  3. 03分析結果レポートの統合
  4. 04事前のアーキテクチャ概要が必要

活用シーン

  1. 01アプリケーションのアップロード処理の確認
  2. 02拡張子やファイル名の検証確認
  3. 03アップロード保存先のリスク調査

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/utkusen/sast-skills --skill sast-fileupload

インストール後、Claude Code で/sast-fileuploadを入力して読み込まれたか確認します。

詳細

安全でないファイルアップロードの検出

コードベースにおける安全でないファイルアップロードの脆弱性を見つけるため、対象を絞ったセキュリティ評価を実施します。このスキルでは、サブエージェントを使った3段階のアプローチを採用します。discovery(アップロードされたファイルを受け取り、保存する箇所をすべて特定する)、batched verify(アップロード箇所を最大3つずつ並列バッチで確認し、回避ベクトルを検証する)、merge(バッチレポートを1つの結果ファイルに統合する)です。

前提条件:sast/architecture.md が存在している必要があります。存在しない場合は、先に分析スキルを実行してください。


安全でないファイルアップロードとは

安全でないファイルアップロードとは、アプリケーションがアップロード可能なファイルを適切に検証または制限せずにユーザーから受け付けることで、攻撃者が実行可能なファイルや悪意のあるファイルをアップロードできる状態のことです。最も重大な結果は**リモートコード実行(RCE)**です。攻撃者がウェブシェル(例:.php ファイル)をアップロードし、直接 URL からアクセスされたときにサーバーが実行します。

基本的なパターンは、ユーザーが提供したファイルが、拡張子の検証が不十分なまま保存場所に到達し、保存されたファイルにアクセスできる、または実行できることです。

安全でないファイルアップロードに該当するもの
  • ファイル形式や内容を確認せず、あらゆるファイル形式を受け付ける:検証なしで file.save(upload_path) を実行する
  • Content-Type のみの検証:実際の拡張子やファイル内容を確認せず、Content-Type: image/png をチェックする — ヘッダーを手動で設定するだけで簡単に回避できます
  • 不備のある拡張子ブロックリスト:.php はブロックするが、.php3、.php4、.php5、.phtml、.phar、.shtml はブロックしない
  • 大文字と小文字の違いを利用した回避:.php はブロックするが、.PHP、.Php、.pHp は許可する
  • 二重拡張子による回避:shell.php.jpg — コードは最後の .jpg を抽出して安全と判断するが、サーバー(Apache)は PHP として処理する
  • ファイル名のパストラバーサル:サニタイズされていないファイル名を使って ../../webroot/shell.php に保存する
  • 不完全なファイル名のサニタイズ:../ のみを取り除き、%2e%2e%2f などのエンコードされた表現を取り除かない
  • 実行を無効にせず、アップロードされたファイルをウェブから実行可能なディレクトリで配信する
安全でないファイルアップロードに該当しないもの

以下はファイルアップロードの脆弱性として報告しないでください。

  • SVG を介した格納型 XSS:<script> が埋め込まれた SVG をアップロードし、その内容が返される — これは XSS であり、アップロードファイルの実行に関する問題ではありません
  • ファイル内容を介した SSRF:外部リクエストを発生させる XML や SVG をアップロードする — これは XXE/SSRF であり、アップロードファイルの実行に関する問題ではありません
  • 大容量ファイルによる DoS:ファイルサイズ制限がない — 可用性に関する別の問題です
  • ダウンロード時の IDOR:認可なしで他のユーザーがアップロードしたファイルにアクセスする — これは IDOR です
  • 安全なアップロード:ファイルをウェブルートの外部に保存する場合、Content-Disposition: attachment を設定する制御されたダウンロードエンドポイントを通じて配信する場合、または公開実行機能のないオブジェクトストレージバケットに保存する場合
安全でないファイルアップロードを防ぐパターン

以下のパターンが組み合わさっている場合、コードは脆弱ではない可能性が高いです。

1. 安全な拡張子の許可リスト(最も重要)

ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif', 'pdf'}
ext = filename.rsplit('.', 1)[-1].lower()
if ext not in ALLOWED_EXTENSIONS:
    abort(400)

2. マジックバイト/ファイル内容の検証(多層防御)

import magic
mime = magic.from_buffer(file.read(2048), mime=True)
ALLOWED_MIMES = {'image/png', 'image/jpeg', 'image/gif'}
if mime not in ALLOWED_MIMES:
    abort(400)

3. 信頼できるライブラリを使ったファイル名のサニタイズ

from werkzeug.utils import secure_filename
filename = secure_filename(file.filename)  # strips path separators and dangerous chars

4. アップロードファイルをウェブルートの外部に保存する

/var/uploads/  ← not served by the web server
/var/www/html/ ← web root (do NOT store uploads here)

5. Content-Disposition を設定する制御されたエンドポイントを通じてアップロードファイルを配信する

@app.route('/download/<filename>')
def download(filename):
    return send_from_directory(UPLOAD_FOLDER, filename,
                               as_attachment=True)  # forces download, prevents execution

6. サーバーが生成した UUID にファイル名を変更する

import uuid
stored_name = str(uuid.uuid4()) + '.jpg'  # extension is server-controlled, not user-controlled

脆弱な例と安全な例

Python — Flask
# VULNERABLE: no extension check, file stored in web-accessible directory
@app.route('/upload', methods=['POST'])
def upload():
    f = request.files['file']
    f.save(os.path.join('static/uploads', f.filename))
    return 'uploaded'

# VULNERABLE: content-type only check (trivially bypassed with curl -H)
@app.route('/upload', methods=['POST'])
def upload():
    f = request.files['file']
    if f.content_type not in ['image/png', 'image/jpeg']:
        abort(400)
    f.save(os.path.join('static/uploads', f.filename))
    return 'uploaded'

# VULNERABLE: blocklist — .phtml/.phar/.php5 not covered
BLOCKED = {'.php', '.sh', '.exe'}
@app.route('/upload', methods=['POST'])
def upload():
    f = request.files['file']
    ext = os.path.splitext(f.filename)[1].lower()
    if ext in BLOCKED:
        abort(400)
    f.save(os.path.join('static/uploads', f.filename))
    return 'uploaded'

# SECURE: allowlist + sanitized filename + outside web root
ALLOWED = {'png', 'jpg', 'jpeg', 'gif'}
UPLOAD_FOLDER = '/var/uploads'  # outside web root

@app.route('/upload', methods=['POST'])
def upload():
    f = request.files['file']
    filename = secure_filename(f.filename)
    ext = filename.rsplit('.', 1)[-1].lower()
    if ext not in ALLOWED:
        abort(400)
    f.save(os.path.join(UPLOAD_FOLDER, filename))
    return 'uploaded'
Python — Django
# VULNERABLE: no validation on FileField
class DocumentForm(forms.ModelForm):
    class Meta:
        model = Document
        fields = ['upload']

# VULNERABLE: manual save with no extension check
def upload(request):
    f = request.FILES['file']
    with open(f'media/uploads/{f.name}', 'wb+') as dest:
        for chunk in f.chunks():
            dest.write(chunk)

# SECURE: custom validator on FileField
def validate_file_extension(value):
    ext = os.path.splitext(value.name)[1].lower()
    if ext not in ['.png', '.jpg', '.jpeg', '.gif']:
        raise ValidationError('Unsupported file extension.')

class DocumentForm(forms.ModelForm):
    upload = forms.FileField(validators=[validate_file_extension])
Node.js — Multer (Express)
// VULNERABLE: no file filter, stored in public directory
const upload = multer({ dest: 'public/uploads/' });
app.post('/upload', upload.single('file'), (req, res) => {
    res.send('uploaded');
});

// VULNERABLE: MIME type filter only (can be faked)
const upload = multer({
    dest: 'uploads/',
    fileFilter: (req, file, cb) => {
        if (!file.mimetype.startsWith('image/')) return cb(null, false);
        cb(null, true);
    }
});

// SECURE: allowlist of extensions + storage outside web root
const ALLOWED_EXT = ['.jpg', '.jpeg', '.png', '.gif'];
const storage = multer.diskStorage({
    destination: '/var/uploads',  // not served by Express
    filename: (req, file, cb) => {
        const ext = path.extname(file.originalname).toLowerCase();
        cb(null, `${uuidv4()}${ext}`);
    }
});
const upload = multer({
    storage,
    fileFilter: (req, file, cb) => {
        const ext = path.extname(file.originalname).toLowerCase();
        cb(null, ALLOWED_EXT.includes(ext));
    }
});
PHP
// VULNERABLE: no extension check, stored in web root
move_uploaded_file($_FILES['file']['tmp_name'], 'uploads/' . $_FILES['file']['name']);

// VULNERABLE: checking only content type header
if ($_FILES['file']['type'] !== 'image/jpeg') {
    die('Invalid file type');
}
move_uploaded_file($_FILES['file']['tmp_name'], 'uploads/' . $_FILES['file']['name']);

// VULNERABLE: blocklist missing phtml/phar
$ext = strtolower(pathinfo($_FILES['file']['name'], PATHINFO_EXTENSION));
$blocked = ['php', 'sh', 'py'];
if (in_array($ext, $blocked)) die('Blocked');
move_uploaded_file($_FILES['file']['tmp_name'], 'uploads/' . $_FILES['file']['name']);

// SECURE: allowlist + rename to UUID + outside web root
$allowed = ['jpg', 'jpeg', 'png', 'gif'];
$ext = strtolower(pathinfo($_FILES['file']['name'], PATHINFO_EXTENSION));
if (!in_array($ext, $allowed)) die('Invalid extension');
$stored = '/var/uploads/' . bin2hex(random_bytes(16)) . '.' . $ext;
move_uploaded_file($_FILES['file']['tmp_name'], $stored);
Java — Spring Boot (MultipartFile)
// VULNERABLE: no validation, stored in web-accessible path
@PostMapping("/upload")
public String upload(@RequestParam("file") MultipartFile file) throws IOException {
    Path path = Paths.get("src/main/resources/static/uploads/" + file.getOriginalFilename());
    Files.write(path, file.getBytes());
    return "uploaded";
}

// VULNERABLE: content type header only
@PostMapping("/upload")
public String upload(@RequestParam("file") MultipartFile file) throws IOException {
    if (!file.getContentType().startsWith("image/")) throw new BadRequestException();
    Files.write(Paths.get("uploads/" + file.getOriginalFilename()), file.getBytes());
    return "uploaded";
}

// SECURE: allowlist + UUID rename + path outside web root
private static final Set<String> ALLOWED = Set.of("jpg", "jpeg", "png", "gif");

@PostMapping("/upload")
public String upload(@RequestParam("file") MultipartFile file) throws IOException {
    String original = StringUtils.cleanPath(file.getOriginalFilename());
    String ext = FilenameUtils.getExtension(original).toLowerCase();
    if (!ALLOWED.contains(ext)) throw new BadRequestException("Invalid extension");
    String stored = UUID.randomUUID() + "." + ext;
    Files.write(Paths.get("/var/uploads/" + stored), file.getBytes());
    return "uploaded";
}
Go
// VULNERABLE: no extension check, stored in static directory
func uploadHandler(w http.ResponseWriter, r *http.Request) {
    file, header, _ := r.FormFile("file")
    defer file.Close()
    dst, _ := os.Create("static/uploads/" + header.Filename)
    defer dst.Close()
    io.Copy(dst, file)
}

// SECURE: allowlist extension + UUID rename + outside web root
var allowed = map[string]bool{"jpg": true, "jpeg": true, "png": true, "gif": true}

func uploadHandler(w http.ResponseWriter, r *http.Request) {
    file, header, _ := r.FormFile("file")
    defer file.Close()
    ext := strings.ToLower(filepath.Ext(header.Filename))
    if ext == "" || !allowed[ext[1:]] {
        http.Error(w, "invalid extension", http.StatusBadRequest)
        return
    }
    stored := "/var/uploads/" + uuid.New().String() + ext
    dst, _ := os.Create(stored)
    defer dst.Close()
    io.Copy(dst, file)
}
Ruby on Rails
# VULNERABLE: no content type or extension validation
def upload
  file = params[:file]
  File.open(Rails.root.join('public', 'uploads', file.original_filename), 'wb') do |f|
    f.write(file.read)
  end
end

# SECURE: ActiveStorage with content type allowlist (Rails 6+)
has_one_attached :avatar
validates :avatar, content_type: ['image/png', 'image/jpg', 'image/jpeg']
# Note: still validate extension too — content_type is user-supplied in some configurations

# SECURE: CarrierWave with extension and content type allowlist
class AvatarUploader < CarrierWave::Uploader::Base
  def extension_allowlist
    %w[jpg jpeg png gif]
  end

  def content_type_allowlist
    /image\//
  end
end
C# — ASP.NET Core
// VULNERABLE: no extension check, stored in wwwroot
[HttpPost]
public async Task<IActionResult> Upload(IFormFile file) {
    var path = Path.Combine("wwwroot/uploads", file.FileName);
    using var stream = new FileStream(path, FileMode.Create);
    await file.CopyToAsync(stream);
    return Ok();
}

// SECURE: allowlist + GUID rename + outside web root
private static readonly HashSet<string> _allowed = new() { ".jpg", ".jpeg", ".png", ".gif" };

[HttpPost]
public async Task<IActionResult> Upload(IFormFile file) {
    var ext = Path.GetExtension(file.FileName).ToLowerInvariant();
    if (!_allowed.Contains(ext)) return BadRequest("Invalid extension");
    var stored = Path.Combine("/var/uploads", $"{Guid.NewGuid()}{ext}");
    using var stream = new FileStream(stored, FileMode.Create);
    await file.CopyToAsync(stream);
    return Ok();
}

実行

このスキルはサブエージェントを使い、3つのフェーズで実行されます。すべてのサブエージェントに、コンテキストとして sast/architecture.md の内容を渡してください。

フェーズ 1:すべてのファイルアップロード箇所を見つける

次の指示をサブエージェントに与えて起動します。

目標:コードベース内で、ユーザーがアップロードしたファイルを受け取り、保存するすべての箇所を見つけます。結果を sast/fileupload-recon.md に書き込みます。

コンテキスト:プロジェクトのアーキテクチャ概要が与えられます。これを使って、フレームワーク、ファイルの保存方法、アップロード先がローカルディスク、クラウドストレージ、CDN のいずれかを把握してください。

検索対象 — ファイルアップロード処理のパターン:

HTTP リクエストからファイルを受け取り、書き込みまたは保存するコードを探してください。検証の有無はまだ評価せず、箇所をすべて見つけてください。

  1. Python / Django:
    • request.FILES へのアクセス
    • InMemoryUploadedFile、TemporaryUploadedFile
    • default_storage.save(...)、FileSystemStorage().save(...)
    • モデルの FileField / ImageField フォーム送信
    • アップロードデータに対する shutil.copyfileobj(f, dest) または手動の .write(f.read())
  2. Python / Flask:
    • request.files.get(...) または request.files[...]
    • FileStorage オブジェクトに対する file.save(...) 呼び出し
    • werkzeug の FileStorage 処理
  3. Node.js:
    • multer ミドルウェア:upload.single(...)、upload.array(...)、upload.fields(...)
    • busboy、formidable、multiparty によるフォーム解析
    • express-fileupload:req.files
    • リクエストストリームに対する fs.writeFile / fs.createWriteStream / pipe() の呼び出し
  4. PHP:
    • $_FILES へのアクセス
    • move_uploaded_file(...) の呼び出し
    • copy($_FILES[...]['tmp_name'], ...)
  5. Java / Spring:
    • コントローラーメソッドの MultipartFile パラメーター:@RequestParam MultipartFile
    • CommonsMultipartFile、StandardMultipartFile
    • Part.write(...)(Servlet API)
    • file.transferTo(...)、Files.write(path, file.getBytes())
  6. Go:
    • r.FormFile(...) または r.MultipartForm.File
    • マルチパートフォーム由来の file に対する io.Copy(dst, file)
    • header.Filename から生成したファイル名を指定する os.Create(...)
  7. Ruby / Rails:
    • .read、.original_filename、.tempfile を伴う params[:file]
    • アップロードデータを指定した File.open(..., 'wb')
    • has_one_attached / has_many_attached(ActiveStorage)
    • CarrierWave の mount_uploader、Shrine の include Shrine::Attachment
  8. C# / ASP.NET:
    • IFormFile パラメーター:file.CopyToAsync(...)、file.OpenReadStream()
    • HttpPostedFileBase.SaveAs(...)
    • Request.Files[...]

出力形式 — sast/fileupload-recon.md に書き込みます:

# ファイルアップロードの調査結果:[プロジェクト名]

## 概要
[N] 件のファイルアップロード箇所が見つかりました。

## アップロード箇所

### 1. [説明的な名前 — 例:「アバターのアップロードエンドポイント」]
- **ファイル**:`path/to/file.ext`(行 X-Y)
- **エンドポイント / 関数**:[ルートまたは関数名]
- **フレームワーク / メソッド**:[例:Flask request.files / multer / move_uploaded_file]
- **保存先**:[パス、変数、またはストレージ抽象化 — 例:「static/uploads/」、「boto3 経由の S3」、「不明」]
- **確認された検証**(暫定。詳細な分析はフェーズ 2 で実施):[拡張子チェック、コンテンツタイプチェック、または「確認できず」を列挙]
- **コードスニペット**:

[ファイルを受信して保存するコード]


[各箇所について繰り返す]
フェーズ 1 の後:続行する前に候補を確認する

フェーズ 1 の完了後、sast/fileupload-recon.md を読みます。調査でアップロード箇所が 0 件(概要に「Found 0」と記載されている、または「Upload Sites」セクションが空か存在しない)だった場合は、フェーズ 2 とフェーズ 3 を完全に省略します。その代わり、次の内容を sast/fileupload-results.md に書き込み、終了します。

# File Upload Analysis Results

No file upload sites found.

フェーズ 2 に進むのは、フェーズ 1 でアップロード箇所が 1 件以上見つかった場合に限ります。

フェーズ 2:拡張子バイパスの脆弱性を確認する(バッチ処理)

フェーズ 1 の完了後、sast/fileupload-recon.md を読み、アップロード箇所を1 バッチ最大 3 箇所に分割します。各バッチにつきサブエージェントを 1 つ起動し、並列で実行します。各サブエージェントは割り当てられた箇所のみを分析し、結果を各自のバッチファイルに書き込みます。

バッチ分割の手順(サブエージェントではなく、オーケストレーターであるあなたが実行します):

  1. sast/fileupload-recon.md を読み、番号付きの箇所セクション(### 1.、### 2. など)の数を数えます。
  2. 最大 3 箇所ずつのバッチに分けます。例:8 箇所の場合は 3 バッチ(1-3、4-6、7-8)です。
  3. 各バッチについて、調査ファイルから該当する箇所セクションの全文を抽出します。
  4. 各バッチに割り当てた箇所のみを渡し、すべてのバッチサブエージェントを並列で起動します。
  5. 各サブエージェントは sast/fileupload-batch-N.md(N は 1 から始まるバッチ番号)に書き込みます。
  6. sast/architecture.md からプロジェクトの主要な言語/フレームワークを特定し、「脆弱な例と安全な例」セクションから一致する例のみを選びます。たとえば、プロジェクトが Multer を使用する Node.js の場合は、「Node.js — Multer(Express)」の例のみを含めます。以下のサブエージェントへの指示内で [TECH-STACK EXAMPLES] と示されている箇所に、選択した例を含めてください。

各バッチサブエージェントに、以下の指示を渡します(バッチごとの値に置き換えてください):

目標:以下に割り当てられた各ファイルアップロードサイトについて、ファイル名、拡張子、または Content-Type ヘッダーを操作することで、攻撃者が悪意のあるファイル(例:PHP Web シェル、JSP シェル、Python スクリプト)をアップロードできるかどうかを判断してください。結果を sast/fileupload-batch-[N].md に記述してください。

割り当てられたアップロードサイト(偵察フェーズより):

[元の番号を保持したまま、割り当てられたサイトのセクション全文をここに貼り付けてください]

コンテキスト:プロジェクトのアーキテクチャ概要が提供されます。フレームワーク、ストレージパス、アップロードファイルの配信方法を理解するために活用してください。

参考 — 安全でないファイルアップロードとは何か、何ではないか:

適切な制御なしに、実行可能または危険なファイルタイプがストレージに到達することに注目してください。SVG による保存型 XSS、アップロードした XML による SSRF、サイズ制限による DoS、ダウンロード時の IDOR は、ファイルアップロードによる実行の問題として報告しないでください(これらはほかのスキルで扱います)。

リスクを低減するパターン — 許可リスト、サニタイズ、Web ルート外への保存、UUID への名前変更が強力に組み合わされている場合、回避方法が適用されない限り、そのサイトは 脆弱ではない と考えられます。

このプロジェクトの技術スタックにおける脆弱な例と安全な例:

[TECH-STACK EXAMPLES]

各アップロードサイトで次の回避ベクトルを評価してください:

  1. 拡張子チェックがない:ファイル名や拡張子に対する検証が一切ありません。どのようなファイルでも受け付けます。直ちに 脆弱 と判定してください。

  2. Content-Type / MIME ヘッダーのみ:検証でリクエストヘッダーの Content-Type または mimetype を読み取りますが、実際のファイル名の拡張子やファイルのバイト列を調べません。攻撃者は shell.php をアップロードしながら Content-Type: image/png を指定できます。脆弱 と判定してください。

  3. ブロックリストによる検証:禁止する拡張子の明示的なリストです。サーバーの技術に対してブロックリストが網羅的かどうかを確認してください:

    • PHP サーバー:.php3、.php4、.php5、.php7、.phtml、.phar、.shtml もブロックされていますか?1 つでも不足していれば、脆弱 と判定してください。
    • Java サーバー:.jsp、.jspx、.jsw、.jsv、.jspf もブロックされていますか?
    • ASP.NET サーバー:.asp、.aspx、.ashx、.asmx、.cer、.asa もブロックされていますか?
    • Node.js:サーバー設定により .js を実行できますか?アップロードディレクトリ内の .js ファイルを require/実行できるか確認してください。
    • どのようなブロックリストも許可リストより本質的に弱いため、一見網羅的であっても 脆弱の可能性あり と判定してください。
  4. 大文字・小文字の違いを利用した回避:.php はブロックしますが、.PHP、.Php、.pHp はブロックしません。.toLowerCase()/.lower()/strtolower()` / 大文字・小文字を区別しない照合を使用しているか確認してください。

  5. 二重拡張子/複数拡張子:shell.php.jpg — 最後のドット以降の部分を取得するメソッドで拡張子を抽出している場合、これは許可リストで拒否されるはずです。ただし、AddHandler の設定に誤りがある Apache サーバーでは、実行時に最も左側にある認識済みの拡張子が使われる場合があります。拡張子の抽出方法を確認してください:

    • 安全:filename.rsplit('.', 1)[-1]、path.extname(filename)(最後の拡張子を取得)
    • リスクのあるサーバー設定:Apache AddHandler application/x-httpd-php .php — shell.php.jpg でも PHP として実行される可能性があります
  6. ファイル名を使ったパストラバーサル:元のファイル名をサニタイズせずにストレージパスで使用している場合、../../webroot/shell.php によって意図しないディレクトリにファイルを配置できます。以下を確認してください:

    • secure_filename()、basename()、path.basename()、Path.GetFileName()、または filepath.Base() の使用 — これらはディレクトリ区切り文字を取り除くため安全です
    • サニタイズせずに file.filename、header.Filename、file.getOriginalFilename()、$_FILES['name'] をパス結合で直接使用 — 脆弱 と判定してください
  7. Web サーバーが実行可能なディレクトリへのファイル保存:拡張子の許可リストが適切でも、アップロード先が Web サーバーによって配信されるディレクトリ(例:static/uploads/、public/uploads/、wwwroot/uploads/)であり、Web サーバーがスクリプトを実行する設定の場合、拡張子検証の回避が重大な問題になります。保存パスが Web からアクセス可能かどうかを記載してください。

  8. コンテンツベースの検証(マジックバイト)がない:サーバーが実際のファイル内容を確認せず、拡張子を信頼しています。拡張子チェックを回避でき、サーバーが実行する場合、PHP コードを含む shell.jpg も危険です。マジックバイトのチェックがないことを、リスクを高める要因として記載してください。

分類:

  • 脆弱:検証が一切ない、または明らかに回避可能なチェック(Content-Type のみ、ブロックリストに一般的な拡張子がない、.lower() がない、ファイル名を使ったパストラバーサル)。
  • 脆弱の可能性あり:網羅的に見えても、許可リストより本質的に弱いブロックリスト。または、大文字の拡張子を考慮しないなど、潜在的な問題を含む許可リスト。
  • 脆弱ではない:安全な拡張子の厳格な許可リスト(大文字・小文字を区別せず適用)に加え、ファイル名のサニタイズやサーバー生成の UUID への名前変更を行い、ファイルを Web ルート外または制御されたダウンロードエンドポイントの背後に保存している。
  • 手動レビューが必要:検証ロジックが共有ヘルパーまたはミドルウェアにあり、全体を確認できなかった場合、またはストレージパスが動的で特定できなかった場合。

出力形式 — sast/fileupload-batch-[N].md に記述してください:

# ファイルアップロードバッチ [N] の結果

## 検出結果

### [脆弱] 説明的な名前
- **ファイル**:`path/to/file.ext`(行 X-Y)
- **エンドポイント/関数**:[ルートまたは関数名]
- **問題**:[例:「拡張子検証なし — すべてのファイルタイプを受け入れる」または「Content-Type ヘッダーのみをチェックに使用」]
- **回避ベクトル**:[具体的な手法 — 例:「shell.php を直接アップロード」または「.php ファイルをアップロードしながら Content-Type: image/png を設定」または「ブロックリストに含まれていない .phtml 拡張子を使用」]
- **保存パス**:[ファイルの保存先 — Web からアクセス可能かどうか]
- **影響**:[例:「攻撃者が PHP Web シェルをアップロードし、/uploads/shell.php にアクセスして RCE を達成する」]
- **修正方法**:[具体的な修正 — 許可リストへの切り替え、`.lower()` の追加、secure_filename の使用、Web ルート外への保存]
- **動的テスト**:

[回避方法を実証する curl または HTTP リクエスト。 例:curl -X POST https://app.example.com/upload
-F "[email protected];type=image/png"
その後アクセス:https://app.example.com/static/uploads/shell.php?cmd=id]


### [脆弱の可能性あり] 説明的な名前
- **ファイル**:`path/to/file.ext`(行 X-Y)
- **エンドポイント/関数**:[ルートまたは関数名]
- **問題**:[例:「ブロックリストによる拡張子チェック — 本質的に不完全」]
- **回避ベクトル**:[回避の可能性 — 例:「サーバーが Apache/PHP の場合は .phtml、.phar、.php5 を試す」]
- **保存パス**:[ファイルの保存先]
- **懸念事項**:[依然としてリスクがある理由]
- **修正方法**:[ブロックリストを許可リストに置き換える]
- **動的テスト**:

[回避を試みるペイロード]


### [脆弱ではない] 説明的な名前
- **ファイル**:`path/to/file.ext`(行 X-Y)
- **エンドポイント/関数**:[ルートまたは関数名]
- **理由**:[例:「png/jpg/gif の厳格な許可リストに加えて、.lower() を使用し、UUID に名前を変更して Web ルート外に保存」]

### [手動レビューが必要] 説明的な名前
- **ファイル**:`path/to/file.ext`(行 X-Y)
- **エンドポイント/関数**:[ルートまたは関数名]
- **不明点**:[検証ロジックまたは保存パスを特定できなかった理由]
- **提案**:[手動で追跡すべき内容]
フェーズ 3:マージ — バッチ結果を統合する

フェーズ 2 のすべてのバッチサブエージェントが完了したら、sast/fileupload-batch-*.md ファイルをすべて読み込み、内容を 1 つの sast/fileupload-results.md に統合します。これはオーケストレーターであるあなたが直接行います。サブエージェントは必要ありません。

マージ手順:

  1. sast/fileupload-batch-1.md、sast/fileupload-batch-2.md、... の各ファイルを読み込みます。
  2. 各バッチファイルからすべての検出事項を収集し、元の分類とすべての詳細フィールドを保持したまま、1 つのリストにまとめます。
  3. エグゼクティブサマリーの合計を、すべてのバッチを対象に集計します(分析したサイトの総数は偵察結果の数と一致し、分類ごとの件数は各バッチの合計とします)。
  4. 次の形式で、統合レポートを sast/fileupload-results.md に書き込みます。
# File Upload Analysis Results: [Project Name]

## Executive Summary
- Upload sites analyzed: [total from recon]
- Vulnerable: [N]
- Likely Vulnerable: [N]
- Not Vulnerable: [N]
- Needs Manual Review: [N]

## Findings

[All findings from all batches, grouped by classification:
 VULNERABLE first, then LIKELY VULNERABLE, then NEEDS MANUAL REVIEW, then NOT VULNERABLE.
 Preserve every field from the batch results exactly as written.]
  1. sast/fileupload-results.md に書き込んだ後、中間バッチファイルをすべて削除します(sast/fileupload-batch-*.md)。

重要な注意事項

  • sast/architecture.md を読み込み、その内容をコンテキストとしてすべてのサブエージェントに渡します。
  • フェーズ 2 は、フェーズ 1 の完了後に実行する必要があります。フェーズ 2 は偵察結果に依存します。
  • フェーズ 3 は、フェーズ 2 のすべてのバッチが完了した後に実行する必要があります。フェーズ 3 は、すべてのバッチ出力に依存します。
  • バッチサイズはサブエージェント 1 つあたりアップロードサイト 3 件です。サイトの合計が 1~3 件の場合は、サブエージェントを 1 つ使用します。10 件の場合は、サブエージェントを 4 つ使用します(3+3+3+1)。
  • すべてのバッチサブエージェントは並列で起動します。順番に実行しないでください。
  • 各バッチサブエージェントには、偵察ファイル全体ではなく、割り当てられたサイトのテキストだけを渡します。これにより、各サブエージェントのコンテキストを小さく、焦点の絞られたものにできます。
  • フェーズ 1 は純粋に発見のみを行います。ユーザー提供のファイルを受け取り、保存するすべての箇所を見つけます。フェーズ 1 では検証について詳しく分析せず、確認できる内容を記録するだけにします。それはフェーズ 2 で行います。
  • フェーズ 2 は純粋にバイパス分析を行います。割り当てられた各アップロード箇所について、検証ロジックを調べ、拡張子の操作、大文字小文字の違い、Content-Type の偽装、またはパストラバーサルによってバイパスできるかどうかを判断します。
  • フェーズ 3 はマージのみを行います。バッチファイルを sast/fileupload-results.md に統合し、中間ファイルを削除します。フェーズ 3 ではコードを再分析しないでください。
  • 許可リストは常に拒否リストより強力です。拒否リスト方式はほぼ必ず不完全なため、拒否リストに基づく方法は少なくとも 脆弱性の可能性が高い として報告してください。
  • Content-Type(HTTP ヘッダーの MIME タイプ)は完全に攻撃者が制御できます。セキュリティ制御として扱わないでください。
  • 大文字小文字の区別は重要です。.toLowerCase() がない場合、.PHP は .php のチェックをバイパスします。必ず確認してください。
  • ファイル名のパストラバーサルは、拡張子バイパスとは別の攻撃ベクトルです。両方を確認してください。
  • 拡張子チェックが正しくても、ファイルが Web から実行可能なディレクトリに保存されている場合は、防御が弱まります。すべての検出事項に保存先を記載してください。
  • マジックバイトのチェック(実際のファイルバイト列の読み取り)は多層防御になりますが、拡張子の許可リストの代わりにはなりません。有効な画像に PHP コードを追加したファイルも、依然として危険な場合があります。
  • 判断に迷う場合は、脆弱性なしではなく「手動レビューが必要」に分類してください。セキュリティ評価では、偽陰性は偽陽性よりも重大です。
  • 中間ファイルをクリーンアップします。最終版の sast/fileupload-results.md を書き込んだ後、sast/fileupload-recon.md とすべての sast/fileupload-batch-*.md ファイルを削除してください。

コメント