Flask ファイルアップロード機能

2026-02-24

はじめに

現代のWebアプリケーションにおいて、ファイルアップロード機能はユーザーがコンテンツを共有するための重要な手段です。プロフィール画像の設定、ドキュメントの提出、写真の共有など、様々な場面で活用されています。Flaskでは比較的少ないコードで安全なファイルアップロード機能を実装することができます。本記事では、画像を中心としたファイルアップロードの実装方法、セキュリティ対策、ベストプラクティスについて詳しく解説します。初心者の方でも理解しやすいように、実際のコード例を交えながら段階的に説明していきます。

画像アップロードの実装

Flaskでファイルアップロード機能を実装するには、まず基本的なHTMLフォームの作成から始めます。ファイルアップロード用のフォームには、enctype属性に”multipart/form-data”を指定することが必須です。この設定がないと、ファイルデータを正しくサーバーに送信することができません。

以下は、シンプルな画像アップロードフォームの例です。

<!DOCTYPE html>
<html>
<head>
    <title>画像アップロード</title>
</head>
<body>
    <h1>画像をアップロード</h1>
    <form method="POST" action="/upload" enctype="multipart/form-data">
        <input type="file" name="image" accept="image/*" required>
        <button type="submit">アップロード</button>
    </form>
</body>
</html>

次に、Flaskアプリケーション側でこのフォームから送信されたファイルを受け取る処理を実装します。Flaskのrequestオブジェクトのfiles属性からアップロードされたファイルにアクセスできます。

from flask import Flask, request, render_template, redirect, url_for
import os
from werkzeug.utils import secure_filename

app = Flask(__name__)
app.config['UPLOAD_FOLDER'] = 'uploads'
app.config['MAX_CONTENT_LENGTH'] = 2 * 1024 * 1024  # 2MB制限

# アップロードフォルダが存在しない場合は作成
if not os.path.exists(app.config['UPLOAD_FOLDER']):
    os.makedirs(app.config['UPLOAD_FOLDER'])

@app.route('/')
def index():
    return render_template('upload.html')

@app.route('/upload', methods=['POST'])
def upload_file():
    if 'image' not in request.files:
        return 'ファイルが選択されていません'

    file = request.files['image']

    if file.filename == '':
        return 'ファイルが選択されていません'

    if file:
        filename = secure_filename(file.filename)
        file_path = os.path.join(app.config['UPLOAD_FOLDER'], filename)
        file.save(file_path)
        return f'ファイル {filename} のアップロードに成功しました'

if __name__ == '__main__':
    app.run(debug=True)

この基本的な実装では、ユーザーがアップロードしたファイルをサーバーのuploadsフォルダに保存します。secure_filename関数を使用することで、ファイル名に含まれる危険な文字を除去し、セキュリティを強化しています。

ファイル形式のバリデーション

ファイルアップロード機能において、バリデーションはセキュリティ上の最重要項目です。適切なバリデーションを行わないと、悪意のあるファイルをアップロードされる危険性があります。特に、実行可能ファイルやスクリプトファイルのアップロードを防ぐことは必須です。

以下に、包括的なバリデーションを実装した例を示します。

import os
from flask import Flask, request, render_template, flash, redirect, url_for
from werkzeug.utils import secure_filename
from PIL import Image

app = Flask(__name__)
app.config['UPLOAD_FOLDER'] = 'uploads'
app.config['MAX_CONTENT_LENGTH'] = 2 * 1024 * 1024  # 2MB
app.secret_key = 'your-secret-key-here'

# 許可するファイル拡張子
ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif', 'bmp'}

def allowed_file(filename):
    """ファイル拡張子のチェック"""
    return '.' in filename and \
           filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS

def is_valid_image(file_stream):
    """実際の画像ファイルかどうかをチェック"""
    try:
        image = Image.open(file_stream)
        image.verify()  # 画像の整合性を検証
        file_stream.seek(0)  # ストリームを先頭に戻す
        return True
    except Exception:
        return False

