---
title: サーバー内のWordPressを一括検出し、緊急脆弱性の対象バージョンを調べる
description: 複数のWordPressを収容するLinuxサーバーで、各サイトのバージョンとCVE-2026-60137・CVE-2026-63030への該当状況をPythonスクリプトで一括確認する方法。
image: wordpress-cve-2026-version-audit.jpg
date: 2026-07-19 22:23:55
---

2026年7月17日、WordPress 7.0.2がセキュリティリリースとして公開された。今回修正されたのはSQLインジェクションの **CVE-2026-60137** と、REST APIのbatchルートの解釈が混同される **CVE-2026-63030**。後者を前者と組み合わせることで、未認証の攻撃者によるリモートコード実行に至る可能性がある。

WordPress公式は影響を受けるサイトの自動更新を強制的に有効化しているが、古いサイトや自動更新に失敗したサイトが残っていないとは限らない。特に1台のサーバーに複数のWordPressを収容している場合、管理画面をひとつずつ開くよりも、ファイルシステムから一括で棚卸しした方が早い。

そこで、指定したディレクトリ以下から `wp-includes/version.php` を探し、WordPressのバージョンと今回の2件のCVEへの該当状況を表示するPythonスクリプトを作った。

## 使い方

スクリプトは [GitHub Gistで公開している](https://gist.github.com/shimarin/89c3835b1e0de75bd4f8d73eacdd23a2)。下記のソースを `find_wp_versions.py` という名前で保存し、Python 3で実行する。RHEL 8などに標準搭載されているPython 3.6でも動作するようにしてある。

たとえば `/var/www` 以下を調べる場合:

```bash
python3 find_wp_versions.py /var/www
```

複数のディレクトリも指定できる。

```bash
python3 find_wp_versions.py /var/www /srv/www
```

シェルに展開させず、スクリプト側でglobパターンを処理させることもできる。

```bash
python3 find_wp_versions.py '/home/*/public_html'
```

実行結果は次のようになる。

```text
/home/example/public_html/wp-includes/version.php: WordPress 6.9.4 / CVE-2026-63030: 対象【脆弱】6.9.5または7.0.2以降へのアップデートが必要です / CVE-2026-60137: 対象【脆弱】6.8.6、6.9.5または7.0.2以降へのアップデートが必要です
/home/another/public_html/wp-includes/version.php: WordPress 6.8.6 / CVE-2026-63030: 対象外 / CVE-2026-60137: 対象外
```

## 判定対象のバージョン

WordPress公式の発表に基づき、次のように判定する。

| WordPress | CVE-2026-60137 | CVE-2026-63030 | 修正版 |
|---|---:|---:|---|
| 6.8.0～6.8.5 | 対象 | 対象外 | 6.8.6 |
| 6.9.0～6.9.4 | 対象 | 対象 | 6.9.5 |
| 7.0.0～7.0.1 | 対象 | 対象 | 7.0.2 |
| 7.1のalpha、Beta 1 | 対象 | 対象 | 7.1 Beta 2 |
| 6.8より前 | 対象外 | 対象外 | ― |

ここでいう「対象外」は、**今回の2件の脆弱性に限った判定**である。古いWordPressを安全だと判定するものではない。サポート状況や、プラグイン・テーマに含まれる別の脆弱性もこのスクリプトでは調べていない。

## スクリプト

```python
#!/usr/bin/env python3
"""指定ディレクトリ配下の wp-includes/version.php を探索し、WordPressバージョンを表示する。

Python 3.6以上で動作する(RHEL8クラスのシステムPythonを想定)。
"""

import argparse
import glob
import os
import re
import sys
from pathlib import Path
from typing import Dict, List, Optional, Tuple

VERSION_RE = re.compile(r"""\$wp_version\s*=\s*['"]([^'"]+)['"]""")

# 2026-07-17 公表。REST APIバッチルート混同(CVE-2026-63030)とWP_Query SQLインジェクション
# (CVE-2026-60137)を組み合わせると未認証RCE(通称 wp2shell)に至る。
CVE_CHECKS = (
    (
        "CVE-2026-63030",
        "対象【脆弱】6.9.5または7.0.2以降へのアップデートが必要です",
        (
            ((6, 9, 0), (6, 9, 4)),
            ((7, 0, 0), (7, 0, 1)),
        ),
    ),
    (
        "CVE-2026-60137",
        "対象【脆弱】6.8.6、6.9.5または7.0.2以降へのアップデートが必要です",
        (
            ((6, 8, 0), (6, 8, 5)),
            ((6, 9, 0), (6, 9, 4)),
            ((7, 0, 0), (7, 0, 1)),
        ),
    ),
)


PRERELEASE_RE = re.compile(r"-(alpha|beta|rc)-?(\d+)?", re.IGNORECASE)
PRERELEASE_STAGE_RANK = {"alpha": 0, "beta": 1, "rc": 2}
# 7.1系は 7.1 Beta 2 で両CVEとも修正。それより前のプレリリース(alpha, beta1)は対象。
WP_7_1_FIX_RANK = (PRERELEASE_STAGE_RANK["beta"], 2)


def parse_version(version: str) -> Optional[Tuple[int, ...]]:
    m = re.match(r"^(\d+(?:\.\d+)*)", version)
    if not m:
        return None
    parts = tuple(int(p) for p in m.group(1).split("."))
    return (parts + (0, 0, 0))[:3]


def parse_prerelease(version: str) -> Optional[Tuple[int, int]]:
    m = PRERELEASE_RE.search(version)
    if not m:
        return None
    stage = m.group(1).lower()
    number = int(m.group(2)) if m.group(2) else 1
    return (PRERELEASE_STAGE_RANK[stage], number)


def check_cves(version: str) -> List[Tuple[str, str]]:
    parsed = parse_version(version)
    if parsed is None:
        return [
            (cve_id, "判定不能（バージョン文字列を解析できません）")
            for cve_id, _, _ in CVE_CHECKS
        ]

    prerelease = parse_prerelease(version)
    is_vulnerable_7_1_prerelease = (
        parsed == (7, 1, 0) and prerelease is not None and prerelease < WP_7_1_FIX_RANK
    )

    results = []
    for cve_id, vulnerable_message, ranges in CVE_CHECKS:
        vulnerable = any(lo <= parsed <= hi for lo, hi in ranges) or is_vulnerable_7_1_prerelease
        results.append((cve_id, vulnerable_message if vulnerable else "対象外"))
    return results


def extract_version(version_php: Path) -> Optional[str]:
    try:
        content = version_php.read_text(encoding="utf-8", errors="replace")
    except OSError as e:
        print(f"  読み込み失敗: {e}", file=sys.stderr)
        return None
    m = VERSION_RE.search(content)
    return m.group(1) if m else None


def resolve_directories(patterns: List[str]) -> List[Path]:
    directories: Dict[Path, None] = {}
    for pattern in patterns:
        matches = sorted(glob.glob(pattern)) or [pattern]
        for match in matches:
            path = Path(match)
            if path.is_dir():
                directories.setdefault(path.resolve())
            else:
                print(f"警告: {match} はディレクトリではないためスキップします", file=sys.stderr)
    return list(directories)


def find_version_php_files(directory: Path) -> List[Path]:
    # pathlib.Path.rglob() はデフォルトでシンボリックリンクを辿らない
    # (recurse_symlinks は Python 3.13以降限定のため、os.walk(followlinks=True)で代替)。
    results = []
    for dirpath, _dirnames, filenames in os.walk(str(directory), followlinks=True):
        if os.path.basename(dirpath) == "wp-includes" and "version.php" in filenames:
            results.append(Path(dirpath) / "version.php")
    return results


def main() -> int:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument(
        "directories",
        nargs="+",
        metavar="directory",
        help="探索対象のディレクトリ（複数指定可、'/home/*/public_html' のようなglobパターンも可）",
    )
    args = parser.parse_args()

    directories = resolve_directories(args.directories)
    if not directories:
        print("エラー: 探索可能なディレクトリがありません", file=sys.stderr)
        return 1

    found = sorted(
        version_php
        for directory in directories
        for version_php in find_version_php_files(directory)
    )

    if not found:
        print("wp-includes/version.php は見つかりませんでした。")
        return 0

    for version_php in found:
        version = extract_version(version_php)
        if version:
            cve_statuses = " / ".join(
                f"{cve_id}: {status}" for cve_id, status in check_cves(version)
            )
            print(f"{version_php}: WordPress {version} / {cve_statuses}")
        else:
            print(f"{version_php}: バージョン文字列が見つかりませんでした")

    return 0


if __name__ == "__main__":
    sys.exit(main())
```

## 何をしているのか

WordPress本体のバージョンは通常、`wp-includes/version.php` にある `$wp_version` へ記録されている。このスクリプトは指定されたディレクトリを再帰的に探索し、見つかったファイルから正規表現でその値を取り出す。

探索には `os.walk(..., followlinks=True)` を使っているため、シンボリックリンクの先に置かれたWordPressも対象になる。反面、循環するシンボリックリンクがある特殊なディレクトリ構成では探索が終わらなくなる可能性がある。その場合は対象ディレクトリを狭く指定するか、`followlinks=False` に変更して実行してほしい。

バージョン文字列は数値部分と `alpha`、`beta`、`rc` のプレリリース表記に分けて比較している。単純な文字列比較では、たとえば `6.9.10` と `6.9.5` の大小を正しく判定できないためである。

## 結果が「対象【脆弱】」だった場合

まずバックアップを確認したうえで、WordPress本体を該当ブランチの修正版以降へ更新する。今回の修正版は6.8.6、6.9.5、7.0.2、または7.1 Beta 2である。WordPress公式は深刻度を踏まえて即時更新を推奨している。

ただし、このスクリプトが確認しているのはディスク上のバージョンファイルだけである。サイトが実際に外部公開されているか、すでに侵害されていないか、更新後に正常動作するかまでは判定しない。更新作業と併せてアクセスログ、管理者ユーザー、設置ファイルなども確認しておきたい。

## WordPressの棚卸しや更新にお困りの場合

「どのサーバーにWordPressが残っているかわからない」「古いプラグインがあり、すぐ更新してよいか判断できない」「侵害の形跡も含めて確認したい」といった企業様:

[ワルブリックス株式会社 お問い合わせフォーム](/#contact)

お気軽にお問い合わせください。

## 参考情報

- [find_wp_versions.py（GitHub Gist）](https://gist.github.com/shimarin/89c3835b1e0de75bd4f8d73eacdd23a2)
- [WordPress 7.0.2 Release](https://wordpress.org/news/2026/07/wordpress-7-0-2-release/)
- [CVE-2026-60137: NVD](https://nvd.nist.gov/vuln/detail/CVE-2026-60137)
- [CVE-2026-63030: NVD](https://nvd.nist.gov/vuln/detail/CVE-2026-63030)

## 関連記事

- [WordPressが発行するCookieにHttpOnly属性が付いていないことを問題にされた場合の対処](wordpress-test-cookie-httponly.md)
- [NGINXのrewrite脆弱性 CVE-2026-42945「NGINX Rift」は何を確認すべきか](cve-2026-42945-nginx-rewrite.md)
- [Linuxカーネル脆弱性 CVE-2026-31635「DirtyDecrypt」は何を確認すべきか](cve-2026-31635-dirtydecrypt.md)
