説明
信頼できないデータが正規表現パターンの全体または一部として解釈されると、正規表現インジェクションが発生します。攻撃者は .*、選択(|)、グループ、量指定子などを挿入して、フィルターや入力検証の意味を変えることができます。
バックトラッキング方式のエンジンでは、問題のあるパターンを十分に長い文字列(通常はマッチしない文字列)に適用すると、CPU負荷が急増し、正規表現によるサービス拒否(ReDoS)を引き起こす場合もあります。メタ文字を含むだけでReDoSになるわけではなく、問題のあるパターンと、それを誘発する対象文字列の両方が必要です。
想定される影響
- 入力検証の回避: 挿入された構文によって、許可・拒否される文字列の範囲が変わるおそれがあります。
- サービス拒否: 過剰なバックトラッキングがワーカーのCPUを長時間占有する可能性があります。
- 運用コストの増加: 処理の蓄積により、遅延、処理能力の低下、オートスケール費用の増加につながる場合があります。
対処方法
- アプリケーションが管理する固定パターンを優先します。単純な包含・前方一致・後方一致には
in、startswith、endswithを使います。 - 信頼できない文字列をパターンのリテラル部分にする場合は、その部分を
re.escape(...)またはregex.escape(...)でエスケープします。re.escapeはsubやsubnの置換文字列用ではありません。 - 検索条件が決まっている場合は、完全な信頼できるパターンを有限の許可リストから選びます。
- ユーザーが正規表現全体を指定する必要がある場合は、構文、パターン長、対象文字列の長さを制限します。バックトラッキングを行わないエンジンや、エンジンが強制するマッチ処理のタイムアウトを使います。ただし、タイムアウトではパターンの意味の変更による検証回避は防げません。
- Python標準の
reAPIには、マッチ処理ごとのタイムアウトはありません。サードパーティーのregexはtimeout=をサポートします。HTTPリクエストのタイムアウトだけで、CPUを占有した正規表現の実行が止まるとは限りません。 - アプリケーションの固定パターンも、曖昧に入れ子になった量指定子など、過剰なバックトラッキングを起こす構造がないか確認します。
例
変更前
python
import re
from flask import Flask, request
app = Flask(__name__)
@app.get("/search")
def search():
pattern = request.args.get("p", "") # ユーザーがパターン全体を制御します
text = request.args.get("t", "")
return "hit" if re.search(pattern, text) else "no"
ユーザーがパターンの意味を直接制御できます。フィルターを回避する構文や、特定の対象文字列と組み合わせて過剰なバックトラッキングを起こすパターンを渡すこともできます。
変更後
python
from flask import Flask, request, abort
app = Flask(__name__)
MAX_TERM_LENGTH = 100
MAX_TEXT_LENGTH = 10_000
@app.get("/search")
def search():
term = request.args.get("p", "")
text = request.args.get("t", "")
if len(term) > MAX_TERM_LENGTH or len(text) > MAX_TEXT_LENGTH:
abort(400, "search input too long")
return "hit" if term in text else "no"
正規表現の機能が不要なため、文字列検索を使います。長さの上限は、製品の入力ポリシーと処理能力に合わせて決めてください。例の値は、どのサービスにも使える安全基準ではありません。
固定の正規表現にユーザー入力をリテラルとして埋め込む場合は、その部分だけをエスケープします。
python
safe_pattern = re.compile(rf"^{re.escape(term)}$")
適用時の注意点
パターン、対象文字列、sub/subn の置換文字列は役割が異なります。入力が使われる位置に応じて処理し、固定パターン自体のReDoSリスクも別途確認してください。