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?

WordPressのバージョンを外部から判定するとき、単一指標を信じると事故る話 — 4エンドポイント突合の実装

0
Posted at

環境情報

検証に使った環境は以下のとおりです。バージョン判定ロジックはWordPress本体のバージョンに依存しないため、概ねどのバージョンでも同じ挙動になります。

  • 判定対象: WordPress 5.x / 6.x 系の公開サイト(自社管理下・検証許可済み)
  • クライアント: curl 8.x, Python 3.11(urllib のみ、外部依存なし)
  • 確認エンドポイント: /(トップHTML), /feed/, /wp-json/, /robots.txt
  • 前提: キャッシュ(Cloudflare等のCDN、プラグインのページキャッシュ)が挟まっている可能性を常に考慮

結論: 外部判定は「参考値」、最終確認は管理画面

実務で起きたのは、/feed/ の generator が 5.6.17 を返しているのに、/wp-json/ には最新系プラグインのREST名前空間が並ぶ、という矛盾でした。バージョンだけを見れば古い、しかしプラグイン構成は新しい。これは単一の指標が「嘘をついた」のではなく、指標ごとに更新タイミングと偽装耐性が違うために起きます。

なので現在は次の運用に落ち着いています。

  1. 4つのエンドポイントを並列で取得する
  2. 各指標を正規化して突合する
  3. 矛盾があればバージョンを断定せず「表示値」として報告する
  4. 最終確認は管理画面の「サイトヘルス」またはダッシュボードで行ってもらう

セキュリティ診断や棚卸しの文脈で外部からバージョンを推測するのは有効ですが、断定して報告するところまで行くと危険です。これは自分の管理下、あるいは許可を得た対象に対してのみ行うものである点も前提として置いておきます。

各エンドポイントが何を漏らすか

まず生のレスポンスを見てみます。

# generator タグ(トップHTML)
curl -s https://example.com/ | grep -i 'name="generator"'
# => <meta name="generator" content="WordPress 6.4.3" />

# RSS フィードの generator
curl -s https://example.com/feed/ | grep -i '<generator>'
# => <generator>https://wordpress.org/?v=5.6.17</generator>

# REST API の名前空間一覧
curl -s https://example.com/wp-json/ | python3 -m json.tool | head -20

/wp-json/ のレスポンスはこうなります。

{
  "name": "Example Site",
  "namespaces": [
    "oembed/1.0",
    "wp/v2",
    "wp-site-health/v1",
    "contact-form-7/v1",
    "yoast/v1"
  ]
}

ここで注目すべきは、バージョンそのものではなく名前空間の有無です。wp-site-health/v1 は WordPress 5.2 以降、wp/v2 は 4.7 以降で追加されました。つまり「REST APIが生きている=4.7以上」「site-healthがある=5.2以上」という下限は推測できます。上限は分かりません。

robots.txt はコアではほぼ何も出しませんが、Yoast SEO や Rank Math が有効だと Sitemap: 行が追加されます。これはバージョンではなくプラグイン構成のシグナルです。

指標ごとの信頼度マトリクス

指標 取得できる情報 偽装・ズレの原因 単独での信頼度
トップHTMLの generator 本体バージョン(表示上) テーマ/プラグインで除去・改変、キャッシュ
/feed/ の generator 本体バージョン(表示上) CDNキャッシュで古い値が残る、意図的な偽装
/wp-json/ の namespaces 機能の下限バージョン、プラグイン構成 無効化されていると404/401
/robots.txt プラグイン構成、サイトマップ プラグイン無効時はほぼ空 低〜中
アセットの ?ver= コアスクリプトのバージョン 最適化プラグインが削除・変更

/wp-includes/js/wp-embed.min.js?ver=6.4.3 のように、コアが出力する ?ver= は本体バージョンと一致することが多いです。ただしこれも「JavaScriptの最適化」系プラグインやCDNで除去・書き換えられるため、単独では決め手になりません。

実装: 4エンドポイントを突合するスクリプト

以下は、取得と正規化、矛盾検出までを1本にしたPythonスクリプトです。標準ライブラリだけで動きます。

import json
import re
import urllib.request
from concurrent.futures import ThreadPoolExecutor

BASE = "https://example.com"
UA = "VersionChecker/1.0 (+contact: you@example.com)"

def fetch(path: str, timeout: int = 10) -> str:
    req = urllib.request.Request(
        BASE + path,
        headers={"User-Agent": UA},
    )
    try:
        with urllib.request.urlopen(req, timeout=timeout) as res:
            return res.read().decode("utf-8", errors="replace")
    except Exception as e:
        return f"__ERROR__: {type(e).__name__}"

def parse_generator(html: str) -> str | None:
    m = re.search(r'name="generator"\s+content="WordPress\s+([\d.]+)"', html)
    if not m:
        m = re.search(r"<generator>https://wordpress\.org/\?v=([\d.]+)</generator>", html)
    return m.group(1) if m else None

def parse_asset_version(html: str) -> str | None:
    m = re.search(r"wp-includes/js/wp-embed\.min\.js\?ver=([\d.]+)", html)
    return m.group(1) if m else None

def parse_namespaces(body: str) -> list[str]:
    if body.startswith("__ERROR__"):
        return []
    try:
        return json.loads(body).get("namespaces", [])
    except json.JSONDecodeError:
        return []

def detect_min_version(namespaces: list[str]) -> str | None:
    if not namespaces:
        return None
    if "wp-site-health/v1" in namespaces:
        return ">=5.2"
    if "wp/v2" in namespaces:
        return ">=4.7"
    return None

