Skip to main content

Python Flaskアプリケーションを安全にする方法

著者
Headshot of Gourav Singh Bais

Gourav Singh Bais

blog feature playwright tests

2024年5月21日

0 分で読めます

Flaskは、Python向けの強力で軽量、かつ柔軟なウェブフレームワークです。定型コードを最小限に抑えながら、開発者がウェブアプリケーションをすばやく開発できるよう設計されています。追加のライブラリやツールを必要としない独立したマイクロフレームワークで、データベース抽象化レイヤーもありません。

Flaskにはルーティング、テンプレートのレンダリング、リクエスト処理などの機能があり、シンプルさと柔軟性で知られるWeb Server Gateway Interface(WSGI)標準に準拠しています。

残念ながら、ほかのウェブフレームワークと同様、適切に保護されていないFlaskには脆弱性が生じる可能性があります。Flaskで最も一般的なセキュリティリスクには、クロスサイトスクリプティング(XSS)、クロスサイトリクエストフォージェリ(CSRF)、SQLインジェクションがあります。

Snyk Vulnerability Databaseでは、PythonのFlaskライブラリで見つかった数多くのセキュリティ脆弱性を確認できます。

Snyk Vulnerability Databaseで見つかった、PythonのFlaskライブラリに関する数多くのセキュリティ脆弱性の一部。

この記事では、Flaskウェブアプリケーションフレームワークで構築したPythonアプリケーションを安全にするためのベストプラクティスを紹介します。まず、安全でない設定例を確認し、問題を緩和して修正する方法を学びます。

Web Server Gateway Interface(WSGI)標準とは何ですか?

WSGIは、WebサーバーがPythonで記述されたWebアプリケーションと標準化された方法で通信できるようにします。WebサーバーからWebアプリケーションやフレームワークへリクエストを転送し、クライアントにレスポンスを返すための仲介レイヤーです。

WSGIが重要なのはなぜですか?

WSGIが登場する前は、PythonのWebフレームワークとサーバーは特定の組み合わせに依存していることが多く、特定のWebフレームワークは限られたWebサーバーでしか動作しませんでした。WSGIはこの制約を解消し、柔軟性と相互運用性を高めました。

WSGIは、他の言語における類似の概念とどのように異なりますか?

WSGIは、JavaのJava Servlet APIやRubyのRackと目的が似ています。これらの技術はHTTPリクエストとレスポンスの詳細を抽象化し、開発者が基盤となるサーバーとの通信を処理するのではなく、Webアプリケーションのロジックの実装に集中できるようにします。

Python Flaskアプリケーションの安全でない設定

以下は、Python Flaskアプリケーションに影響する一般的な安全でない設定の問題です。開発者は、コードベースで同じ間違いを繰り返さないよう十分に注意する必要があります。

シークレットキーの露出

Flaskのシークレットキー、APIキー、パスワードなどの機密情報をソースコードに含めてしまうのは、よくあるセキュリティ上の不備です。機密情報をソースコードに直接埋め込むと、悪意のある攻撃者がこの脆弱性を悪用して、さまざまなサービスに不正アクセスするおそれがあり、大きなセキュリティリスクとなります。

シークレットキーとAPIキーを誤ってソースコードにハードコードしてしまうと、次のようになります。

# Insecure Code
api_key = "my_sensitive_api_key"
api_url = "https://api.example.com"
SEC_KEY = "your_secret_key"

# use of secret key
app.config["SECRET_KEY"] = SEC_KEY
# usage of the API key
response = requests.get(api_url, headers={"Authorization": f"Bearer {api_key}"})

ヒント:この機会にIDEにSnyk拡張機能をインストールしてみましょう。エディターに表示されている青い波線のように、シークレットキーの露出を視覚的に確認できます。

Snyk IDE拡張機能で、機密性の高いAPIキーの漏えいや認証情報の流出など、コードのセキュリティ上の問題が表示されています。

