Все працює, поки не перестає. Редактор не зберігає зміни. Налаштування крутиться вічно. Кнопка «Завантажити ще» припиняє завантаження. У DevTools видно запит до /wp-admin/admin-ajax.php, який повертає 400 або 403, а бізнес питає, чому «сайт відмовляється натискати».
admin-ajax.php — один із найстаріших інструментів WordPress. Він також приваблює контролі безпеки, помилки кешування та «корисні» оптимізації продуктивності. Хороша новина: 400/403 зазвичай — свідоме блокування. Ваше завдання — знайти, на якому рівні кажуть «ні», і чому.
Швидкий план діагностики
Це шлях «отримати сигнал за п’ять хвилин». Не елегантно, зате дієво.
1) Підтвердьте, де генерується 400/403: edge, WAF, веб-сервер чи WordPress
- Подивіться заголовки відповіді у DevTools за підказками:
server:,cf-ray,x-sucuri-id,x-mod-security,x-cache,via. - Перевірте тіло відповіді: продукти WAF часто повертають брендований HTML, JSON або загальний «Access denied.» WordPress зазвичай повертає
0або невеликий рядок, коли AJAX-обробник завершується раніше.
2) Відтворіть з сервера і ззовні
- Якщо запит не проходить із самого сервера, але працює з вашого ноутбука, підозрюйте брандмауер хоста, SELinux, локальний реверс-проксі або маршрутизацію віртуального хоста.
- Якщо він не працює ззовні, але локально — підозрюйте CDN/WAF або гео/IP контролі.
3) Читайте логи, що відповідають рівню
- Спочатку логи CDN/WAF, якщо вони є. Якщо не бачите їх, тимчасово обійдіть CDN через hosts-override, щоб потрапити на origin, і порівняйте.
- Далі access+error логи Nginx/Apache. Підтвердіть, що код статусу походить від веб-сервера, а не від edge.
- Наприкінці — PHP-FPM/WordPress debug лог. 403 рідко йде від PHP, якщо плагін явно так не робить.
4) Визначте конкретний шаблон запиту, який блокується
Виклики до admin-ajax.php різні. Деякі вимагають аутентифікації; інші — публічні. Захопіть:
- HTTP-метод (GET чи POST)
- Параметри:
action, поля nonce, розмір payload - Кукі (сесії увійшли користувача важливі)
- Заголовки Referer і Origin
5) Вирішіть: allowlist, виправити логіку застосунку або змінити архітектуру
Більшість виправлень — це одне з наступного:
- Allowlist відомих безпечних AJAX-дій у WAF/mod_security з вузьким охопленням.
- Виправити nonces/автентифікацію, якщо це проблема на рівні WordPress («вам не дозволено»).
- Припинити кешування admin-ajax.php (або припинити кешувати сторінки, що неправильно вставляють nonces).
- Перенести на REST API публічні, високонавантажені взаємодії та залишити admin-ajax для спадщинних адмін-процесів.
Як насправді працює admin-ajax.php (і чому його блокують)
admin-ajax.php — це застарілий RPC-ендпоїнт WordPress. Ви звертаєтесь до нього з action=some_hook_name, WordPress завантажує ядро, а потім викликає вашу функцію-обробник, якщо вона зареєстрована на wp_ajax_{action} (для аутентифікованих) або wp_ajax_nopriv_{action} (для неавторизованих).
Це завантаження — ключове. Для кожного запиту WordPress підвантажує багато PHP. Важкі плагіни додають ще більше. Якщо ви викликаєте його занадто часто — опитування, віджети чату, нескінченний скрол — ви фактично створюєте міні-API, яке не поводиться як сучасний API.
І оскільки він живе під /wp-admin/, інструменти безпеки трактують його як «адмінський», навіть коли він обслуговує фронтенд. Багато WAF мають правила для WordPress, які спеціально націлені на admin-ajax.php через brute-force та бот-атаку. Іноді вони праві. Іноді блокують ваш чекаут.
Корисна модель мислення: admin-ajax одночасно є контрольною площиною та публічним API, залежно від того, як плагіни його використовують. Сервіси безпеки воліють бачити його як перше. Плагіни часто трактують як друге. Конфлікт неминучий.
Жарт №1: admin-ajax.php як двері офісу з табличкою «тільки для співробітників», але кожен клієнт постійно їх знаходить і просить допомоги.
Що означає 400 vs 403 у цьому контексті
400 Bad Request
400 зазвичай означає «сервер не сподобався ваш запит», але це розмите. На практиці для admin-ajax 400 частіше буває:
- Некоректний запит: відсутній обов’язковий параметр, наприклад
action, неправильний Content-Type, пошкоджена кодування, усічене тіло. - Запит занадто великий: обмеження тіла клієнта (
client_max_body_size), обмеження інспекції тіла в WAF, обмеження розміру заголовків. - Модуль безпеки відкидає payload: mod_security повертає 400 у деяких конфігураціях, особливо для порушень у тілі запиту.
- Невідповідність проксі/апстріму: HTTP/2 на edge, HTTP/1.1 на origin з дивною перезапискою заголовків або балансувальник навів щось некоректно.
403 Forbidden
403 чесніше: «Я зрозумів вас. Вам не дозволено.» Для admin-ajax це зазвичай:
- Правило WAF/CDN блокує шлях, параметр запиту, IP, країну, ASN або threat score.
- Правило веб-сервера (
deny all, відсутній дозвіл для PHP, питання пріоритету location). - Відсутність автентифікації/куків для дій, що потребують входу.
- Перевірки nonce/capability у WordPress, які провалюються і обробник повертає 403.
- Fail2ban або лімітування частоти блокує IP клієнта.
Цікаві факти та контекст (так, тут є історія)
- admin-ajax.php з’явився ще до ери REST API у WordPress. Він став стандартним «AJAX-ендпоїнтом» задовго до того, як сучасні JSON-API стали мейнстрімом у WP.
- Історично багато плагінів використовували admin-ajax для фронтенд-запитів, бо він завжди був доступний і не вимагав «гарних» постійних посилань.
- WordPress REST API став частиною ядра в 4.7, змістивши кращі практики в бік
/wp-json/— але admin-ajax досі всюди через сумісність. - admin-ajax знаходиться під /wp-admin/, що змушує інструменти безпеки трактувати його як «адмінський трафік», навіть якщо це не так.
- Система nonce у WordPress прив’язана до часу і до сесій користувача; кешування сторінок з вбудованими nonce може ламати AJAX-запити, виглядаючи як «випадкові 403».
- Деякі набори правил WAF цілеспрямовано націлені на поширені імена параметрів WordPress, як-от
action,_wpnonceта специфічні ключі плагінів, бо зловмисники їх повторно використовують. - mod_security часто повертає 403, але може також повернути 400 залежно від того, чи вважає проблему «доступ заборонено», чи «некоректне тіло запиту».
- admin-ajax часто зловживають для DoS, бо кожен виклик може спричинити повне завантаження WordPress і важку роботу з БД.
- Багато хардення-патчів «закрити доступ до wp-admin» випадково ламають admin-ajax, бо блокують весь
/wp-admin/без винятків.
Мапа рівнів: на кожному рівні запит може загинути
Коли ви бачите 400/403 — не сперечайтесь із симптомом. Знайдіть вышибалу.
Рівень 0: Браузер і JavaScript
- Неправильний URL ендпоїнта: інколи на сайтах
ajaxurlвизначений неправильно (змішана схема, інший домен, неправильний шлях після міграції). - Помилка CORS у прекваліфікації, якщо ви надсилаєте кастомні заголовки або робите крос-доменні запити.
- Невідповідність Content-Type (
application/jsonвідправлено, але сервер очікує form-encoding).
Рівень 1: DNS і CDN edge
- CDN кешує відповідь, яку ніколи не слід кешувати.
- WAF блокує запит по шляху, юзер-агенту, шаблону запиту або ліміту частоти.
- Захист від ботів кидає виклики, що ламає фонові XHR/fetch-запити.
Рівень 2: Load balancer / reverse proxy
- Нормалізація заголовків змінює форму запиту.
- Обмеження розміру тіла відрізняються між edge і origin.
- Обмеження HTTP-методів: деякі проксі блокують POST до «адмінських» шляхів.
Рівень 3: Веб-сервер (Nginx/Apache)
- Пріоритет
locationв Nginx: широкий deny-блок ловитьadmin-ajax.php. - Правила .htaccess в Apache: харденінг-плагіни додають правила, які виглядають правильно, поки не починають ламатися.
- Права/власність файлів: файл існує, але не читається веб-користувачем.
Рівень 4: Модулі безпеки (mod_security, OWASP CRS, Imunify тощо)
- Правило спрацьовує на тілі запиту або рядку запиту.
- Аномалійний скоринг досягає порога після оновлення плагіна, що змінює форму payload.
- Хибні спрацьовування на серіалізовані дані, base64-блобах або JSON.
Рівень 5: Ядро WordPress і плагіни
- Немає зареєстрованого обробника для
action; WordPress повертає0(не 403, але часто це неправильно інтерпретують як «заблоковано»). - Перевірка nonce провалюється (
check_ajax_referer) і обробник завершується з 403. - Перевірка прав (
current_user_can) провалюється і плагін відмовляє в доступі. - Плагіни свідомо блокують трафік з «підозрілих» юзер-агентів або без referer.
Рівень 6: Рантайм PHP та інфраструктура
- Таймаути проявляються як дивна поведінка клієнта, повторні спроби або часткові тіла, які трактуються як 400.
- Брак дискового простору або inode може ламати сесії/кеші, опосередковано спричиняючи збої автентифікації.
Практичні завдання: команди, виводи та що вони означають
Потрібні докази, а не відчуття. Ось реальні завдання, які можна виконати на типовому Linux-хості з Nginx/Apache, PHP-FPM та WordPress. Кожне завдання включає: команду, приклад виводу, що означає вивід, і рішення.
Завдання 1: Відтворіть помилку за допомогою curl (базово)
cr0x@server:~$ curl -i -s -X POST https://example.com/wp-admin/admin-ajax.php -d 'action=heartbeat'
HTTP/2 403
date: Sat, 27 Dec 2025 10:12:11 GMT
content-type: text/html; charset=UTF-8
server: cloudflare
cf-ray: 88f0abc1234abcd-LHR
<html>...Access denied...</html>
Значення: Заголовок server: cloudflare і cf-ray кричать «edge/WAF згенерував відповідь». WordPress цей запит не бачив.
Рішення: Зупиніть налагодження WordPress. Перейдіть до логів CDN/WAF і правил. Також спробуйте обійти edge, щоб вдарити по origin.
Завдання 2: Обійти CDN/edge і вдарити напряму по origin (ізоляція)
cr0x@server:~$ curl -i -s --resolve example.com:443:203.0.113.10 https://example.com/wp-admin/admin-ajax.php -d 'action=heartbeat'
HTTP/2 200
date: Sat, 27 Dec 2025 10:12:44 GMT
content-type: text/html; charset=UTF-8
server: nginx
content-length: 1
0
Значення: Origin повертає 200 з тілом 0. Це стандартний «немає виводу» WordPress для AJAX-действії, яку він не обробив, або очікування автентифікації/nonce.
Рішення: Якщо edge відмовляє, а origin працює — виправляйте правило WAF/ліміт частоти/захист від ботів для admin-ajax. Якщо дія має існувати, перевірте її реєстрацію.
Завдання 3: Перегляньте access-логи Nginx для статусу і поведінки апстріму
cr0x@server:~$ sudo tail -n 20 /var/log/nginx/access.log
198.51.100.24 - - [27/Dec/2025:10:12:44 +0000] "POST /wp-admin/admin-ajax.php HTTP/2.0" 200 1 "-" "curl/7.88.1"
198.51.100.24 - - [27/Dec/2025:10:13:02 +0000] "POST /wp-admin/admin-ajax.php HTTP/2.0" 403 153 "-" "Mozilla/5.0 ..."
Значення: Origin інколи повертає 403 теж. Це означає, що це не лише «Cloudflare». Ймовірно, є правило веб-сервера, mod_security або WordPress-рівень відмови для браузероподібних запитів.
Рішення: Зіставте з error-логами і логами модулів безпеки за часовими мітками.
Завдання 4: Перевірте error-лог Nginx навколо події
cr0x@server:~$ sudo grep -n "admin-ajax.php" /var/log/nginx/error.log | tail -n 5
41288#41288: *910 access forbidden by rule, client: 198.51.100.24, server: example.com, request: "POST /wp-admin/admin-ajax.php HTTP/2.0", host: "example.com"
Значення: «access forbidden by rule» — класичний Nginx deny/allow, не PHP.
Рішення: Знайдіть відповідний location-блок і виправте пріоритет, щоб дозволити admin-ajax.php (або дозволити POST для нього).
Завдання 5: Вивантажте ефективну конфігурацію Nginx і знайдіть deny-правила
cr0x@server:~$ sudo nginx -T 2>/dev/null | grep -nE "location|deny all|wp-admin|admin-ajax\.php" | head -n 40
1123: location ^~ /wp-admin/ { deny all; }
1158: location = /wp-admin/admin-ajax.php { include fastcgi_params; fastcgi_pass unix:/run/php/php8.2-fpm.sock; }
Значення: Є широкий deny для /wp-admin/ і специфічний allow для admin-ajax. Це може працювати — але лише якщо точне співпадіння location досягається.
Рішення: Перевірте порядок location і модифікатори. location = має перевагу над префіксними, але інші правила (наприклад internal redirects) можуть заважати. Тестуйте й спрощуйте.
Завдання 6: Перевірте, чи бере участь Apache (.htaccess) (поширено на шаред-хостингах)
cr0x@server:~$ sudo apachectl -M 2>/dev/null | grep -E "rewrite|security2"
rewrite_module (shared)
security2_module (shared)
Значення: В Apache увімкнені mod_rewrite і mod_security. Навіть за умови проксі, Apache може застосувати свої правила на origin.
Рішення: Перевірте модульні логи mod_security і блоки .htaccess, що харденять wp-admin шляхи.
Завдання 7: Перевірте audit-лог mod_security на спрацьовування правила
cr0x@server:~$ sudo grep -n "admin-ajax.php" /var/log/modsec_audit.log | tail -n 8
--e3f2b9c7-H--
Message: Access denied with code 403 (phase 2). Matched phrase "select" at ARGS:query. [id "942100"] [msg "SQL Injection Attack Detected"] [severity "CRITICAL"]
Apache-Handler: proxy:unix:/run/php/php8.2-fpm.sock|fcgi://localhost/var/www/html/wp-admin/admin-ajax.php
Значення: OWASP CRS вважає, що в запиті є патерни SQLi. Параметр query тригерить — це може бути пошуковий термін, фільтр або payload плагіна.
Рішення: Не відключайте mod_security глобально. Створіть вузьку виключення для цього rule ID на конкретну дію/параметр або змініть плагін, щоб кодувати/перейменовувати поля.
Завдання 8: Підтвердіть, що WordPress взагалі бачить запит (access або slow лог PHP-FPM)
cr0x@server:~$ sudo tail -n 20 /var/log/php8.2-fpm.log
[27-Dec-2025 10:13:02] WARNING: [pool www] child 18223 said into stderr: "Primary script unknown"
Значення: «Primary script unknown» зазвичай через некоректно налаштований SCRIPT_FILENAME або погану мапу try_files/fastcgi_param. Це часто дає 404/403/400 дивні явища залежно від поведінки сервера.
Рішення: Виправте fastcgi-параметри, щоб /wp-admin/admin-ajax.php мапився на фактичний шлях у файловій системі.
Завдання 9: Перевірте, чи файл існує і чи права коректні
cr0x@server:~$ sudo ls -l /var/www/html/wp-admin/admin-ajax.php
-rw-r--r-- 1 www-data www-data 4496 Nov 8 12:10 /var/www/html/wp-admin/admin-ajax.php
Значення: Файл існує і читається. Права не проблема.
Рішення: Підіймайтесь вгору по стеку: конфігураційні правила, WAF, mod_security, WordPress-автентифікація.
Завдання 10: Перевірте, чи не заблокований IP клієнта локально (fail2ban / firewall)
cr0x@server:~$ sudo fail2ban-client status
Status
|- Number of jail: 2
`- Jail list: sshd, nginx-http-auth
cr0x@server:~$ sudo fail2ban-client status nginx-http-auth
Status for the jail: nginx-http-auth
|- Filter
| |- Currently failed: 2
| `- Total failed: 18
`- Actions
|- Currently banned: 1
`- Banned IP list: 198.51.100.24
Значення: IP браузера забанено. Це може давати 403 на сервері ще до запуску WordPress.
Рішення: Розбаньте у випадку хибного спрацьовування і налаштуйте jail. Також підтвердіть, що реальний IP клієнта не підміняється адресою проксі.
Завдання 11: Розблокувати IP (обережно) і повторно протестувати
cr0x@server:~$ sudo fail2ban-client set nginx-http-auth unbanip 198.51.100.24
1
Значення: «1» означає успіх (один IP розблоковано).
Рішення: Повторно протестуйте AJAX-запит; якщо проходить — ви підтвердили блокувальника. Налаштуйте фільтри, щоб нормальні сплески admin-ajax не скидалися як credential stuffing.
Завдання 12: Підтвердьте, що WordPress повертає 403 через провал nonce
cr0x@server:~$ sudo -u www-data wp option get home --path=/var/www/html
https://example.com
cr0x@server:~$ sudo -u www-data wp option get siteurl --path=/var/www/html
https://example.com
Значення: Home і siteurl коректні. Якщо вони неправильні (http проти https, старий домен), WordPress може згенерувати AJAX-URL або nonces, що не відповідають середовищу запиту.
Рішення: Якщо значення не відповідають реальності, виправте їх. Потім очистіть кеші і повторіть тест.
Завдання 13: Тимчасово увімкніть WordPress debug logging (і прочитайте)
cr0x@server:~$ sudo -u www-data bash -lc "grep -n \"WP_DEBUG\" /var/www/html/wp-config.php | head"
90:define('WP_DEBUG', false);
91:define('WP_DEBUG_LOG', false);
cr0x@server:~$ sudo -u www-data bash -lc "sed -i \"90,95{s/false/true/}\" /var/www/html/wp-config.php"
cr0x@server:~$ sudo -u www-data tail -n 30 /var/www/html/wp-content/debug.log
[27-Dec-2025 10:16:09 UTC] PHP Notice: check_ajax_referer failed for action=save_widget in /var/www/html/wp-content/plugins/example/plugin.php on line 211
Значення: Плагін провалює перевірку nonce. Це відмова на рівні WordPress, а не блок веб-сервера/WAF.
Рішення: Виправте кешування сторінок, що вставляють nonces, забезпечте коректні кукі та налаштування same-site, і підтвердіть, що AJAX-запит містить nonce, який очікує плагін.
Завдання 14: Підтвердіть, чи відповідь приходить від WordPress або від шару безпеки (заголовки і тіло)
cr0x@server:~$ curl -i -s https://example.com/wp-admin/admin-ajax.php?action=does_not_exist | head -n 20
HTTP/2 200
date: Sat, 27 Dec 2025 10:17:01 GMT
content-type: text/html; charset=UTF-8
server: nginx
x-powered-by: PHP/8.2.10
0
Значення: WordPress відповідає 0 для неіснуючих дій. Це нормально і вказує, що запит дістався до WordPress успішно.
Рішення: Якщо ваш провалюваний запит повертає брендовану WAF-сторінку, це інший режим помилки ніж WordPress 0 або JSON-помилка.
Завдання 15: Перевірте ліміти розміру тіла клієнта в Nginx (тригери 400)
cr0x@server:~$ sudo nginx -T 2>/dev/null | grep -n "client_max_body_size" | head
210: client_max_body_size 1m;
Значення: 1 МБ може бути замало для деяких payload плагінів (збереження конструктора сторінок, метадані медіа, великі серіалізовані опції). Перевищення може проявлятися як 413, але залежно від проксі/WAF це може деградувати у 400.
Рішення: Підвищте ліміт для сайту (або специфічно для wp-admin), якщо це обґрунтовано, і синхронізуйте ліміти з WAF.
Завдання 16: Перевірте CORS та обробку Origin (крос-доменні AJAX)
cr0x@server:~$ curl -i -s -X OPTIONS https://example.com/wp-admin/admin-ajax.php \
-H 'Origin: https://shop.example.com' \
-H 'Access-Control-Request-Method: POST' \
-H 'Access-Control-Request-Headers: content-type'
HTTP/2 403
server: nginx
content-type: text/html
Значення: OPTIONS заборонено. Якщо ви виконуєте крос-доменні запити, сервер має адекватно відповідати на preflight-запити.
Рішення: Або уникайте крос-доменних викликів до admin-ajax (краще), або явно дозволіть OPTIONS і встановіть коректні CORS-заголовки для конкретних origin.
Три корпоративні міні-історії з продакшену
Міні-історія 1: Інцидент через неправильне припущення
Маркетинг запустив плагін «spin-to-win» на сайті з великим трафіком. Він приносив конверсії. Плагін використовував admin-ajax.php для всього: створення сесій, перевірки купонів і «логування показів». Розробник, що підписав реліз, вважав admin-ajax «внутрішнім», бо він знаходиться під /wp-admin/.
Безпека зробила те, що робить: розгорнула нову політику WAF, що посилила доступ до адмін-шляхів, особливо будь-чого під /wp-admin/, що не було сесією адміністратора. Було allowlist для сторінок wp-admin, які використовують реальні адміністратори, але ніхто не додав admin-ajax, бо «це не сторінка».
Опівдні конверсії впали. Popup все ще з’являвся, але кожне «обертання» закінчувалось мертвим запитом. DevTools показав 403. Пішли тикети підтримки зі знайомим тоном сучасної комерції: «ваш сайт зламався і я злий».
Рішення зайняло десять хвилин, коли правильні люди перестали сперечатись і почали тестувати. Правило WAF оновили, щоб дозволити POST до /wp-admin/admin-ajax.php для конкретних дій плагіна. Також ввели лімітування та заблокували решту. Потім створили маленьку сторінку інвентаря AJAX-ендпоїнтів у ранбуку, бо виявилось, що половина інтерактивності сайту залежить від admin-ajax.
Урок не в тому, що WAF погані. Урок у тому, що неймінг шляхів у WordPress вводить в оману, і припущення роблять відмови персональними.
Міні-історія 2: Оптимізація, що backfired
Платформна команда хотіла зменшити PHP-навантаження. Розумно. Вони помітили, що admin-ajax.php складає значну частину запитів і вирішили кешувати його на edge для «анонімних користувачів», бо «більшість цих викликів однакові». Додали правило CDN: кешувати /wp-admin/admin-ajax.php з коротким TTL для запитів без куків увійшовшого користувача.
У staging це працювало. У production «працювало» достатньо довго, щоб всіх переконати. Потім почались дивності: користувачі отримували застарілі відповіді на дії, що мали бути унікальними; деякі бачили збій перевірки купонів; у випадку найвеселішому — плагін повернув nonce в AJAX-відповіді, і CDN із задоволенням віддав його всім протягом 30 секунд.
До того часу, як хтось визнав, що кешування admin-ajax — ризик, симптом був купою несумісних багрепортів: збій кошика, випадкові 403, «кнопка нічого не робить» і кілька проблем безпеки від уважних клієнтів. Відтворити складно, бо кеш залежав від POP і часу.
Rollback був негайний. Наслідок — замінили високонавантажені анонімні взаємодії на REST-ендпоїнти, що можна кешувати, і перестали «перехитрювати» WordPress, кешуючи все підряд.
Жарт №2: кешувати admin-ajax — як робити ксерокопії вашої пропускної картки і роздавати їх «для ефективності». Це знижує тертя.
Міні-історія 3: Нудна, але правильна практика, що врятувала день
Велика компанія вела кілька WordPress-ресурсів за одним WAF, однаковим Nginx і спільним набором правил mod_security. Їх вже палили хибні спрацьовування. Тому вони зробили нудну річ: кожне виняток у WAF/mod_security мало бути прив’язане до (1) конкретного ендпоїнта, (2) конкретного параметра і (3) тикету з командою для відтворення.
Одного дня admin-ajax почав падати з 400 через нову функцію конструктора сторінок. Редактори не могли зберегти макети. Без драми, просто гроші тихо витікали. Команда на виклику виконала збережену команду curl з тикета (вони їх зберігали в нотатках). Вона провалилася так само з чистого IP. Чудово — тепер не гадки.
Переглянули audit-логи mod_security і знайшли одне правило CRS, що фіксує JSON-поле, яке щойно змінило формат. Оскільки в них була політика «вузьких виключень», вони не відключили всю групу правил. Написали виключення, прив’язане до однієї дії і одного поля, з коментарями.
Аутедж був коротким. Безпековий стан залишився. Постмортем був коротким, бо докази вже були збережені. Нудна рутина перемогла знову.
Поширені помилки: симптом → корінь → виправлення
Це ті, що я бачу постійно, включно з тими, які починаються з «ми нічого не міняли». Хтось зробив. Або щось оновилось. Або кеш истік.
1) Симптом: 403 з брендованою сторінкою блокування
Корінь: CDN/WAF бот-захист, лімітування або керовані правила WordPress блокують /wp-admin/admin-ajax.php.
Виправлення: Створіть allow-правило для admin-ajax з обмеженнями: дозволити лише необхідні HTTP-методи, лише потрібні дії (якщо WAF може інспектувати тіло/запит), і лімітувати частоту замість блокування. Якщо увімкнено бот-челенджі, виключіть admin-ajax з челенджів.
2) Симптом: 403 тільки для увійшлих користувачів, а анонімні працюють
Корінь: Проблеми з куками або SameSite після змін HTTPS/проксі; аутентифіковані дії вимагають коректних куків. Інколи браузер припиняє надсилати кукі через SameSite чи невідповідність домену.
Виправлення: Забезпечте узгодженість схеми і домену для home/siteurl. Підтвердіть, що кукі встановлені для правильного домену, і що проксі передає X-Forwarded-Proto правильно, щоб WordPress знав, що це HTTPS.
3) Симптом: 403 після зміни «закрити wp-admin»
Корінь: Правило в Nginx/Apache забороняє весь /wp-admin/ і забули винятки для admin-ajax.php та admin-post.php.
Виправлення: Додайте специфічний allow-виняток для = /wp-admin/admin-ajax.php (і протестуйте пріоритет location). Якщо ви обмежуєте доступ до wp-admin по IP, переконайтесь, що admin-ajax не випадково також IP-обмежено для публічних дій.
4) Симптом: 400 без корисного тіла відповіді
Корінь: Тіло запиту відкидається mod_security або ліміти розміру (заголовки/тіло). Іноді проксі усічує тіло, спричиняючи «некоректний запит» вниз по ланцюжку.
Виправлення: Перевірте audit-лог mod_security спочатку. Далі перевірте ліміти Nginx/Apache і проксі. Узгодьте ліміти між edge → LB → origin. Не «підвищуйте все до нескінченності»; встановіть реалістичне обмеження.
5) Симптом: 200 OK, але тіло відповіді — «0»
Корінь: Запитана дія не зареєстрована, або обробник вийшов раніше. Іноді ви потрапляєте на неправильний сайт після міграції (не той vhost), і WordPress відповідає, але не ваш плагін.
Виправлення: Підтвердіть, що JS використовує правильний AJAX-URL. Переконайтесь, що плагін реєструє wp_ajax_ хук. Перевірте, що вам не перешкоджають must-use плагіни чи умовне завантаження для середовища.
6) Симптом: 403 тільки для певних параметрів (пошукові запити, фільтри)
Корінь: WAF/mod_security хибно спрацьовує на вміст payload (SQLi/XSS патерни) або base64/серіалізовані блоби.
Виправлення: Вузьке виняток: rule ID + endpoint + parameter. Або змініть плагін, щоб надсилати JSON і більш передбачувано кодувати/санітизувати поля.
7) Симптом: періодичний 403, часто «працює після оновлення сторінки»
Корінь: Кешована сторінка містить прострочені або невідповідні nonces; запит користувача використовує nonce, який WordPress відкидає.
Виправлення: Виключіть сторінки з user-specific nonces з повно-сторінкового кешування, або використайте ESI/фрагментне кешування. Також перевірте синхронізацію часу на серверах; вікно nonce залежить від точного часу.
8) Симптом: admin-ajax падає тільки з офісних/VPN IP-діапазонів
Корінь: Корпоративний NAT IP у списку загроз або лімітування частоти спрацьовує через багатьох користувачів за одним IP.
Виправлення: Налаштуйте лімітування WAF по сесії/куке, якщо можливо, або allowlist корпоративні IP-діапазони з моніторингом і компенсуючими контролями.
Чеклисти / покроковий план
Покроково: виправити admin-ajax.php 400/403, не погіршивши ситуацію
- Захопіть один невдалий запит з DevTools (метод, заголовки, payload, заголовки/тіло відповіді).
- Відтворіть за допомогою curl з тими ж методом і параметрами. Якщо не вдається відтворити, ви пропускаєте кукі або nonce.
- Визначте рівень:
- Є заголовки WAF/CDN? Почніть з них.
- У Nginx «access forbidden by rule»? Почніть з конфігу.
- Спрацьовування mod_security? Почніть з rule ID і параметра.
- WordPress debug показує провал nonce/прав? Виправляйте app/caching/auth.
- Доведете поведінку origin обхідом CDN і повторним тестом (шаблон з Завдання 2).
- Перевірте локальні блокування (fail2ban/firewall). Не витрачайте годину на відлагодження забаненого IP.
- Застосуйте найменшу безпечну зміну:
- Віддавайте перевагу allowlist конкретної дії/параметра замість дозволу всього admin-ajax.
- Краще лімітувати, ніж блокувати цілком.
- Для публічного трафіку віддавайте перевагу REST ендпоїнтам.
- Перевірте з кількох мереж (офіс, мобільний хотспот, сервер), щоб упевнитися, що виправлення не прив’язане до одного IP-класу.
- Додайте моніторинг: відстежуйте частоту 4xx для admin-ajax на edge та origin окремо. Змішана метрика ховає винуватця.
- Напишіть нотатку до ранбука: що блокувало, який лог це довів, яка зміна виправила і що могло б виявити проблему раніше.
Операційний чеклист: щоб admin-ajax не став фабрикою прихованих відмов
- Зробіть інвентар дій з високим об’ємом AJAX (особливо
nopriv). - Не кешуйте
/wp-admin/admin-ajax.phpна CDN або проксі. - Переконайтесь, що
/wp-admin/admin-ajax.phpдоступний, навіть якщо wp-admin IP-обмежено (якщо ви не хочете спеціально ламати фронтенд-фічі). - Узгодьте ліміти тіла/заголовків між edge, LB і origin.
- Тримайте синхронізацію часу (NTP) на всіх нодах, щоб уникнути проблем з nonce.
- Для WAF/mod_security: використовуйте вузькі виключення і відстежуйте, які плагіни/дії їх потребують.
FAQ
Чому admin-ajax.php повертає 403, хоча сайт завантажується нормально?
Бо ваша HTML-сторінка може бути кешованою і публічною, а admin-ajax — інтерактивний і часто POST-запит. Правила WAF, IP-обмеження, mod_security або перевірки nonce/куків можуть блокувати його, не впливаючи на звичайні сторінки.
Чи безпечно allowlistити /wp-admin/admin-ajax.php у WAF?
Може бути безпечно, якщо робити з обмеженнями. Allowlist шляху без умов — запрошення до зловживань. Віддавайте перевагу: дозволяйте лише необхідні методи, застосовуйте лімітування і, якщо можливо, дозволяйте лише певні значення action.
Чому я бачу «0» в тілі відповіді?
Це стандартний вивід WordPress, коли AJAX-дія не оброблена або обробник завершився без виводу. Це часто означає, що запит досяг WordPress, але ваш обробник не запустився (неправильна дія, плагін не завантажено або не виконуються очікування автентифікації).
Чи можуть кеш-плагіни спричинити admin-ajax 403?
Опосередковано — так. Вони можуть кешувати сторінки з вбудованими nonces або значеннями, що залежать від сесії, що призводить до провалу nonce, який проявляється як 403 при виклику check_ajax_referer. Вони також можуть заважати кукам або компресії в рідкісних випадках.
Чи слід переходити з admin-ajax на WordPress REST API?
Для публічних, високонавантажених взаємодій — зазвичай так. REST-ендпоїнти легше захищати, моніторити та кешувати правильно. Залишайте admin-ajax для спадщинних і адмінських UI-флоу, якщо не готові до рефакторингу.
Який найшвидший спосіб перевірити, чи блокує mod_security admin-ajax?
Подивіться audit-лог mod_security на спрацьовування правила з таймстемпом, що відповідає невдалому запиту. Якщо бачите rule ID і параметр, у вас є «димова труба».
Чому це провалюється тільки для певних пошукових запитів чи значень фільтрів?
Правила безпеки інспектують вміст payload. Деякий введений користувачем текст виглядає як атака (або випадково співпадає з сигнатурами). Тонке виключення або зміна формату/кодування запиту вирішує проблему.
Чому воно працює з домашньої мережі, але не з офісу?
Офіси часто NAT-ять багатьох користувачів за одним IP. Лімітування і детекція ботів можуть карати цей спільний IP. Також корпоративні проксі можуть змінювати заголовки, що тригерить суворіші правила.
Чи можуть неправильні налаштування home/siteurl у WordPress спричинити збої admin-ajax?
Так. Якщо WordPress вважає, що він працює на іншій схемі або домені, він може згенерувати AJAX-URL і nonces, що не відповідають контексту браузера. Виправте ці опції та перевірте заголовки проксі.
Наступні кроки, які дійсно працюють
admin-ajax.php 400/403 не містить містики. Це проблема ланцюжка відповідальності: потрібно довести, який компонент повернув статус, і яке правило або перевірка спрацювали. Коли це доведено, виправлення прості — і їх можна зробити вузькими, не руйнуючи безпеку.
Зробіть це далі:
- Візьміть один невдалий запит і відтворіть його через curl (включаючи кукі, якщо потрібно).
- Обійдіть CDN, щоб порівняти edge vs origin поведінку.
- Читати відповідні логи у порядку: WAF → веб-сервер → mod_security → PHP/WordPress.
- Застосуйте найменше allowlist або кодове/кешове виправлення, що вирішує корінь проблеми.
- Додайте одну панель у дашборді: admin-ajax 4xx rate, розділену по edge та origin. Наступний інцидент має бути нудним.
Цитата інженера: «Сподівання — це не стратегія.» — Gen. H. Norman Schwarzkopf