@app.route('/upload', methods=['POST'])
def upload_file():
    if 'image' not in request.files:
        flash('ファイルが選択されていません')
        return redirect(request.url)

    file = request.files['image']

    if file.filename == '':
        flash('ファイルが選択されていません')
        return redirect(request.url)

    if not file:
        flash('無効なファイルです')
        return redirect(request.url)

    # ファイル拡張子のチェック
    if not allowed_file(file.filename):
        flash('許可されていないファイル形式です。PNG, JPG, JPEG, GIF, BMP形式のみアップロードできます。')
        return redirect(request.url)

    # 実際の画像ファイルかチェック
    if not is_valid_image(file.stream):
        flash('無効な画像ファイルです')
        return redirect(request.url)

    # ファイルサイズのチェック(request.content_lengthを使用)
    if request.content_length > app.config['MAX_CONTENT_LENGTH']:
        flash('ファイルサイズが大きすぎます。2MB以下のファイルをアップロードしてください。')
        return redirect(request.url)

    if file and allowed_file(file.filename):
        filename = secure_filename(file.filename)

        # ファイル名の重複を防ぐための処理
        base_name, extension = os.path.splitext(filename)
        counter = 1
        while os.path.exists(os.path.join(app.config['UPLOAD_FOLDER'], filename)):
            filename = f"{base_name}_{counter}{extension}"
            counter += 1

        file_path = os.path.join(app.config['UPLOAD_FOLDER'], filename)
        file.save(file_path)

        flash(f'ファイル {filename} のアップロードに成功しました')
        return redirect(url_for('index'))

    flash('アップロードに失敗しました')
    return redirect(request.url)

@app.route('/')
def index():
    return render_template('upload_with_flash.html')

この改良版のコードでは、以下のような多層的なバリデーションを実装しています。

ファイル拡張子のチェックでは、事前に定義した許可リストと照合します。このホワイトリスト方式により、未知のファイル形式を排除できます。

実際の画像ファイルかどうかの検証にはPIL(Pillow)ライブラリを使用しています。このチェックにより、拡張子を偽装した悪意のあるファイルを検出できます。

ファイルサイズの制限は、FlaskのMAX_CONTENT_LENGTH設定とリクエストのcontent_lengthを比較することで行います。

安全なファイル保存

ファイルを安全に保存するためには、保存場所の管理、ファイル名の処理、アクセス権限の設定など、様々な観点からの対策が必要です。特に、公開されるべきでないファイルがWebから直接アクセスされないようにすることは極めて重要です。

以下に、安全なファイル保存を実現するための完全な実装例を示します。

import os
import uuid
from datetime import datetime
from flask import Flask, request, render_template, flash, redirect, url_for, send_from_directory
from werkzeug.utils import secure_filename
from PIL import Image

app = Flask(__name__)
app.config['UPLOAD_FOLDER'] = 'uploads'
app.config['MAX_CONTENT_LENGTH'] = 2 * 1024 * 1024
app.config['ALLOWED_EXTENSIONS'] = {'png', 'jpg', 'jpeg', 'gif', 'bmp'}
app.secret_key = 'your-secret-key-here'

# 安全なアップロードディレクトリの設定
BASE_DIR = os.path.abspath(os.path.dirname(__file__))
UPLOAD_DIR = os.path.join(BASE_DIR, app.config['UPLOAD_FOLDER'])

# アップロードディレクトリの作成(存在しない場合)
if not os.path.exists(UPLOAD_DIR):
    os.makedirs(UPLOAD_DIR)

def allowed_file(filename):
    return '.' in filename and \
           filename.rsplit('.', 1)[1].lower() in app.config['ALLOWED_EXTENSIONS']

def is_valid_image(file_stream):
    try:
        image = Image.open(file_stream)
        image.verify()
        file_stream.seek(0)
        return True
    except Exception:
        return False

def generate_safe_filename(original_filename):
    """安全なファイル名を生成"""
    # 元のファイルの拡張子を取得
    extension = original_filename.rsplit('.', 1)[1].lower() if '.' in original_filename else ''

    # タイムスタンプとUUIDを組み合わせた一意のファイル名を生成
    timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
    unique_id = str(uuid.uuid4())[:8]

    if extension:
        return f"image_{timestamp}_{unique_id}.{extension}"
    else:
        return f"file_{timestamp}_{unique_id}"