この場合、シークレットキーの露出を防ぐには、キーを安全に生成する必要があります。生成方法はいくつかありますが、よく使われる方法の1つはuuidモジュールを使うことです。uuidモジュールは、主にRFC 4122で定義された原則に基づいて、汎用一意識別子(UUID)を生成するために使われます。このモジュールは、シークレットキーとして使用できる32バイトのランダムな16進数文字列を生成します。

安全なキーを生成するには、コードでuuidモジュールをインポートし、uuid4().hex機能を使用します。

import uuid

uuid.uuid4().hex

ただし、シークレットキーを生成するだけでは不十分です。Flaskアプリで安全に使用する必要もあります。推奨される方法は、シークレットキーをenvironment variableに設定するか、暗号化した設定ファイルに保存することです。シークレットキーをenvironment variableに設定する場合は、次のようになります。

import os

# Better Code
api_key = os.environ.get("MY_API_KEY")
api_url = "https://api.example.com"
SEC_KEY = os.environ.get("MY_SEC_KEY")

# use of secret key
app.config["SECRET_KEY"] = SEC_KEY

# Usage of the API key
response = requests.get(api_url, headers={"Authorization": f"Bearer {api_key}"})

開発者は、認証情報を環境変数に保存して、データ漏えい、不正アクセス、悪用の可能性を防ぐため、12 factor appで推奨されているような設定規約に従う必要があります。

デバッグモードが有効

デバッグモードは通常、開発環境で使用するもので、詳細なエラーメッセージやアプリケーションの状態に関する包括的な情報を提供します。しかし、本番環境でこのモードを有効にすると、機密情報やシステムの内部動作が露出します。

たとえば、このFlaskウェブアプリケーションでは、本番環境でデバッグモードが有効になっています。

# Insecure code
from flask import Flask
appFlask = Flask(__name__)

@appFlask.route('/home')
def home():
    result = "Testing Snyk"
    return 'example of debug mode enabled'

if __name__ == "__main__":
    appFlask.run(debug=True)

ここではデバッグモードがTrueに設定されています。そのため、本番環境でエラーが発生すると、ユーザー名やパスワードなどの機密情報がデバッグコンソールに表示されるおそれがあります。

このような場合、デバッグモードをTrueに設定したことを忘れたまま、本番環境へのデプロイ前に変更し忘れてしまうのは簡単です。そのため、コードやアプリケーションの脆弱性を発見し、修正するのに役立つSnykのようなプラットフォームを利用するのがおすすめです。SnykはPython、Go、PHP、JavaScriptなど、幅広いプログラミング言語に対応しています。

さらに、SnykはVisual Studio CodeやPyCharmなどの各種IDEのほか、Jenkins、CircleCI、MavenなどのCIパイプラインやワークフローにも簡単に統合できます。

たとえば、このデバッグのシナリオでSnykを使うには、Visual Studio Codeで「Snyk」を検索してインストールするだけです。

VS Code向けSnyk Security拡張機能。

インストールすると、Snykが左側のパネルに表示されます。まだSnykアカウントをお持ちでない場合は、Connectボタンをクリックしてログインするか、アカウントを作成してください。すると、次のようなブラウザーウィンドウが表示されます。

IDE拡張機能をSnykアプリケーションに接続します。

接続したらVisual Studio Codeに戻り、Flaskスクリプトを保存しているフォルダーを開きます。コード内の脆弱性が表示されます。

Snyk拡張機能をインストールしたVS Code IDEで、脆弱性やコードのセキュリティ上の問題が表示されているFlask製Pythonアプリケーション。

ご覧のとおり、Snykはデバッグモードがまだ有効であることを検出しました。この問題を解決するには、デバッグモードをFalseに設定してFlaskアプリケーションを安全にします。

# Secure Code
from flask import Flask
appFlask = Flask(__name__)

@appFlask.route('/home')
def home():
    result = "Testing Snyk"
    return 'example of debug mode enabled'

