RでWordPressコアファイル整合性検査:公式配布版とのSHA-256照合

目的

弊社が運営しております https://www.saecanet.com/ と https://www.contact.am-consulting.co.jp/ の2つのWebサイトの2026年10月25日時点のWordPressコアファイル(ルート直下ファイル、wp-admin および wp-includes。以降同様)と同じく2026年10月25日に https://wordpress.org/ からダウンロードしました公式コアファイル(wordpress-7.1.3.zip)との整合性をRを利用して検査します。

コアファイルダウンロード時点の両WebサイトのWordPressバージョンが 7.1.3 であることは確認済みです。

なお、両Webサイトのルートに置かれていますGoogleサーチコンソール用HTMLファイル(google*******.html)は、いずれも google-site-verification: google*******.htmlと1行書かれているのみであることを確認しておりますので検査から除外しています。

また、Webサイトのセキュリティプラグインが設置していますPHPファイルが1つありますが、サイトにあるが公式に無いファイルの個別検査 を利用して eval、base64_decode、include / require は無いこと及び外部の URL を読み込んでいないことを確認しています。

同PHPファイルのファイル名には利用しておりますセキュリティプラグインの名称が含まれていますため、D:/wp-saecanet および D:/wp-contact から上述の検査後に削除して、以降の検査を実施しています。

さらに、弊社自身がWebサイト管理のために置いておりますファイル(PHPファイル×1、HTMLファイル×1。当然公式コアファイルには存在しません)もサイトにあるが公式に無いファイルの個別検査 を利用して不審な記述のパターンが無いことを確認した後に削除し、以降の検査を実施しています、

Rコード

全体検査

# 設定 ------------------------------------------------------------------

# 公式WordPressの展開先
dir_official <- "D:/wp-official/wordpress"

# 稼働中サイトのフォルダ(名前付きベクトル)
dir_sites <- c(
  saecanet = "D:/wp-saecanet",
  contact  = "D:/wp-contact"
)

# ルート直下のファイルに加えて比較するコアディレクトリ
core_dirs <- c("wp-admin", "wp-includes")

# 検査から除外するルート直下のファイル名(正規表現)
# Googleサーチコンソールの所有権確認用ファイル(google + 英数字 + .html)
pattern_exclude <- "^google[0-9a-z]+\\.html$"


# 関数定義 --------------------------------------------------------------

# コアファイルの相対パス一覧を取得する関数
get_core_files <- function(root_dir) {
  # ルート直下のファイル(ディレクトリは除く、隠しファイルを含む)
  root_all <- list.files(root_dir, all.files = TRUE, no.. = TRUE)
  root_files <- root_all[!dir.exists(file.path(root_dir, root_all))]

  # 検査対象外のファイルを除外
  root_files <- root_files[!grepl(pattern_exclude, root_files)]

  # wp-admin, wp-includes 以下の全ファイル(隠しファイルを含む)
  sub_files <- lapply(core_dirs, \(d) {
    p <- file.path(root_dir, d)
    if (!dir.exists(p)) {
      warning(p, " が存在しません")
      return(character(0))
    }
    list.files(p, recursive = TRUE, all.files = TRUE, no.. = TRUE) |>
      (\(x) file.path(d, x))()
  }) |>
    unlist()

  sort(c(root_files, sub_files))
}

# 公式とサイトのコアファイルを比較する関数
compare_core <- function(dir_official, dir_site) {
  files_official <- get_core_files(dir_official)
  files_site <- get_core_files(dir_site)

  # 両方に存在するファイルのSHA-256ハッシュを比較
  files_common <- intersect(files_official, files_site)
  hash_official <- tools::sha256sum(file.path(dir_official, files_common)) |>
    unname()
  hash_site <- tools::sha256sum(file.path(dir_site, files_common)) |>
    unname()

  # 読み込み失敗(NA)は差異ありとして扱う
  is_same <- !is.na(hash_official) & !is.na(hash_site) &
    hash_official == hash_site

  list(
    n_official    = length(files_official),
    n_site        = length(files_site),
    same          = files_common[is_same],
    different     = files_common[!is_same],
    only_official = setdiff(files_official, files_site),
    only_site     = setdiff(files_site, files_official)
  )
}

