wlwmanifest.xml и RSD: два маяка, которые говорят ботам «здесь WordPress».
Ты закрыл xmlrpc.php в nginx. Поставил fail2ban. Настроил Cloudflare WAF. Доволен собой.
Открываешь access.log через неделю — и видишь:
94.102.49.190 - - [26/May/2026:04:17:33 +0300] "GET //wp-includes/wlwmanifest.xml HTTP/1.1" 404 162
94.102.49.191 - - [26/May/2026:04:17:34 +0300] "GET //xmlrpc.php?rsd HTTP/1.1" 404 162
94.102.49.192 - - [26/May/2026:04:17:35 +0300] "GET //blog/wp-includes/wlwmanifest.xml HTTP/1.1" 404 162
94.102.49.193 - - [26/May/2026:04:17:35 +0300] "GET //web/wp-includes/wlwmanifest.xml HTTP/1.1" 404 162
404 — значит файлы не нашли. Казалось бы, всё хорошо. Но боты не ищут эти файлы чтобы их читать. Они ищут их чтобы понять: тут WordPress? Если да — следующий запрос будет к xmlrpc.php или wp-login.php.
Это разведка. И большинство администраторов её не закрывают.
ЧТО ЭТО ТАКОЕ И ОТКУДА ВЗЯЛОСЬ
wlwmanifest.xml — XML-файл из эпохи Windows Live Writer. Microsoft выпустила этот блог-редактор в середине 2000-х, WordPress добавил поддержку начиная с версии 2.3.1. Файл описывал возможности сайта для десктопного клиента: какие форматы изображений поддерживаются, как публиковать посты, как подключиться к xmlrpc.php.
Microsoft окончательно свернула Windows Live Writer в 2017 году. WordPress удалил wlwmanifest.xml из ядра в версии 6.3.0 (август 2023) — функция wlwmanifest_link() помечена как deprecated и больше ничего не выводит. Но:
Во-первых, огромное количество сайтов до сих пор работает на WordPress младше 6.3. Во-вторых, сайты которые обновились с более старой версии без полной переустановки — могут иметь файл wlwmanifest.xml физически лежащим в /wp-includes/ даже при WordPress 6.3+. В-третьих, боты не проверяют версию WordPress перед сканированием — они просто стучат по всем известным путям.
Проверить есть ли файл на твоём сервере:
find /var/www -name "wlwmanifest.xml" 2>/dev/null
RSD (Really Simple Discovery) — ещё более старый стандарт, 2002 год. Позволял внешним приложениям автоматически обнаруживать API блог-платформы. WordPress до сих пор добавляет ссылку на него в <head> каждой страницы через функцию rsd_link(), которая не депрекирована:
<link rel="EditURI" type="application/rsd+xml" title="RSD"
href="https://example.com/xmlrpc.php?rsd" />
Обрати внимание на значение href: xmlrpc.php?rsd. RSD-ссылка буквально указывает на xmlrpc.php. Любой бот, прочитавший HTML страницы, сразу знает где искать.
ПОЧЕМУ ЭТО ПРОБЛЕМА
Три уровня раскрытия информации:
Уровень 1: подтверждение что это WordPress
type="application/wlwmanifest+xml" и путь /wp-includes/wlwmanifest.xml — уникальный идентификатор WordPress. Ни один другой CMS не использует этот MIME-тип и этот путь. Боты-сканеры используют именно его для детектирования WordPress, даже если файл возвращает 404.
Уровень 2: прямое указание на xmlrpc.php
RSD-ссылка в <head> буквально содержит URL xmlrpc.php. Можно закрыть xmlrpc.php в nginx — но если RSD-ссылка в HTML, сканер знает что xmlrpc.php существует и будет пробовать его напрямую.
Уровень 3: паттерн сканирования
Типичная последовательность бот-атаки на WordPress выглядит так:
GET /wp-includes/wlwmanifest.xml → 200 или 404? Есть WordPress?
GET /xmlrpc.php?rsd → подтверждение xmlrpc.php
GET /xmlrpc.php → атака начинается
POST /xmlrpc.php → брутфорс / pingback / DDoS
Закрыв xmlrpc.php но оставив wlwmanifest.xml и RSD, ты убрал саму атаку, но оставил разведку. Боты это видят, делают заметки и возвращаются позже — может быть с другого IP, может быть с другим вектором (wp-login.php, REST API).
КАК ЗАКРЫТЬ: ДВА УРОВНЯ
Уровень 1: nginx — блокировать прямой доступ
Это работает независимо от версии WordPress и независимо от того, лежит файл на диске или нет. Запрос блокируется до PHP.
Добавить в блок server{} твоего nginx-конфига:
# Блокировать wlwmanifest.xml — работает для всех версий WordPress
location ~* /wp-includes/wlwmanifest\.xml {
deny all;
access_log off;
log_not_found off;
}
# Блокировать RSD endpoint
# Если xmlrpc.php уже закрыт через location = /xmlrpc.php { deny all; }
# то /xmlrpc.php?rsd тоже закрыт — nginx exact match игнорирует query string.
# Отдельный блок нужен только если xmlrpc.php закрыт через regex location ~*
location ~* /xmlrpc\.php {
deny all;
access_log off;
log_not_found off;
}
Проверить что блокировка работает:
nginx -t && systemctl reload nginx
curl -I https://example.com/wp-includes/wlwmanifest.xml
# Ожидаемый ответ: HTTP/1.1 403 Forbidden
curl -I "https://example.com/xmlrpc.php?rsd"
# Ожидаемый ответ: HTTP/1.1 403 Forbidden
Уровень 2: WordPress — убрать RSD-ссылку из HTML <head>
Блокировка в nginx закрывает прямой доступ к endpoint, но RSD-ссылка всё ещё присутствует в HTML каждой страницы. Бот читает HTML и видит xmlrpc.php?rsd в href. Нужно убрать её из источника.
rsd_link() — актуальная функция, не депрекирована. Убрать через mu-plugin:
# Создать mu-plugin — загружается всегда, не требует активации
cat > /var/www/html/wp-content/mu-plugins/remove-legacy-links.php << 'EOF'
<?php
/**
* Plugin Name: Remove Legacy Discovery Links
* Description: Removes RSD link and WordPress version from <head>
*/
// RSD-ссылка (актуальна для всех версий WordPress)
remove_action('wp_head', 'rsd_link');
// Версия WordPress в <meta name="generator"> — раскрывает версию для CVE-поиска
remove_action('wp_head', 'wp_generator');
// wlwmanifest — нужно только для WordPress до 6.3
// В 6.3+ функция депрекирована и не выводит ничего, строка безвредна
remove_action('wp_head', 'wlwmanifest_link');
EOF
mu-plugins загружаются автоматически, не требуют активации, не отображаются в списке плагинов и не отключаются случайно.
Проверить результат:
curl -s https://example.com/ | grep -E "wlwmanifest|EditURI|generator.*WordPress"
# Всё должно быть пустым
ПРОВЕРИТЬ ВЕРСИЮ WORDPRESS И НАЛИЧИЕ ФАЙЛА
Понять с чем именно имеешь дело:
# Версия WordPress
cat /var/www/html/wp-includes/version.php | grep wp_version
# Есть ли файл wlwmanifest.xml физически на диске
find /var/www -name "wlwmanifest.xml" 2>/dev/null
# Доступен ли файл по HTTP
curl -o /dev/null -w "%{http_code}" https://example.com/wp-includes/wlwmanifest.xml
Если WordPress 6.3+ и файл не найден командой find — wlwmanifest.xml не существует. Но nginx-блокировка всё равно нужна: боты стучат по этому пути на всех сайтах подряд, и 403 вместо 404 позволяет их банить через fail2ban.
ДИАГНОСТИКА: СКОЛЬКО ЗАПРОСОВ ПРИХОДИТ
Посмотреть активность сканеров за последние сутки:
grep "wlwmanifest" /var/log/nginx/access.log | \
awk '{print $1}' | sort | uniq -c | sort -rn | head -20
Посмотреть паттерн — одни IP часто запрашивают wlwmanifest, xmlrpc и wp-login в связке:
grep -E "wlwmanifest|xmlrpc|wp-login" /var/log/nginx/access.log | \
awk '{print $1, $7}' | sort | head -40
Добавить wlwmanifest в fail2ban фильтр для WordPress:
# /etc/fail2ban/filter.d/nginx-wordpress.conf
[Definition]
failregex = ^<HOST> - - \[.*\] "(GET|POST) .*xmlrpc\.php.* HTTP/.*" (200|403|404) \d+
^<HOST> - - \[.*\] "GET .*wlwmanifest\.xml HTTP/.*" (200|403|404) \d+
ignoreregex =
ТИПИЧНЫЕ ОШИБКИ
Ошибка 1: «У меня WordPress 6.3+ — wlwmanifest меня не касается»
Касается — с двух сторон. Первое: боты не проверяют твою версию WordPress, они стучат по /wp-includes/wlwmanifest.xml на всех сайтах подряд. Без nginx-блокировки они получают 404 и фиксируют что тут WordPress. Второе: после обновления с более старой версии файл может физически лежать на диске.
Ошибка 2: «Файл отдаёт 404 — значит всё хорошо»
404 означает что бот получил ответ от сервера, записал что сканировал этот адрес, и сделал вывод — тут WordPress (потому что только WordPress запрашивают именно этот путь). 403 лучше: можно забанить того кто спрашивает.
Ошибка 3: «Убрал RSD через functions.php родительской темы»
Изменения в functions.php родительской темы не переживут обновление темы. Используй mu-plugin или functions.php дочерней темы.
ПОЛНЫЙ ЧЕКЛИСТ ПРОВЕРКИ
# 1. wlwmanifest.xml недоступен напрямую
curl -o /dev/null -w "%{http_code}" https://example.com/wp-includes/wlwmanifest.xml
# Ожидаем: 403
# 2. RSD endpoint недоступен
curl -o /dev/null -w "%{http_code}" "https://example.com/xmlrpc.php?rsd"
# Ожидаем: 403
# 3. RSD-ссылка не присутствует в HTML
curl -s https://example.com/ | grep -c "EditURI"
# Ожидаем: 0
# 4. Версия WordPress не видна в HTML
curl -s https://example.com/ | grep "generator.*WordPress"
# Ожидаем: пустой вывод
ИТОГ
wlwmanifest.xml и RSD — не уязвимости в классическом смысле. Они не дают атакующему прямого доступа. Но они дают информацию: тут WordPress, тут xmlrpc.php, вот точный URL.
В контексте автоматизированных атак информация = преимущество. Боты работают по спискам сигнатур. Убрав эти сигнатуры, ты выпадаешь из большинства автоматических сканов.
Два действия, пять минут:
# nginx: закрыть прямой доступ
location ~* /wp-includes/wlwmanifest\.xml {
deny all;
access_log off;
log_not_found off;
}
# WordPress mu-plugin: убрать RSD и версию из HTML
remove_action('wp_head', 'rsd_link');
remove_action('wp_head', 'wp_generator');
Это не серебряная пуля. Это закрытие лишних дверей которые всё равно никому не нужны.