if __name__ == "__main__":
    appFlask.run(debug=False)

本番環境でデバッグモードを無効にするだけでなく、ロギング機能を使ってアプリケーションのログを収集し、コードに問題がないか確認する必要もあります。

設定ファイル内の機密データが保護されていない

Flaskアプリケーションでは、データベースの認証情報、APIキー、暗号化キーなどの重要な情報を設定ファイルに保存するのが一般的です。しかし、ファイルを保護せずに置いておくと、データが漏えいするおそれがあります。

設定ファイル(通常はconfig.ini)にデータベース接続用の認証情報が保存されている、次の例を見てみましょう。

# Insecure Config
[database]
username = admin
password = my_secure_password

ここではユーザー名とパスワードが平文で記載されています。攻撃者がこのファイルにアクセスすると、認証情報を使って機密情報にアクセスできてしまいます。

この問題を解決する最善の方法は、設定ファイルを使わず、環境変数に認証情報を保存することです。必要な情報を.bash_profileなどの隠しファイルにまとめて保存する環境変数を使えば、このファイルをgitignoreに追加して、攻撃者が容易にアクセスできるGitやBitbucketなどのバージョン管理プラットフォームに誤ってプッシュされないようにできます。

Flaskアプリで設定ファイルを使う必要がある場合は、機密データが暗号化されていることを確認してください。

# Secure Config
[database]
username = admin
password = $encrypted_password

ここでは、パスワードを表すためにプレースホルダーまたは暗号化された値を使用しています。この場合、外部の安全な鍵管理システムや安全な復号手法を使って、実行時に機密データにアクセスできます。

Flaskアプリケーションの脆弱性

Python Flaskアプリケーションで安全でない設定がどのようなものかを理解したところで、Flaskアプリケーションにも影響する一般的なウェブセキュリティの脆弱性を見ていきましょう。

クロスサイトスクリプティング(XSS)

XSSは、攻撃者が任意のJavaScriptまたはHTMLコードをウェブページに挿入しようとするウェブセキュリティ上の脆弱性です。これにより、攻撃者はユーザーのブラウザーで悪意のあるスクリプトを実行できます。

Flaskで使われるテンプレートエンジンのJinja2には、XSSの脆弱性に対処するための自動エスケープ機能があります。デフォルトではすべての変数が自動的にエスケープされます(つまり、明示的に指定しない限りHTML要素は含まれません)。そのため、HTMLやJavaScriptコードを含むユーザー入力はJinja2によって安全にエスケープされ、悪意のあるスクリプトの実行を防ぎます。

自動エスケープがFalseに設定されている場合、FlaskアプリケーションはXSSの脆弱性にさらされます。たとえば、ページの表示にユーザー入力が必要なウェブアプリケーションを考えてみましょう。

from flask import Flask, render_template, request

app = Flask(__name__)

@app.route('/')
def index():
    user_input = request.args.get('input')
    return render_template('index.html', input=user_input)

自動エスケープがfalseに設定された対応するindex.htmlページは、次のようになります。

<!DOCTYPE html>
{% autoescape false %}
<html>
<head>
    <title>XSS Example</title>
</head>
<body>
    <p>User Input: {{ input }}</p>
</body>
{% endautoescape %}
</html>

攻撃者がinputパラメーターを通じて悪意のあるスクリプトを挿入すると、ページの表示時に実行され、データ窃取、アカウント乗っ取り、フィッシング攻撃などのセキュリティリスクにつながるおそれがあります。

XSSを防ぐには、テキストを適切にエスケープし、HTML要素が含まれないようにウェブサイトを設定する必要があります。自動エスケープをtrueに設定するだけで、FlaskアプリケーションへのXSS攻撃を防げます。

<!DOCTYPE html>
{% autoescape true %}
<html>
<head>
    <title>XSS Example</title>
</head>
<body>
    <p>User Input: {{ input }}</p>
</body>
{% endautoescape %}
</html>