# 結果を整理して表示する関数
print_result <- function(site_name, result) {
  print_files <- function(files) {
    if (length(files) == 0) {
      cat("  (なし)\n")
    } else {
      cat(paste0("  ", files), sep = "\n")
    }
  }

  cat("\n", strrep("=", 60), "\n", sep = "")
  cat("サイト:", site_name, "\n")
  cat(strrep("=", 60), "\n", sep = "")
  cat("公式のコアファイル数  :", result$n_official, "\n")
  cat("サイトのコアファイル数:", result$n_site, "\n")
  cat("同一のファイル数      :", length(result$same), "\n")

  cat("\n【差異のあるファイル】", length(result$different), "件\n")
  print_files(result$different)

  cat("\n【公式にあるがサイトに無いファイル】", length(result$only_official), "件\n")
  print_files(result$only_official)

  cat("\n【サイトにあるが公式に無いファイル】", length(result$only_site), "件\n")
  print_files(result$only_site)
}


# 実行 ------------------------------------------------------------------

results <- lapply(dir_sites, \(d) compare_core(dir_official, d))

for (nm in names(results)) {
  print_result(nm, results[[nm]])
}

# 件数の一覧表
summary_table <- data.frame(
  site          = names(results),
  same          = sapply(results, \(r) length(r$same)),
  different     = sapply(results, \(r) length(r$different)),
  only_official = sapply(results, \(r) length(r$only_official)),
  only_site     = sapply(results, \(r) length(r$only_site)),
  row.names     = NULL
)
cat("\n", strrep("=", 60), "\n【件数の一覧】\n", sep = "")
print(summary_table)

============================================================
サイト: saecanet 
============================================================
公式のコアファイル数  : 3338 
サイトのコアファイル数: 3341 
同一のファイル数      : 3338 

【差異のあるファイル】 0 件
  (なし)

【公式にあるがサイトに無いファイル】 0 件
  (なし)

【サイトにあるが公式に無いファイル】 3 件
  .htaccess
  wp-cli.local.yml
  wp-config.php

============================================================
サイト: contact 
============================================================
公式のコアファイル数  : 3338 
サイトのコアファイル数: 3337 
同一のファイル数      : 3335 

【差異のあるファイル】 0 件
  (なし)

【公式にあるがサイトに無いファイル】 3 件
  license.txt
  readme.html
  wp-config-sample.php

【サイトにあるが公式に無いファイル】 2 件
  .htaccess
  wp-config.php

============================================================
【件数の一覧】
      site same different only_official only_site
1 saecanet 3338         0             0         3
2  contact 3335         0             3         2

サイトにあるが公式に無いファイルの個別検査

公式コアファイルに含まれていない wp-config.php 、.htaccess、wp-cli.local.yml はそれぞれ以下のコードで不審な記述のパターンの有無を確認します。

PHPファイル

以下のコードを利用して、不審な記述のパターンが無いことを確認済みです。

# 検査対象(サイト固有の確認ファイル)
files_check <- c("wp-config.php")

# 不審な記述のパターン
patterns_suspicious <- c(
  "eval\\s*\\(",
  "base64_decode",
  "gzinflate",
  "str_rot13",
  "auto_prepend_file",
  "auto_append_file",
  "AddHandler",
  "RewriteRule.*https?://",
  "https?://"
)