@app.route('/upload', methods=['POST'])
def upload_file():
    if 'image' not in request.files:
        flash('ファイルが選択されていません')
        return redirect(request.url)

    file = request.files['image']

    if file.filename == '':
        flash('ファイルが選択されていません')
        return redirect(request.url)

    # バリデーションの実施
    if not file or not allowed_file(file.filename):
        flash('許可されていないファイル形式です')
        return redirect(request.url)

    if not is_valid_image(file.stream):
        flash('無効な画像ファイルです')
        return redirect(request.url)

    # 安全なファイル名の生成
    safe_filename = generate_safe_filename(file.filename)
    file_path = os.path.join(UPLOAD_DIR, safe_filename)

    try:
        # 画像のリサイズや最適化(オプション)
        image = Image.open(file.stream)

        # 必要に応じて画像をリサイズ
        if image.size[0] > 1200 or image.size[1] > 1200:
            image.thumbnail((1200, 1200), Image.Resampling.LANCZOS)

        # 最適化して保存
        image.save(file_path, optimize=True, quality=85)

        # アップロードされたファイルの情報をログに記録(実際のアプリではデータベースに保存)
        file_info = {
            'original_name': file.filename,
            'saved_name': safe_filename,
            'file_size': os.path.getsize(file_path),
            'upload_time': datetime.now().isoformat(),
            'content_type': file.content_type
        }

        flash(f'ファイル {file.filename} のアップロードに成功しました(保存名: {safe_filename})')

    except Exception as e:
        flash(f'ファイルの処理中にエラーが発生しました: {str(e)}')
        return redirect(request.url)

    return redirect(url_for('index'))

@app.route('/uploads/')
def uploaded_file(filename):
    """アップロードされたファイルへの安全なアクセスを提供"""
    return send_from_directory(UPLOAD_DIR, filename)

@app.route('/')
def index():
    return render_template('upload_secure.html')

if __name__ == '__main__':
    app.run(debug=True)

この安全なファイル保存の実装では、以下の重要な対策を講じています。

ファイル名の一意性確保のために、タイムスタンプとUUIDを組み合わせた命名規則を採用しています。これにより、ファイル名の衝突を防ぎ、予測困難なファイル名を生成します。

画像の最適化処理では、Pillowライブラリを使用して画像のリサイズと画質調整を行います。これにより、ストレージの節約と表示パフォーマンスの向上を図ります。

ファイルへのアクセス制御では、send_from_directory関数を使用して、適切なディレクトリからのみファイルを提供します。これにより、ディレクトリトラバーサル攻撃を防ぎます。

アップロードディレクトリの配置にも注意が必要です。静的ファイル用のディレクトリとは別の場所に設定し、必要に応じて.htaccessファイル(Apacheの場合)やサーバー設定で直接アクセスを制限します。

実際のプロダクション環境では、さらに以下のような対策を追加することをお勧めします。

クラウドストレージサービスの利用(AWS S3、Google Cloud Storageなど)
定期的なウイルススキャンの実施
ファイルのアクセスログの監視
アップロード制限のIPベースでの制御

まとめ

Flaskを使用した安全なファイルアップロード機能の実装について、基本的な概念から高度なセキュリティ対策まで詳しく解説しました。適切なバリデーション、安全なファイル保存、適切なエラーハンドリングの重要性を理解することが、堅牢なアプリケーション開発につながります。

実際のプロジェクトでは、ここで紹介した基本技術を土台として、プロジェクトの要件に合わせたカスタマイズを行うことが求められます。特に、大規模なアプリケーションでは、非同期処理によるファイル処理、進捗状況の表示、クラウドストレージとの連携などの機能追加を検討する必要があります。

ファイルアップロード機能は便利である一方、適切に実装しないと重大なセキュリティホールとなる可能性があります。常に最新のセキュリティプラクティスを学び、定期的なコードレビューとテストを実施することで、安全で信頼性の高いアプリケーションを構築してください。

