はじめに
チーム開発において、eval() や os.system()、生のSQL文字列結合といった危険なコードがうっかり混入するリスクは常に存在します。コードレビューでの目視チェックには限界があり、PRの見落としからインシデントに繋がる危険もあります。
一方で、重厚な商用SASTツールの導入はハードルが高く、外部ライブラリへの依存を増やしたくない場面も少なくありません。
本記事では、Pythonの標準ライブラリ(ast モジュール)のみを用いて、危険な関数呼び出しを瞬時に検知する軽量チェッカーの作り方を解説します。外部パッケージのインストール不要で、GitHub Actions等のCIにそのまま組み込めます。
前提環境
- Python: 3.10 以上(3.11, 3.12 でもそのまま動作します)
-
依存ライブラリ: なし(標準ライブラリ
ast,pathlib,sysのみ使用)
1. ast(抽象構文木)で危険な関数呼び出しを捕まえる仕組み
Pythonの ast モジュールを使うと、ソースコードを実行せずに構文木として解析できます。
危険な関数呼び出し(eval や subprocess.Popen など)は、構文木上では ast.Call ノードとして表現されます。ast.NodeVisitor を継承したクラスを作成し、visit_Call メソッドをオーバーライドすることで、該当ノードを効率的に走査可能です。
import ast
# 監視対象とする危険な関数・属性の定義(Sinks)
DANGEROUS_CALLS = {
"eval": "コード実行 (CWE-95)",
"exec": "コード実行 (CWE-95)",
"system": "コマンドインジェクション (CWE-78)",
"popen": "コマンドインジェクション (CWE-78)",
"execute": "SQLインジェクションの懸念 (CWE-89)",
}
関数呼び出しの形式には以下の2種類があります。
-
直接呼び出し:
eval(...)やexec(...)→node.funcはast.Name -
属性経由の呼び出し:
os.system(...)やdb.execute(...)→node.funcはast.Attribute
これらを判定し、危険シンボルと一致した箇所を抽出します。
2. 完成版スクリプト: security_check.py
リポジトリ内の指定ディレクトリを再帰的に走査し、違反が見つかった場合に終了コード 1 を返すCLIスクリプトを実装します。
プロジェクトルートに security_check.py として配置してください。
import ast
from pathlib import Path
import sys
# 監視対象とする危険な呼び出し
DANGEROUS_CALLS = {
"eval": "任意コード実行 (CWE-95)",
"exec": "任意コード実行 (CWE-95)",
"system": "コマンドインジェクション (CWE-78)",
"popen": "コマンドインジェクション (CWE-78)",
"execute": "潜在的なSQLインジェクション (CWE-89)",
}
class SecurityVisitor(ast.NodeVisitor):
def __init__(self, filepath: Path):
self.filepath = filepath
self.issues = []
def visit_Call(self, node: ast.Call):
func_name = None
if isinstance(node.func, ast.Name):
func_name = node.func.id
elif isinstance(node.func, ast.Attribute):
func_name = node.func.attr
if func_name in DANGEROUS_CALLS:
self.issues.append({
"file": str(self.filepath),
"line": node.lineno,
"symbol": func_name,
"reason": DANGEROUS_CALLS[func_name],
})
self.generic_visit(node)
def scan_file(filepath: Path) -> list[dict]:
try:
content = filepath.read_text(encoding="utf-8")
tree = ast.parse(content, filename=str(filepath))
visitor = SecurityVisitor(filepath)
visitor.visit(tree)
return visitor.issues
except SyntaxError as e:
print(f"⚠️ 構文エラーによりスキップ: {filepath} ({e})")
return []
def main():
target_dir = Path(sys.argv[1]) if len(sys.argv) > 1 else Path(".")
all_issues = []
# .venv や build ディレクトリ等を除外して走査
exclude_dirs = {".venv", "venv", ".git", "__pycache__", "build", "dist"}
for py_file in target_dir.rglob("*.py"):
if any(part in exclude_dirs for part in py_file.parts):
continue
issues = scan_file(py_file)
all_issues.extend(issues)
if all_issues:
print(f"\n❌ セキュリティリスクの懸念がある呼び出しが {len(all_issues)} 件検出されました:\n")
for item in all_issues:
print(f" - {item['file']}:{item['line']} | シンボル: {item['symbol']} ({item['reason']})")
print("\n修正するか、安全性が確認されている場合は除外設定を行ってください。")
sys.exit(1)
else:
print("✅ 危険な関数呼び出しは検出されませんでした。")
sys.exit(0)
if __name__ == "__main__":
main()
手元での実行方法
# カレントディレクトリ配下をスキャン
python3 security_check.py
# 特定のディレクトリ(例: src 配下)のみをスキャン
python3 security_check.py src/
もし危険なコードが存在する場合、以下のように行番号付きで出力され、プロセス終了コード 1 で終了します。
❌ セキュリティリスクの懸念がある呼び出しが 2 件検出されました:
- src/services/user.py:42 | シンボル: execute (潜在的なSQLインジェクション (CWE-89))
- src/utils/helper.py:18 | シンボル: system (コマンドインジェクション (CWE-78))
修正するか、安全性が確認されている場合は除外設定を行ってください。
3. GitHub Actions(CI)へ組み込む方法
このスクリプトをCIに組み込むことで、プルリクエスト作成時に自動でスキャンを走らせ、危険なコードのマージを阻止できます。
リポジトリの .github/workflows/security-check.yml に以下を配置します。
name: Security AST Check
on:
pull_request:
branches: [main, develop]
push:
branches: [main]
jobs:
security-scan:
runs-on: ubuntu-latest
steps:
- name: リポジトリのチェックアウト
uses: actions/checkout@v4
- name: Python環境のセットアップ
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: ASTセキュリティスキャンの実行
run: |
python3 security_check.py .
外部ライブラリのインストール(pip install)が一切不要なため、CIの実行時間はわずか数秒で完了します。
4. 運用の注意点と静的パースの限界
本スクリプトはシンプルで強力ですが、静的解析特有の以下の限界があります。実運用時は前提として認識しておく必要があります。
-
動的な関数呼び出しの追跡不可:
getattr(os, "sys" + "tem")(...)のように、文字列操作によって動的に組み立てられた呼び出しは検知できません。 -
安全な実装に対する偽陽性(誤検知):
安全なプレースホルダーを使用していても警告が出ます。たとえばcursor.execute(..., (user_id,))のような安全な実装でも、シンボル名executeに反応するためです。
偽陽性への対策アプローチ
-
コメントによるインライン抑止: 特定の行に
# noqa: securityなどのコメントがある場合はスキップする処理を追加する。 -
引数の型判定:
execute()の第1引数がast.JoinedStr(f-strings)やast.BinOp(文字列加算)の場合のみ警告を出すようにルールを絞り込む。
まとめ
Python標準の ast モジュールを活用すれば、追加パッケージなしで軽量なセキュリティチェックを構築できます。
- 外部依存ゼロのため、CIを高速かつ安定して実行可能
-
ast.Callを監視するだけで、危険な関数呼び出しを網羅的に走査 - チーム独自のコーディング規約や禁止関数の追加も容易
まずは小さなチェッカーから導入し、危険なコードの混入を未然に防ぐ仕組みを整えてみてください。
関連記事・お問い合わせ
-
開発背景・アーキテクチャの解説:
本ツールの背景思想や動的PoC検証(偽陽性排除)の仕組みは、以下のZenn記事で詳しく解説しています。
なぜAI脆弱性スキャンは嘘ばかりつくのか?Google Mantis思想に学ぶ「真陽性率<7%の壁」を突破する設計アーキテクチャ -
業務導入・受託開発のご相談:
AI Nexusでは、業務効率化ツールの受託開発やAI導入コンサルティングを行っています。詳細は AI Nexus 公式サイト をご覧ください。