# 該当行を表示する関数
scan_suspicious <- function(dir_site, file_name) {
  path <- file.path(dir_site, file_name)
  lines <- readLines(path, warn = FALSE, encoding = "UTF-8")
  hit <- Reduce(`|`, lapply(patterns_suspicious, \(p) grepl(p, lines)))
  line_no <- which(hit)

  # 該当なしの場合も0行のデータフレームを返す
  data.frame(
    file = rep(file_name, length(line_no)),
    line = line_no,
    text = trimws(lines[line_no])
  )
}

for (nm in names(dir_sites)) {
  cat("\n", strrep("=", 60), "\nサイト:", nm, "\n", sep = "")
  for (f in files_check) {
    # ファイルが存在しない場合はスキップ
    if (!file.exists(file.path(dir_sites[[nm]], f))) {
      cat("\n[", f, "] ファイルなし(スキップ)\n", sep = "")
      next
    }
    res <- scan_suspicious(dir_sites[[nm]], f)
    cat("\n[", f, "] 該当", nrow(res), "行\n", sep = "")
    if (nrow(res) > 0) print(res, row.names = FALSE)
  }
}

============================================================
サイト:saecanet

[wp-config.php] 該当3行
          file line                                                                                                                   text
 wp-config.php   15                       * より詳しい情報は {@link http://wpdocs.sourceforge.jp/wp-config.php_%E3%81%AE%E7%B7%A8%E9%9B%86
 wp-config.php   28                                               // (http://wpdocs.sourceforge.jp/Codex:%E8%AB%87%E8%A9%B1%E5%AE%A4 参照)
 wp-config.php   55 * {@link https://api.wordpress.org/secret-key/1.1/salt/ WordPress.org の秘密鍵サービス} で自動生成することもできます。

============================================================
サイト:contact

[wp-config.php] 該当2行
          file line                                                                                                                       text
 wp-config.php   18                                              * by visiting {@link http://codex.wordpress.org/Editing_wp-config.php Editing
 wp-config.php   52 * You can generate these using the {@link https://api.wordpress.org/secret-key/1.1/salt/ WordPress.org secret-key service}

(注意) 上述のとおり、sourceforge.jpは2025年3月31日に閉鎖されております。リンク先には移動しないようにして下さい。

.htaccessファイル

.htaccess に関しましても外部サイトへのリダイレクト、auto_prepend_file、AddHandler がないことを以下のコードで確認します。

なお、弊社Webサイトのリモートディレクトリが出力されますため検査結果をここでは表示していませんが、不審な記述のパターンが無いことを確認済みです。

# 検査対象(サイト固有の確認ファイル)
files_check <- c(".htaccess")

for (nm in names(dir_sites)) {
  cat("\n", strrep("=", 60), "\nサイト:", nm, "\n", sep = "")
  for (f in files_check) {
    # ファイルが存在しない場合はスキップ
    if (!file.exists(file.path(dir_sites[[nm]], f))) {
      cat("\n[", f, "] ファイルなし(スキップ)\n", sep = "")
      next
    }
    res <- scan_suspicious(dir_sites[[nm]], f)
    cat("\n[", f, "] 該当", nrow(res), "行\n", sep = "")
    if (nrow(res) > 0) print(res, row.names = FALSE)
  }
}
yml ファイル

ファイルの更新日時、サイズおよび記述されている全コードから問題ないことを確認済みです。

# 確認するファイル
file_wpcli <- "wp-cli.local.yml"

for (nm in names(dir_sites)) {
  path <- file.path(dir_sites[[nm]], file_wpcli)
  if (!file.exists(path)) next

  cat("\n", strrep("=", 60), "\nサイト:", nm, "\n", sep = "")
  cat("更新日時:", format(file.mtime(path), "%Y-%m-%d %H:%M:%S"), "\n")
  cat("サイズ  :", file.size(path), "バイト\n\n")
  cat(readLines(path, warn = FALSE, encoding = "UTF-8"), sep = "\n")
}

============================================================
サイト:saecanet
更新日時: 2023-06-13 10:44:33 
サイズ  : 32 バイト

apache_modules:
  - mod_rewrite

以上です。