0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Python標準機能】astモジュールで危険なコード(eval・SQL・コマンド実行)をCIで自動検出する軽量チェッカーの作り方

0
Last updated at Posted at 2026-10-28

はじめに

チーム開発において、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種類があります。

  1. 直接呼び出し: eval(...) や exec(...) → node.func は ast.Name
  2. 属性経由の呼び出し: os.system(...) や db.execute(...) → node.func は ast.Attribute

これらを判定し、危険シンボルと一致した箇所を抽出します。


2. 完成版スクリプト: security_check.py

リポジトリ内の指定ディレクトリを再帰的に走査し、違反が見つかった場合に終了コード 1 を返すCLIスクリプトを実装します。

プロジェクトルートに security_check.py として配置してください。

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 に以下を配置します。

.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. 運用の注意点と静的パースの限界

本スクリプトはシンプルで強力ですが、静的解析特有の以下の限界があります。実運用時は前提として認識しておく必要があります。

  1. 動的な関数呼び出しの追跡不可:
    getattr(os, "sys" + "tem")(...) のように、文字列操作によって動的に組み立てられた呼び出しは検知できません。
  2. 安全な実装に対する偽陽性(誤検知):
    安全なプレースホルダーを使用していても警告が出ます。たとえば cursor.execute(..., (user_id,)) のような安全な実装でも、シンボル名 execute に反応するためです。

偽陽性への対策アプローチ

  • コメントによるインライン抑止: 特定の行に # noqa: security などのコメントがある場合はスキップする処理を追加する。
  • 引数の型判定: execute() の第1引数が ast.JoinedStr(f-strings)や ast.BinOp(文字列加算)の場合のみ警告を出すようにルールを絞り込む。

まとめ

Python標準の ast モジュールを活用すれば、追加パッケージなしで軽量なセキュリティチェックを構築できます。

  • 外部依存ゼロのため、CIを高速かつ安定して実行可能
  • ast.Call を監視するだけで、危険な関数呼び出しを網羅的に走査
  • チーム独自のコーディング規約や禁止関数の追加も容易

まずは小さなチェッカーから導入し、危険なコードの混入を未然に防ぐ仕組みを整えてみてください。


関連記事・お問い合わせ

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?