演習問題

初級問題

問題1: 基本的なファイルアップロードフォーム

画像ファイルをアップロードする基本的なFlaskアプリケーションを作成してください。アップロードフォームを表示するルートと、ファイルを受け取ってuploadsフォルダに保存するルートを実装しましょう。

問題2: ファイル拡張子のバリデーション

アップロードされるファイルが画像ファイル(png, jpg, jpeg, gif)のみを受け付けるバリデーション機能を追加してください。許可されていない形式のファイルがアップロードされた場合はエラーメッセージを表示しましょう。

問題3: 安全なファイル名の処理

アップロードされたファイルのファイル名を安全な形式に変換して保存する機能を実装してください。werkzeugのsecure_filename関数を使用し、ファイル名の重複を防ぐための処理も追加しましょう。

中級問題

問題4: ファイルサイズ制限の実装

アップロードされるファイルのサイズを最大2MBに制限する機能を追加してください。制限を超えるファイルがアップロードされた場合は適切なエラーメッセージを表示しましょう。

問題5: 画像ファイルの実際の検証

ファイルの拡張子に加えて、実際に画像ファイルとして開けるかどうかを検証する機能を実装してください。Pillowライブラリを使用して画像ファイルの整合性をチェックしましょう。

問題6: 複数ファイルアップロード

一度に複数のファイルをアップロードできる機能を実装してください。HTMLフォームでmultiple属性を設定し、Flask側で複数ファイルを処理するロジックを追加しましょう。

問題7: アップロード進捗表示

ファイルアップロードの進捗状況を表示する機能を追加してください。JavaScriptを使用してアップロードの進捗を表示するUIを実装しましょう。

問題8: 画像のプレビュー機能

アップロード前に選択した画像をプレビュー表示する機能を実装してください。JavaScriptのFileReader APIを使用してクライアント側でプレビューを表示しましょう。

問題9: アップロードファイルの一覧表示

アップロードされたファイルの一覧を表示するページを作成してください。uploadsフォルダ内のファイルをリスト表示し、各ファイルへのリンクを提供しましょう。

上級問題

問題10: 画像の自動リサイズ機能

アップロードされた画像が一定サイズ(例: 1200×1200ピクセル)を超える場合、自動的にリサイズして保存する機能を実装してください。Pillowライブラリを使用してアスペクト比を維持したリサイズを行いましょう。

問題11: クラウドストレージ連携

ローカルストレージの代わりにクラウドストレージ(AWS S3など)にファイルをアップロードする機能を実装してください。boto3ライブラリを使用してS3との連携を行いましょう。

問題12: 高度なセキュリティ対策

ファイルアップロード機能に対する包括的なセキュリティ対策を実装してください。コンテンツタイプの検証、EXIFデータの削除、ウイルススキャン(模擬)などの多層的な防御策を追加しましょう。

演習問題 解答例

初級問題解答例(3問)

問題1解答例

from flask import Flask, request, render_template
import os

app = Flask(__name__)
app.config['UPLOAD_FOLDER'] = 'uploads'

@app.route('/')
def index():
    return render_template('upload.html')

@app.route('/upload', methods=['POST'])
def upload_file():
    if 'file' not in request.files:
        return 'ファイルが選択されていません'

    file = request.files['file']

    if file.filename == '':
        return 'ファイルが選択されていません'

    if file:
        filename = file.filename
        file.save(os.path.join(app.config['UPLOAD_FOLDER'], filename))
        return f'ファイル {filename} のアップロードに成功しました'

if __name__ == '__main__':
    if not os.path.exists('uploads'):
        os.makedirs('uploads')
    app.run(debug=True)

問題2解答例

from flask import Flask, request, render_template, flash, redirect, url_for
import os

app = Flask(__name__)
app.config['UPLOAD_FOLDER'] = 'uploads'
app.secret_key = 'secret'

ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif'}

def allowed_file(filename):
    return '.' in filename and \
           filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS

@app.route('/')
def index():
    return render_template('upload_with_validation.html')

@app.route('/upload', methods=['POST'])
def upload_file():
    if 'file' not in request.files:
        flash('ファイルが選択されていません')
        return redirect(request.url)

    file = request.files['file']

    if file.filename == '':
        flash('ファイルが選択されていません')
        return redirect(request.url)

    if not allowed_file(file.filename):
        flash('許可されていないファイル形式です')
        return redirect(request.url)

    if file and allowed_file(file.filename):
        filename = file.filename
        file.save(os.path.join(app.config['UPLOAD_FOLDER'], filename))
        flash('ファイルのアップロードに成功しました')
        return redirect(url_for('index'))

if __name__ == '__main__':
    if not os.path.exists('uploads'):
        os.makedirs('uploads')
    app.run(debug=True)

問題3解答例

from flask import Flask, request, render_template, flash, redirect, url_for
from werkzeug.utils import secure_filename
import os

app = Flask(__name__)
app.config['UPLOAD_FOLDER'] = 'uploads'
app.secret_key = 'secret'

ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif'}

def allowed_file(filename):
    return '.' in filename and \
           filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS

@app.route('/')
def index():
    return render_template('upload_secure.html')

@app.route('/upload', methods=['POST'])
def upload_file():
    if 'file' not in request.files:
        flash('ファイルが選択されていません')
        return redirect(request.url)

    file = request.files['file']

    if file.filename == '':
        flash('ファイルが選択されていません')
        return redirect(request.url)

    if not allowed_file(file.filename):
        flash('許可されていないファイル形式です')
        return redirect(request.url)

    if file and allowed_file(file.filename):
        filename = secure_filename(file.filename)

        # 重複防止
        base_name, extension = os.path.splitext(filename)
        counter = 1
        while os.path.exists(os.path.join(app.config['UPLOAD_FOLDER'], filename)):
            filename = f"{base_name}_{counter}{extension}"
            counter += 1

        file.save(os.path.join(app.config['UPLOAD_FOLDER'], filename))
        flash(f'ファイルのアップロードに成功しました(保存名: {filename})')
        return redirect(url_for('index'))

if __name__ == '__main__':
    if not os.path.exists('uploads'):
        os.makedirs('uploads')
    app.run(debug=True)

中級問題解答例(6問)

問題4解答例

from flask import Flask, request, render_template, flash, redirect, url_for
from werkzeug.utils import secure_filename
import os

app = Flask(__name__)
app.config['UPLOAD_FOLDER'] = 'uploads'
app.config['MAX_CONTENT_LENGTH'] = 2 * 1024 * 1024  # 2MB
app.secret_key = 'secret'

ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif'}

def allowed_file(filename):
    return '.' in filename and \
           filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS

@app.errorhandler(413)
def too_large(e):
    flash('ファイルサイズが大きすぎます。2MB以下のファイルをアップロードしてください。')
    return redirect(request.url)

@app.route('/')
def index():
    return render_template('upload_with_size_limit.html')

@app.route('/upload', methods=['POST'])
def upload_file():
    if 'file' not in request.files:
        flash('ファイルが選択されていません')
        return redirect(request.url)

    file = request.files['file']

    if file.filename == '':
        flash('ファイルが選択されていません')
        return redirect(request.url)

    if not allowed_file(file.filename):
        flash('許可されていないファイル形式です')
        return redirect(request.url)

    if file and allowed_file(file.filename):
        filename = secure_filename(file.filename)

        base_name, extension = os.path.splitext(filename)
        counter = 1
        while os.path.exists(os.path.join(app.config['UPLOAD_FOLDER'], filename)):
            filename = f"{base_name}_{counter}{extension}"
            counter += 1

        file.save(os.path.join(app.config['UPLOAD_FOLDER'], filename))
        flash('ファイルのアップロードに成功しました')
        return redirect(url_for('index'))

if __name__ == '__main__':
    if not os.path.exists('uploads'):
        os.makedirs('uploads')
    app.run(debug=True)

問題5解答例