def main() -> None:
    with ThreadPoolExecutor(max_workers=4) as ex:
        paths = ["/", "/feed/", "/wp-json/", "/robots.txt"]
        raw = dict(zip(paths, ex.map(fetch, paths)))

    top = parse_generator(raw["/"])
    feed = parse_generator(raw["/feed/"])
    asset = parse_asset_version(raw["/"])
    ns = parse_namespaces(raw["/wp-json/"])
    floor = detect_min_version(ns)

    print("top   :", top)
    print("feed  :", feed)
    print("asset :", asset)
    print("floor :", floor)
    print("ns    :", ns[:8])

    cands = {v for v in (top, feed, asset) if v}
    if len(cands) > 1:
        print(f"[WARN] 表示値が矛盾しています: {sorted(cands)}")
        print("       → 断定せず『表示値』として報告すること")

if __name__ == "__main__":
    main()

出力例はこうなります。

$ python3 check_wp_version.py
top   : 6.4.3
feed  : 5.6.17
asset : 6.4.3
floor : >=5.2
ns    : ['oembed/1.0', 'wp/v2', 'wp-site-health/v1', 'contact-form-7/v1']
[WARN] 表示値が矛盾しています: ['5.6.17', '6.4.3']
       → 断定せず『表示値』として報告すること

この [WARN] が出た時点で、バージョンを断定するのをやめます。実務ではこのケースが珍しくありません。原因は多くの場合、/feed/ だけがCDNに強くキャッシュされているか、フィード生成時にセキュリティプラグインのフィルタが別経路で効いているかのどちらかです。

実装の手順(運用に落とすまで)

  1. 対象の許可を確認する。自社管理下か、書面で許可された対象に限定します。
  2. 並列で4エンドポイントを叩く。直列にするとタイムアウトの影響を受けやすく、判定がぶれます。
  3. User-Agentを明示する。匿名のスキャンはWAFで弾かれ、誤って「REST APIが無効」と判定してしまいます。
  4. 値を正規化するWordPress 6.4.36.4.3 を同じ形に揃えないと突合できません。
  5. 矛盾を検出したら断定しない。レポートには「表示値」と明記し、確度を併記します。
  6. 最終確認は管理画面へ誘導する/wp-admin/ の「サイトヘルス」→「情報」タブで、実際のバージョンとプラグイン一覧を確認できます。
  7. 結果を時系列で保存する。同じサイトを定期取得すると、キャッシュ由来のズレか実際の更新かを判別できます。

ハマりポイント

  • CDNキャッシュ: /feed/ はキャッシュTTLが長いことが多く、更新直後は古いバージョンを返します。クエリ文字列を付けてもCDN設定によっては無視されます。
  • セキュリティプラグインの偽装: generator を除去する設定や、意図的に別バージョンを出力する設定があります。「値が出ている=正しい」ではありません。
  • WAFによるブロック: 403が返ると「REST APIが無効」と誤判定しがちです。ステータスコードまで記録しましょう。
  • リダイレクト: httphttpswww あり/なしで別のキャッシュを引きます。正規URLを最初に確定させてください。
  • ?ver= の除去: 最適化プラグインが ?ver= を削ると、アセット経由の判定が丸ごと使えなくなります。

FAQ

Q. /wp-json/ が404や401を返す場合は?
A. バージョン不明ではなく「判定不能」として扱います。REST APIを無効化するプラグインは珍しくありません。判定材料が1つ減っただけで、他の指標の信頼度が上がるわけではない点に注意してください。

Q. generatorがどのエンドポイントにも出ない場合は?
A. セキュリティプラグインかテーマで除去されている可能性が高いです。この場合、外部から本体バージョンを特定するのは実質的に不可能です。素直に管理画面での確認を依頼しましょう。

Q. 一番信頼できる単一指標はありますか?
A. 「これ1つ」と言えるものはありません。強いて言えば /wp-json/ の名前空間から得られる下限バージョンは偽装が難しいですが、あくまで下限です。複数指標の整合性で判断するのが結局は一番安全です。

Q. 古いバージョンだと分かったら、そのまま報告していい?
A. ダメです。「表示値では5.6.17」までが外部から言える限界で、実際に古いかどうかは管理画面でしか確定できません。断定して報告すると、顧客との信頼を一度で失います。

Q. 定期監視に組み込むならどうする?
A. 上記スクリプトをcronで回し、top / feed / asset の3値が一致したときだけ「確定に近い」、割れたら「表示値」としてログに残す運用が現実的です。差分が出た日をトリガーに管理画面を確認してもらう流れにすると、更新の見逃しも防げます。

まとめ

  • 外部からのバージョン判定は、指標ごとに更新タイミングと偽装耐性が違う
  • generator単独、/wp-json/ 単独では簡単に騙される
  • 4エンドポイントを突合し、矛盾があれば「表示値」として報告する
  • 最終確認は必ず管理画面。外部判定は参考値と割り切るのが安全

皆さんは外部からのバージョン判定、何を最初に見ていますか? 4指標を突合する運用に変えてから、誤報がかなり減りました。逆に「この指標も実は当てにならない」という知見があればぜひ教えてください。


この記事を書いた人

BENTEN Web Works — 業務自動化・システム開発のフリーランスエンジニアです。

GAS / Python / RPA を使った業務自動化や、Web制作・システム開発のご相談を承っています。
「こんなこと自動化できる?」というご質問だけでもお気軽にどうぞ。

👉 情シス代行サービス — 詳細・お問い合わせはこちら
🐦 X(旧Twitter) — 日々の知見を発信中

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?