XSSを防ぐもう1つの方法は、明示的なマークアップのエスケープです。これを実装するには、ウェブページで|safeフィルターを次のように使用します。

<!DOCTYPE html>
<html>
<head>
    <title>XSS Example</title>
</head>
<body>
    <p>User Input: {{ input|safe }}</p>
</body>
</html>

ただし、|safeを使う際は十分な注意が必要です。安全と指定したコンテンツに悪意のあるスクリプトが含まれていないことが前提となるためです。

Pythonのクロスサイトスクリプティングの脆弱性について詳しく知りたい方は、Snyk Learnの短時間で学べるセキュリティレッスンをご覧ください。

クロスサイトリクエストフォージェリ(CSRF)

CSRFは、被害者をだまして悪意のあるリクエストを送信させる攻撃です。ウェブアプリケーションにおけるCSRFでは、攻撃者が正規ユーザーになりすまして、ウェブページに不正なリクエストを送信します。たとえばFacebookのようなプラットフォームでは、グループに画像を送信し、グループのユーザーがその画像をクリックするとアカウントが削除される、といった攻撃が見られます。

CSRFをよりよく理解するために、次のようなCSRF保護のない、状態を変更するシンプルなアプリケーションを考えてみましょう。

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

app = Flask(__name__)

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

@app.route('/change_password', methods=['POST'])
def change_password():
    new_password = request.form.get('new_password')
    # Change password logic here
    return redirect(url_for('index'))

このコードは、POSTリクエストでフォームを送信してパスワードを変更します。CSRF対策がないため、攻撃者は細工したHTMLやスクリプトを含む別のウェブサイトにユーザーを誘導し、パスワードを変更させることができます。CSRF保護のないフォームは次のようになります。

<!DOCTYPE html>
<html>
<head>
    <title>CSRF Example</title>
</head>
<body>
    <form method="post" action="{{ url_for('change_password') }}">
   	 <label for="new_password">New Password:</label>
   	 <input type="password" name="new_password" required>
   	 <button type="submit">Change Password</button>
    </form>
</body>
</html>

CSRFの脆弱性を防ぐには、Flask-WTF拡張機能とCSRFProtectミドルウェアを使用します。Flask-WTFはWTFormsライブラリを組み込んだFlaskプラグインです。Flaskウェブアプリケーションのフォームを簡単に生成、管理するための便利な機能を提供します。

次のようにpipを使ってインストールできます。

pip install Flask-WTF

インストールしたら、CSRF保護のためにFlask-WTFを使ってFlaskアプリケーションを更新できます。

from flask import Flask, render_template, request, redirect, url_for
from flask_wtf.csrf import CSRFProtect

app = Flask(__name__)
app.config['SECRET_KEY'] = 'your_secret_key'  # Set a secret key for CSRF protection
csrf = CSRFProtect(app)

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

@app.route('/change_password', methods=['POST'])
def change_password():
    new_password = request.form.get('new_password')
    # Change password logic here
    return redirect(url_for('index'))

ここでは、CSRFProtectをFlaskアプリで初期化し、SECRET_KEYを設定しています。このキーはCSRFトークンの作成と検証に使用されます。

テンプレート(index.html)も忘れずに更新し、次のようにCSRFトークンを含めてください。

<!DOCTYPE html>
<html>
<head>
    <title>CSRF Example</title>
</head>
<body>
    <form method="post" action="{{ url_for('change_password') }}">
   	 {{ csrf_token() }}
   	 <label for="new_password">New Password:</label>
   	 <input type="password" name="new_password" required>
   	 <button type="submit">Change Password</button>
    </form>
</body>
</html>

このcsrf_token()関数は、CSRFトークンを含む非表示の入力フィールドを生成します。フォームの送信時にFlask-WTFがトークンを検証し、リクエストがCSRF攻撃でないことを確認します。

FSQLインジェクションとデータベースのセキュリティ