from flask import Flask, request, render_template, flash, redirect, url_for
from werkzeug.utils import secure_filename
from PIL import Image
import os

app = Flask(__name__)
app.config['UPLOAD_FOLDER'] = 'uploads'
app.config['MAX_CONTENT_LENGTH'] = 2 * 1024 * 1024
app.secret_key = 'secret'

ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif'}

def allowed_file(filename):
    return '.' in filename and \
           filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS

def is_valid_image(file_stream):
    try:
        image = Image.open(file_stream)
        image.verify()
        file_stream.seek(0)
        return True
    except Exception:
        return False

@app.route('/')
def index():
    return render_template('upload_image_validation.html')

@app.route('/upload', methods=['POST'])
def upload_file():
    if 'file' not in request.files:
        flash('ファイルが選択されていません')
        return redirect(request.url)

    file = request.files['file']

    if file.filename == '':
        flash('ファイルが選択されていません')
        return redirect(request.url)

    if not allowed_file(file.filename):
        flash('許可されていないファイル形式です')
        return redirect(request.url)

    if not is_valid_image(file.stream):
        flash('無効な画像ファイルです')
        return redirect(request.url)

    if file and allowed_file(file.filename):
        filename = secure_filename(file.filename)

        base_name, extension = os.path.splitext(filename)
        counter = 1
        while os.path.exists(os.path.join(app.config['UPLOAD_FOLDER'], filename)):
            filename = f"{base_name}_{counter}{extension}"
            counter += 1

        file.save(os.path.join(app.config['UPLOAD_FOLDER'], filename))
        flash('画像ファイルのアップロードに成功しました')
        return redirect(url_for('index'))

if __name__ == '__main__':
    if not os.path.exists('uploads'):
        os.makedirs('uploads')
    app.run(debug=True)

問題6解答例

from flask import Flask, request, render_template, flash, redirect, url_for
from werkzeug.utils import secure_filename
from PIL import Image
import os

app = Flask(__name__)
app.config['UPLOAD_FOLDER'] = 'uploads'
app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024  # 16MB for multiple files
app.secret_key = 'secret'

ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif'}

def allowed_file(filename):
    return '.' in filename and \
           filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS

def is_valid_image(file_stream):
    try:
        image = Image.open(file_stream)
        image.verify()
        file_stream.seek(0)
        return True
    except Exception:
        return False

@app.route('/')
def index():
    return render_template('upload_multiple.html')

@app.route('/upload', methods=['POST'])
def upload_files():
    if 'files' not in request.files:
        flash('ファイルが選択されていません')
        return redirect(request.url)

    files = request.files.getlist('files')
    success_count = 0

    for file in files:
        if file.filename == '':
            continue

        if not allowed_file(file.filename):
            flash(f'{file.filename} は許可されていないファイル形式です')
            continue

        if not is_valid_image(file.stream):
            flash(f'{file.filename} は無効な画像ファイルです')
            continue

        if file and allowed_file(file.filename):
            filename = secure_filename(file.filename)

            base_name, extension = os.path.splitext(filename)
            counter = 1
            while os.path.exists(os.path.join(app.config['UPLOAD_FOLDER'], filename)):
                filename = f"{base_name}_{counter}{extension}"
                counter += 1

            file.save(os.path.join(app.config['UPLOAD_FOLDER'], filename))
            success_count += 1

    flash(f'{success_count} 個のファイルのアップロードに成功しました')
    return redirect(url_for('index'))

if __name__ == '__main__':
    if not os.path.exists('uploads'):
        os.makedirs('uploads')
    app.run(debug=True)

上級問題解答例(3問)

問題10解答例

from flask import Flask, request, render_template, flash, redirect, url_for
from werkzeug.utils import secure_filename
from PIL import Image
import os

app = Flask(__name__)
app.config['UPLOAD_FOLDER'] = 'uploads'
app.config['MAX_CONTENT_LENGTH'] = 2 * 1024 * 1024
app.secret_key = 'secret'

ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif'}

def allowed_file(filename):
    return '.' in filename and \
           filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS

def is_valid_image(file_stream):
    try:
        image = Image.open(file_stream)
        image.verify()
        file_stream.seek(0)
        return True
    except Exception:
        return False

def resize_image(image, max_size=(1200, 1200)):
    """画像を指定された最大サイズ以内にリサイズ"""
    if image.size[0] > max_size[0] or image.size[1] > max_size[1]:
        image.thumbnail(max_size, Image.Resampling.LANCZOS)
    return image

@app.route('/')
def index():
    return render_template('upload_resize.html')

@app.route('/upload', methods=['POST'])
def upload_file():
    if 'file' not in request.files:
        flash('ファイルが選択されていません')
        return redirect(request.url)

    file = request.files['file']

    if file.filename == '':
        flash('ファイルが選択されていません')
        return redirect(request.url)

    if not allowed_file(file.filename):
        flash('許可されていないファイル形式です')
        return redirect(request.url)

    if not is_valid_image(file.stream):
        flash('無効な画像ファイルです')
        return redirect(request.url)

    if file and allowed_file(file.filename):
        filename = secure_filename(file.filename)

        base_name, extension = os.path.splitext(filename)
        counter = 1
        while os.path.exists(os.path.join(app.config['UPLOAD_FOLDER'], filename)):
            filename = f"{base_name}_{counter}{extension}"
            counter += 1

        # 画像のリサイズと保存
        image = Image.open(file.stream)
        resized_image = resize_image(image)
        resized_image.save(os.path.join(app.config['UPLOAD_FOLDER'], filename), 
                          optimize=True, quality=85)

        original_size = image.size
        new_size = resized_image.size

        flash(f'画像のアップロードに成功しました({original_size} → {new_size} にリサイズ)')
        return redirect(url_for('index'))

if __name__ == '__main__':
    if not os.path.exists('uploads'):
        os.makedirs('uploads')
    app.run(debug=True)

問題11解答例

import boto3
from flask import Flask, request, render_template, flash, redirect, url_for
from werkzeug.utils import secure_filename
from PIL import Image
import os

app = Flask(__name__)
app.secret_key = 'secret'

ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif'}

# AWS S3設定
S3_BUCKET = 'your-bucket-name'
S3_REGION = 'your-region'

s3_client = boto3.client(
    's3',
    aws_access_key_id='your-access-key',
    aws_secret_access_key='your-secret-key',
    region_name=S3_REGION
)

def allowed_file(filename):
    return '.' in filename and \
           filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS

def is_valid_image(file_stream):
    try:
        image = Image.open(file_stream)
        image.verify()
        file_stream.seek(0)
        return True
    except Exception:
        return False

def upload_to_s3(file, filename):
    """ファイルをS3にアップロード"""
    try:
        s3_client.upload_fileobj(
            file,
            S3_BUCKET,
            filename,
            ExtraArgs={
                'ContentType': file.content_type,
                'ACL': 'public-read'
            }
        )
        return f"https://{S3_BUCKET}.s3.{S3_REGION}.amazonaws.com/{filename}"
    except Exception as e:
        raise Exception(f"S3アップロードエラー: {str(e)}")

@app.route('/')
def index():
    return render_template('upload_s3.html')

@app.route('/upload', methods=['POST'])
def upload_file():
    if 'file' not in request.files:
        flash('ファイルが選択されていません')
        return redirect(request.url)

    file = request.files['file']

    if file.filename == '':
        flash('ファイルが選択されていません')
        return redirect(request.url)

    if not allowed_file(file.filename):
        flash('許可されていないファイル形式です')
        return redirect(request.url)

    if not is_valid_image(file.stream):
        flash('無効な画像ファイルです')
        return redirect(request.url)

    if file and allowed_file(file.filename):
        filename = secure_filename(file.filename)

        try:
            file_url = upload_to_s3(file, filename)
            flash(f'ファイルのアップロードに成功しました: {file_url}')
        except Exception as e:
            flash(f'アップロードに失敗しました: {str(e)}')

        return redirect(url_for('index'))

if __name__ == '__main__':
    app.run(debug=True)

問題12解答例