次に、データベースのセキュリティに関連するFlaskの問題をいくつか見てみましょう。

SQLインジェクション

SQLインジェクションは、SQLクエリで使用されるパラメーターを操作し、攻撃者がデータベース上で任意のSQLコードを実行する深刻な問題です。不正アクセス、データの改ざん、機密情報の漏えいにつながるおそれがあります。

Flaskアプリケーションでは、SQLクエリに使用する前に入力が適切にサニタイズされていないと、SQLインジェクション攻撃が発生する可能性があります。

この攻撃をよりよく理解するために、データベースからデータを取得するFlaskコードの例を見てみましょう。

from flask import Flask, request, render_template
import sqlite3

app = Flask(__name__)

@app.route('/login', methods=['POST'])
def login():
    username = request.form['username']
    password = request.form['password']

    # Vulnerable SQL query
    query = f"SELECT * FROM users WHERE username = '{username}' AND password = '{password}'"

    # Execute the query
    conn = sqlite3.connect('database.db')
    cursor = conn.cursor()
    cursor.execute(query)
    user = cursor.fetchone()
    conn.close()

    if user:
   	 return 'Login successful'
    else:
   	 return 'Login failed'

このクエリは、クエリ文字列からユーザー名とパスワードを取得します。攻撃者が' OR '1'='1'; -- のような入力を挿入すると、クエリは次のようになります。

SELECT * FROM users WHERE username = '' OR '1'='1'; --' AND password = '';

このクエリは常にTrueとなるため、攻撃者はログインしてデータに不正アクセスできてしまいます。

SQLインジェクションの影響を受けやすいコードを特定するのは困難ですが、Snykなら対応できます。Visual Studio CodeでSnykをすでに設定しているのであれば、スクリプトを開くだけで、SnykがSQLインジェクションの問題を自動検出します。

Snykは、ユーザー入力をSQLクエリに連結することで発生するSQLインジェクションの脆弱性について警告しています。

このSQLインジェクション攻撃を軽減する簡単な方法は、パラメーター化クエリまたはステートメントを使用することです。

Flaskでは、カーソルオブジェクトのexecuteメソッドを使ってパラメーターを安全にバインドすることもできます。パラメーター化クエリは次のようになります。

from flask import Flask, request, render_template
import sqlite3

app = Flask(__name__)

@app.route('/login', methods=['POST'])
def login():
    username = request.form['username']
    password = request.form['password']

    # Secure SQL query using parameterized query
    query = "SELECT * FROM users WHERE username = ? AND password = ?"

    # Execute the query with parameters
    conn = sqlite3.connect('database.db')
    cursor = conn.cursor()
    cursor.execute(query, (username, password))
    user = cursor.fetchone()
    conn.close()

    if user:
   	 return 'Login successful'
    else:
   	 return 'Login failed'

このコードでは?プレースホルダーを使用し、実際の値をexecuteメソッドにタプルとして渡します。これにより、ユーザー入力は実行可能なSQLコードではなくデータとして扱われます。

この種の攻撃を避けるため、ユーザー入力は必ず検証・サニタイズしてください。エスケープ文字、サードパーティ製ライブラリ、正規表現などの手法を使って入力をサニタイズできます。

SQLインジェクションについて詳しく解説した記事として、SQLインジェクション対策チートシート:攻撃を防ぐ8つのベストプラクティスをおすすめします。

認証と認可

認証と認可は、Webセキュリティの重要な要素です。認証や認可が侵害されると、さまざまなセキュリティ問題につながる可能性があります。次に、その具体例を見ていきましょう。

認証に関するセキュリティ問題

よくある認証関連のセキュリティ問題の一つがブルートフォース攻撃です。攻撃者がユーザー名とパスワードを何度も推測し、Webサイトやサービスへのアクセスを試みます。また、セッション固定攻撃もよくある問題です。これは、フィッシング攻撃などを通じて、攻撃者がセッションIDを既知の値に設定するものです。

ブルートフォース攻撃を軽減するには、セッションロックアウトの仕組みを実装する必要があります。この方法では、ログインに一定回数失敗すると、ユーザーをロックアウトします。

from flask import Flask, request, session

app = Flask(__name__)
app.config['MAX_LOGIN_ATTEMPTS'] = 3

@app.route('/login', methods=['POST'])
def login():
    username = request.form['username']
    password = request.form['password']

    # Check if the user is locked out
    if 'login_attempts' in session and session['login_attempts'] >= app.config['MAX_LOGIN_ATTEMPTS']:
        return 'Account locked. Please try again later.'

    # Validate the username and password
    # ...

    # Update login attempts
    if 'login_attempts' in session:
        session['login_attempts'] += 1
    else:
        session['login_attempts'] = 1

    # Authenticate the user
    # ...

このコードではMAX_LOGIN_ATTEMPTSを3に設定しています。つまり、認証情報を3回より多く間違えると、ユーザーはロックアウトされます。

login()メソッドではusernameとpasswordを取得し、ユーザーがロックアウトされているか確認します。ロックアウトされていなければ、ログイン試行を確認します。認証情報が正しければログインできます。間違っている場合はログイン試行回数を1増やし、ログインに成功するかロックアウトされるまで、この処理を繰り返します。

セッションロックアウトの仕組みに加えて、強力なパスワードポリシーを適用し、多要素認証(MFA)の導入も検討しましょう。

セッション固定の問題を軽減するには、ユーザーのログイン後、または一定時間が経過した後(自動失効)に、ユーザーがログインを継続していてもセッションIDを再生成します。

from flask import Flask, request, session

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

@app.route('/login', methods=['POST'])
def login():
    # Authenticate the user
    # ...

    # Regenerate session ID after successful login
    session.regenerate()
    # ...

このコードでは、ユーザーのログイン成功後にlogin()メソッドがregenerate() メソッドを使ってセッションIDを再生成します。

認可に関するセキュリティ問題

Webアプリのすべての機能が、すべてのユーザーを対象としているわけではありません(つまり、特定のユーザーが利用できる機能は限定されるべきです)。ユーザーに適切なロールが割り当てられていないと、不正アクセスが発生する可能性があります。そのため、ロールベースのアクセス制御(RBAC)が必要です。

認証済みユーザーがパラメーターやURLを操作して、許可されていないリソースにアクセスすることもあります。これは、しばしば安全でない直接オブジェクト参照(IDOR)と呼ばれます。詳しくは、Pythonで安全でない直接オブジェクト参照を見つけて修正する方法の記事をぜひご覧ください。

RBACやIDORの問題を解決するには、ユーザーに特定のロールを割り当て、そのロールに基づいてアクセス制御を適用する必要があります。例を見てみましょう。

from flask import Flask, request, session

app = Flask(__name__)

@app.route('/admin_dashboard')
def admin_dashboard():
    if 'role' in session and session['role'] == 'admin':
   	 # User has admin role and is authorized to access the admin dashboard
   	 # ...
   	 return 'Admin dashboard page'
    else:
   	 # User is not authorized
   	 return 'Unauthorized access'

このコードでは、admin_dashboard()メソッドが、アクセスロールがadminの場合に利用できる機能を実装しています。それ以外の場合はUnauthorized accessという警告を返します。

また、ユーザーが許可されたリソースだけにアクセスできるよう、アクセス制御を管理する必要があります。

from flask import Flask, request, session

app = Flask(__name__)

@app.route('/user_profile/<int:user_id>')
def user_profile(user_id):
    if 'user_id' in session and session['user_id'] == user_id:
   	 # User is authorized to access their own profile
   	 # ...
   	 return 'User profile page'
    else:
   	 # User is not authorized
   	 return 'Unauthorized access'

認証情報を平文で保存してはいけません。攻撃者に簡単に取得され、悪用されるおそれがあります。これを防ぐには、ハッシュ化とソルトを使ってユーザーの認証情報を保存します。