from flask import Flask, request, render_template, flash, redirect, url_for
from werkzeug.utils import secure_filename
from PIL import Image, ExifTags
import os
import magic

app = Flask(__name__)
app.config['UPLOAD_FOLDER'] = 'uploads'
app.config['MAX_CONTENT_LENGTH'] = 2 * 1024 * 1024
app.secret_key = 'secret'

ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif'}
ALLOWED_MIME_TYPES = {'image/jpeg', 'image/png', 'image/gif'}

def allowed_file(filename):
    return '.' in filename and \
           filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS

def validate_file_content(file_stream):
    """ファイルの実際のコンテンツを検証"""
    # MIMEタイプの検証
    mime = magic.Magic(mime=True)
    file_stream.seek(0)
    detected_mime = mime.from_buffer(file_stream.read(1024))
    file_stream.seek(0)

    if detected_mime not in ALLOWED_MIME_TYPES:
        return False, f"許可されていないMIMEタイプです: {detected_mime}"

    # 画像ファイルの検証
    try:
        image = Image.open(file_stream)
        image.verify()
        file_stream.seek(0)
        return True, "検証成功"
    except Exception as e:
        return False, f"画像ファイルの検証に失敗しました: {str(e)}"

def remove_exif_data(image):
    """EXIFデータを削除"""
    data = list(image.getdata())
    image_without_exif = Image.new(image.mode, image.size)
    image_without_exif.putdata(data)
    return image_without_exif

def simulate_virus_scan(file_path):
    """ウイルススキャンの模擬(実際のプロダクションではClamAVなどを使用)"""
    # 実際の環境ではClamAVなどのウイルススキャンエンジンを統合
    import hashlib
    with open(file_path, 'rb') as f:
        file_hash = hashlib.md5(f.read()).hexdigest()

    # 既知の悪意のあるファイルのハッシュ(模擬)
    known_malicious_hashes = {
        "d41d8cd98f00b204e9800998ecf8427e": "テスト用の悪意のあるファイル"
    }

    if file_hash in known_malicious_hashes:
        return False, known_malicious_hashes[file_hash]
    return True, "スキャン完了"

@app.route('/')
def index():
    return render_template('upload_secure_advanced.html')

@app.route('/upload', methods=['POST'])
def upload_file():
    if 'file' not in request.files:
        flash('ファイルが選択されていません')
        return redirect(request.url)

    file = request.files['file']

    if file.filename == '':
        flash('ファイルが選択されていません')
        return redirect(request.url)

    if not allowed_file(file.filename):
        flash('許可されていないファイル形式です')
        return redirect(request.url)

    # コンテンツ検証
    is_valid, message = validate_file_content(file.stream)
    if not is_valid:
        flash(message)
        return redirect(request.url)

    if file and allowed_file(file.filename):
        filename = secure_filename(file.filename)

        base_name, extension = os.path.splitext(filename)
        counter = 1
        while os.path.exists(os.path.join(app.config['UPLOAD_FOLDER'], filename)):
            filename = f"{base_name}_{counter}{extension}"
            counter += 1

        file_path = os.path.join(app.config['UPLOAD_FOLDER'], filename)

        try:
            # 一時的に保存してウイルススキャン
            file.save(file_path)

            # 模擬ウイルススキャン
            scan_ok, scan_message = simulate_virus_scan(file_path)
            if not scan_ok:
                os.remove(file_path)
                flash(f'ウイルススキャンで問題が検出されました: {scan_message}')
                return redirect(request.url)

            # EXIFデータの削除と画像処理
            image = Image.open(file_path)
            clean_image = remove_exif_data(image)
            clean_image.save(file_path, optimize=True, quality=85)

            flash('ファイルのアップロードとセキュリティ処理が完了しました')

        except Exception as e:
            if os.path.exists(file_path):
                os.remove(file_path)
            flash(f'処理中にエラーが発生しました: {str(e)}')

        return redirect(url_for('index'))

if __name__ == '__main__':
    if not os.path.exists('uploads'):
        os.makedirs('uploads')
    app.run(debug=True)