ハッシュ化では、パスワードを固定長の文字列に変換します。この処理を逆にたどって元のパスワードを得ることは、計算上ほぼ不可能です。ソルトとは、ハッシュ化の前に各パスワードへ追加する一意のランダムな値です。レインボーテーブル攻撃などの防止に役立ちます。

古いライブラリとセキュリティ更新

古いFlaskライブラリを使っていると、攻撃者に悪用される脆弱性が含まれている可能性があります。ライブラリでは、脆弱性に対処するためのセキュリティ更新やパッチが公開されることがよくあります。常に最新のパッケージを使うようにしてください。

最新のセキュリティパッチを適用するには、pipやcondaなどのツールを利用できます。さらに、Snykなどのツールでコード全体の依存関係とInfrastructure as Code(IaC)構成をスキャンできます。

もう一つのベストプラクティスは、requirements.txtファイルで依存関係のバージョンを固定することです。これにより、プロジェクトで使用するバージョンが明確になり、開発者は更新を管理し、互換性をテストできます。

ファイルアップロードの保護

FlaskのWebサイトで、誰でもあらゆる種類のファイルをアップロードできるようにすると、さまざまな問題が発生するおそれがあります。たとえば、攻撃者が悪意のあるファイルをサーバーにアップロードすると、既存ファイルを上書きされる可能性があるほか、サービス拒否(DoS)攻撃につながることがあります。また、実行可能ファイルによって、さまざまな種類のコンピューターウイルスが実行される可能性もあります。

以下は、あらゆるユーザーが任意のファイルをアップロードできるコードの例です。

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

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

@app.route('/upload', methods=['POST'])
def upload_file():
    file = request.files['file']
    filename = secure_filename(file.filename)
    file.save(os.path.join(app.config['UPLOAD_FOLDER'], filename))
    flash('File successfully uploaded')
    return redirect(url_for('uploaded_file', filename=filename))

このコードでは、upload_file()メソッドがユーザーから提供されたファイルを読み込み、uploadsフォルダーに保存します。

安全でないファイルを防ぐための最初の対策は、許可するファイル拡張子を定義することです。Flaskアプリでは、実行可能ファイルやスクリプトファイルを決して許可しないでください。

また、不正アクセスを防ぐため、ファイルは安全な場所に保存してください。

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

app = Flask(__name__)
app.config['UPLOAD_FOLDER'] = 'uploads'
app.config['ALLOWED_EXTENSIONS'] = {'txt', 'pdf', 'png', 'jpg', 'jpeg', 'gif'}

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

@app.route('/upload', methods=['POST'])
def upload_file():
    if 'file' not in request.files:
   	 flash('No file part')
   	 return redirect(request.url)

    file = request.files['file']

    if file.filename == '':
   	 flash('No selected file')
   	 return redirect(request.url)

    if file and allowed_file(file.filename):
   	 filename = secure_filename(file.filename)
   	 file.save(os.path.join(app.config['UPLOAD_FOLDER'], filename))
   	 flash('File successfully uploaded')
   	 return redirect(url_for('uploaded_file', filename=filename))
    else:
   	 flash('Invalid file type')
   	 return redirect(request.url)

ここでは、複数の条件を適用してファイル形式が正しいかどうか確認しています。

上書きを防ぎ、予測可能な命名規則によるリスクを抑えるために、ファイル名をランダム化することも有効なベストプラクティスです。

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

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

def generate_random_filename():
    return str(uuid.uuid4())

@app.route('/upload', methods=['POST'])
def upload_file():
    # ... (other code)
    if file and allowed_file(file.filename):
   	 filename = generate_random_filename()
   	 file.save(os.path.join(app.config['UPLOAD_FOLDER'], filename))
   	 # ... (other code)

エラー処理と情報漏えい

どのようなアプリケーションでも、エラー処理は重要です。本番環境でprint文を使ったり、デバッグモードを有効にしたりするのは避けましょう。不適切なログの仕組みを使うと、アプリケーションに関する重要な情報が漏れる可能性があります。

たとえば、本番環境で詳細なエラーメッセージを表示すると、アプリケーションの内部情報が漏れ、攻撃者が潜在的な脆弱性を特定しやすくなります。

OperationalError: (1045, "Access denied for user 'root'@'localhost' (using password: YES)")

この例では、Flaskアプリに関連付けられたデータベースの重要な情報が漏れています。

また、エラーレスポンスにトレースバック全体を表示すると、アプリケーションのコード構造が明らかになり、次の例のように脆弱性が露呈する可能性があります。

File "/path/to/app.py", line 42, in some_function
	result = 1 / 0
ZeroDivisionError: division by zero

エラー処理による情報漏えいを軽減するには、機密情報を公開せずに、一般的なエラーコード(例:404 Not Found、500 Internal Server Error)に対するカスタムエラーページを作成できます。そのためには、errorhandlerデコレーターを使ってカスタムエラーハンドラーを定義します。

from flask import Flask, render_template

app = Flask(__name__)

@app.errorhandler(404)
def page_not_found(error):
    return render_template('404.html'), 404

@app.errorhandler(500)
def internal_server_error(error):
    return render_template('500.html'), 500

さらに、例外やエラーを適切にログに記録しましょう。ログメッセージはデバッグに十分な詳細を含めつつ、機密情報を漏らさないようにする必要があります。

import logging
from flask import Flask

app = Flask(__name__)

logging.basicConfig(filename='error.log', level=logging.ERROR)

@app.route('/divide')
def divide_by_zero():
    try:
   	 result = 1 / 0
   	 return str(result)
    except Exception as e:
   	 app.logger.error(f"An error occurred: {str(e)}")
   	 return "Internal Server Error", 500

これらのベストプラクティスに従えば、堅牢で安全なFlaskアプリケーションを実現できます。

まとめ

Flaskは、小規模から中規模のWebアプリケーションで広く使われているWebフレームワークです。しかし、他のWeb開発ツールやフレームワークと同様、適切に扱わなければ問題が発生することがあります。こうした問題を理解するだけでなく、悪意ある攻撃者に悪用されないよう、対策を講じることが重要です。

この記事で取り上げたFlaskアプリケーションのセキュリティに関するトピックの実用例については、GitHubのこちらのコードリポジトリをご覧ください。

Snykを使えば、コードの脆弱性を簡単に見つけて軽減できます。Snykはさまざまなプログラミング言語に対応し、ツール、パイプライン、ワークフローにシームレスに統合できます。今すぐSnykを無料で始めましょう。

最後に、SnykのPythonセキュリティのベストプラクティス・チートシートをぜひご活用ください。また、無料のSnyk Learnサイトで、Pythonのアプリケーションセキュリティスキルを身につけましょう。

続きを読む

Blog

フロンティアモデルは脆弱性を発見した。攻撃者だけがエクスプロイトチェーンを見つけた。

静的解析で欠陥は見つかりましたが、ライブ攻撃テストで侵害につながる連鎖を実証できたのは唯一でした。Evo COS、Claude Security、Claude Code Securityを比較します。

feature insights context
Blog

自律型攻撃はすでに始まっている。防御もそのスピードに追いつかなければならない。

自律型攻撃者によって、防御に使える時間は短くなっています。継続的な検出、修復、検証、予防で、セキュリティチームが攻撃に歩調を合わせる方法をご紹介します。

Blog

AIコーディングエージェントが不適切なアクセス制御を繰り返し実装する理由

AIコーディングエージェントは、コンパイルが通りレビューも通過する一方で、あるテナントのデータを別のテナントに公開してしまう認可ロジックを生成することがあります。不適切なアクセス制御が検出しにくい理由と、その防止策をご紹介します。