Otwarte na nowe projekty +48 887 255 528 · kontakt[at]kosari.pl

wp-cron zwraca 403? Zapora WordPressa zablokowała nasz własny cron

wp-cron zwraca 403, choć strona działa? Zapora po aktualizacji reguł blokuje curl z crontaba. Pokazujemy, jak to sprawdzić i naprawić w 5 minut.

9 min czytania Bezpieczeństwo WordPressa
wp-cron zwraca 403? Zapora WordPressa zablokowała nasz własny cron

Poniedziałek, wieczór. Przy zupełnie innej robocie zaglądamy do logu zapory na stronie klientki i widzimy serię blokad. Co 15 minut ten sam wpis: wp-cron zwraca 403, powód „Suspicious bots/scanners”. Adres, z którego idzie żądanie, to nasz własny serwer. Zapora WordPressa od kilku godzin blokowała zadanie, które sami ustawiliśmy, żeby strony działały punktualnie.

Sprawdzamy następną stronę. To samo. I następną. W ciągu kilku minut wiemy, że problem dotyczy ponad dwudziestu stron klientów naraz – i że zaczął się tego samego dnia przed południem.

Kontekst: po co w ogóle wołamy wp-cron z zewnątrz

WordPress nie ma prawdziwego harmonogramu zadań. Jego „cron” odpala się przy okazji wizyt na stronie: ktoś wchodzi, WordPress sprawdza, czy nie zalega jakieś zadanie, i je wykonuje. Na stronie z dużym ruchem to wystarcza. Na stronie, na którą w nocy nikt nie zagląda, zaległe zadania czekają do pierwszego gościa.

A tymi zadaniami są rzeczy, na których stoi biznes: przypomnienia o płatnościach, maile do kursantów, odnowienia subskrypcji, kopie zapasowe, synchronizacje. Dlatego na stronach, którymi się opiekujemy, wyłączamy wbudowany mechanizm i wołamy wp-cron.php z crontaba serwera co kilka minut. Pisaliśmy o tym przy okazji historii z wyczyszczonym crontabem i niedziałającym wp-cronem.

Tym razem crontab był na miejscu. Zadanie odpalało się co 15 minut, jak w zegarku. Tyle że za każdym razem odbijało się od ściany.

Diagnoza: co sprawdzaliśmy

Hipoteza 1: awaria hostingu. Nie to.

Pierwsza myśl przy masowym problemie na wielu stronach to zawsze serwer. Ale strony odpowiadały normalnie, panel działał, logowanie działało. Blokady były widoczne w logu zapory samej strony, a nie w logach serwera WWW. Hosting robił swoje – to coś w WordPressie mówiło „nie”.

Hipoteza 2: ktoś zmienił konfigurację zapory. Też nie.

Na stronach działa NinjaFirewall. Sprawdziliśmy ustawienia i daty modyfikacji plików konfiguracyjnych – nikt ich nie ruszał od tygodni. Ta sama konfiguracja, która wczoraj przepuszczała nasz cron, dziś go blokowała.

Hipoteza 3: strony przestały działać. Wręcz przeciwnie.

Tu zrobiło się ciekawie. Na stronach z ruchem nic nie zalegało – WordPress odpalał zadania przy wizytach gości, więc zablokowany cron niczego nie psuł. Problem ujawniał się tylko na stronach z małym ruchem. Policzyliśmy zdarzenia zaległe ponad 30 minut:

Zaległe zadania wp-cron po kilku godzinach blokadyZadania zaległe ponad 30 minut (wieczór dnia awarii)Strona A20Strona B14Strona C12Strona D7
Strony z ruchem miały zero zaległości. Rosły tylko tam, gdzie mało kto zagląda – czyli dokładnie tam, gdzie zewnętrzny cron jest potrzebny najbardziej.

To jest najgroźniejszy rodzaj awarii: nic nie krzyczy. Strona działa, sklep działa, panel działa. Po prostu przypomnienie o płatności wyjdzie godzinę później. Albo jutro. Albo wcale, jeśli zadanie ma swoje okno czasowe.

Moment odkrycia: zapora sama zaktualizowała reguły

W logu każdej blokady był numer reguły i rodzaj zagrożenia. Zestawiliśmy godzinę pierwszej blokady z historią aktualizacji zapory i wszystko się złożyło.

NinjaFirewall pobiera aktualizacje reguł automatycznie, tak jak antywirus pobiera bazy sygnatur. Tego dnia przyszła nowa paczka reguł, w której reguła wykrywająca podejrzane boty i skanery dostała nowy wzorzec: user-agent zaczynający się od curl/. Od tej chwili każde żądanie wysłane gołym curlem było traktowane jak skaner szukający dziur. Także nasze wywołanie wp-cron z crontaba.

Z punktu widzenia zapory to ma sens. Prawdziwi użytkownicy nie przeglądają stron curlem, a automatyczne skanery często tak. Z naszego punktu widzenia – zadanie, które działało latami, przestało działać bez żadnej zmiany po naszej stronie. Nikt nie dostał maila, nic nie pojawiło się w kokpicie WordPressa.

Ta sama komenda, inny podpis - inny wynikPRZED - goły curlCrontab serweraco 15 minutŻądanie HTTPuser-agent: curl/8NinjaFirewallreguła „boty”403 Forbiddenzadania stojąPO - własny user-agentCrontab serweraco 15 minutŻądanie HTTPMojaFirmaCron/1.0NinjaFirewallprzepuszcza200 OKwp-cron działa
Zapora oceniała żądanie po podpisie przeglądarki (user-agent). Ta sama komenda z własnym podpisem przechodzi bez problemu.

Rozwiązanie krok po kroku

Mieliśmy trzy opcje: wyłączyć regułę w zaporze, dodać wyjątek dla adresu IP serwera albo zmienić sposób, w jaki cron się przedstawia. Wybraliśmy trzecią. Wyłączenie reguły osłabiłoby ochronę przed prawdziwymi skanerami na każdej stronie, a wyjątek dla IP trzeba by utrzymywać na każdej stronie osobno.

1. Potwierdź, że to ten problem

Najpierw sprawdzamy kod odpowiedzi dla gołego curla i dla curla z własnym user-agentem:

curl -s -o /dev/null -w '%{http_code}\n' 'https://twojadomena.pl/wp-cron.php?doing_wp_cron'
# 403 = zapora blokuje

curl -s -o /dev/null -w '%{http_code}\n' -A 'MojaFirmaCron/1.0 (+https://twojadomena.pl)' 'https://twojadomena.pl/wp-cron.php?doing_wp_cron'
# 200 = to ten przypadek

Jeśli masz dostęp do plików strony, zajrzyj też do logu NinjaFirewall. Plik leży w katalogu wp-content/nfwlog/ i ma w nazwie rok i miesiąc:

grep -c 'wp-cron.php.*Suspicious' wp-content/nfwlog/firewall_2026-10.php

Liczba większa od zera oznacza, że zapora blokowała wywołania crona.

2. Dodaj własny user-agent do wpisu w crontabie

Wpis, który wcześniej wyglądał tak:

*/15 * * * * curl -s 'https://twojadomena.pl/wp-cron.php?doing_wp_cron' >/dev/null 2>&1

zamieniamy na:

*/15 * * * * curl -s -A 'MojaFirmaCron/1.0 (+https://twojadomena.pl)' 'https://twojadomena.pl/wp-cron.php?doing_wp_cron' >/dev/null 2>&1

Nazwa jest dowolna, byle nie zaczynała się od curl/ ani Wget/. Dopisanie adresu strony w nawiasie to dobra praktyka – kto zobaczy to żądanie w logach, od razu wie, czyje jest.

Uwaga przy edycji crontaba. Nie zmieniaj go konstrukcją typu crontab -l | sed ... | crontab - bez kopii zapasowej. Jeśli pierwsza komenda zwróci pusty wynik, crontab zostanie wyczyszczony w całości i nic o tym nie powie. Najpierw crontab -l > kopia.txt, potem edycja pliku, potem porównanie liczby wpisów przed i po.

U nas wpisów było kilkadziesiąt na kilku serwerach, więc zrobiliśmy to jednym skryptem: kopia crontaba, podmiana w każdej linii z wp-cron.php, sprawdzenie, że liczba wpisów się nie zmieniła, i test każdej strony nowym wywołaniem. Poprawiliśmy też szablon, z którego powstają wpisy dla nowych stron – inaczej problem wróciłby przy pierwszym nowym wdrożeniu.

Nie chcesz grzebać w crontabie i logach zapory na własnej stronie? Takie rzeczy wyłapujemy w ramach opieki nad WordPressem, zanim klient zauważy, że maile wyszły z opóźnieniem.

3. Alternatywa: WP-CLI zamiast HTTP

Jeśli hosting daje dostęp do WP-CLI z crona, można w ogóle ominąć HTTP i zaporę:

*/15 * * * * cd /sciezka/do/wordpressa && wp cron event run --due-now --quiet

To rozwiązanie jest odporne na reguły zapory, ale wymaga poprawnej ścieżki do PHP i WP-CLI w środowisku crona, co na hostingach współdzielonych bywa kłopotliwe. Dlatego przy dużej liczbie stron zostaliśmy przy HTTP z własnym user-agentem.

4. Sprawdź, że zaległości zniknęły

wp cron event list --fields=hook,next_run_relative | head -20

Po kilkunastu minutach od poprawki żadne zdarzenie nie powinno mieć daty w przeszłości. Log zapory nie powinien dostawać nowych wpisów z wp-cron.php.

Efekt

Tego samego wieczoru wszystkie wpisy wp-cron na naszych serwerach przeszły na własny user-agent. Test pokazał to, co chcieliśmy zobaczyć: goły curl dalej dostaje 403, nasz cron dostaje 200. Zaległe zadania na stronach z małym ruchem wykonały się przy pierwszym przebiegu.

Na jednym hostingu cron szedł przez wget zamiast curla – nowa reguła go nie łapała i tam blokad nie było wcale. Dobra lekcja, że ta sama zmiana reguł może dotknąć tylko część stron, zależnie od drobnego szczegółu konfiguracji.

Był też efekt uboczny, który wyszedł dzień później. Nasze własne narzędzia diagnostyczne, które sprawdzają strony przez HTTP, też używały gołego curla. Zamiast prawdziwej strony dostawały kilkusetbajtową stronę blokady i zgłaszały, że coś nie działa. Fałszywy alarm, ale tylko dlatego, że ktoś go przeczytał uważnie. Te narzędzia też dostały własny user-agent.

Jak sprawdzić u siebie

  • Masz zewnętrzny cron dla WordPressa? Sprawdź w crontabie lub panelu hostingu, czym wołany jest wp-cron.php – curl, wget, czy WP-CLI.
  • Wykonaj dwa testy curlem z kroku 1. Różnica 403 i 200 oznacza, że zapora blokuje domyślny user-agent.
  • Jeśli używasz NinjaFirewall, przejrzyj log w wp-content/nfwlog/ pod kątem wpisów z wp-cron.php.
  • Uruchom wp cron event list i poszukaj zdarzeń z datą w przeszłości.
  • Sprawdź też narzędzia, które monitorują stronę przez HTTP – jeśli wysyłają goły curl, mogą zgłaszać awarię, której nie ma, albo nie zgłaszać prawdziwej.

Czego nauczyła nas ta historia

Zapora, która sama się aktualizuje, to dobra rzecz – ale oznacza, że reguły zmieniają się bez naszego udziału. Każde automatyczne zadanie, które rozmawia ze stroną przez HTTP, przedstawia się teraz własnym podpisem, a nie domyślnym podpisem narzędzia. I sprawdzamy nie tylko to, czy cron się uruchamia, ale czy zadania w WordPressie faktycznie się wykonują. Zadanie, które odpala się co 15 minut i za każdym razem dostaje 403, wygląda w crontabie dokładnie tak samo jak zadanie, które działa.

FAQ

Dlaczego wp-cron zwraca 403, skoro strona działa normalnie?

Zapora aplikacyjna (np. NinjaFirewall, Wordfence) ocenia każde żądanie osobno. Przeglądarka gościa przechodzi, a żądanie z crona z user-agentem curl/ może zostać uznane za skaner i zablokowane. Strona działa, ale zadania w tle stoją.

Czy wyłączenie reguły „Suspicious bots” w NinjaFirewall to dobre rozwiązanie?

Działa, ale osłabia ochronę przed prawdziwymi skanerami szukającymi dziur na stronie. Lepiej zmienić user-agent crona albo dodać wyjątek dla adresu IP własnego serwera.

Czy własny user-agent nie jest furtką dla atakujących?

Podpis user-agenta łatwo podrobić, więc nie traktuj go jako zabezpieczenia. Reguła blokująca curla to tylko heurystyka na najprostsze skanery. Wywołanie wp-cron.php samo w sobie nie daje nikomu dostępu – uruchamia tylko zaplanowane zadania.

Skąd mam wiedzieć, że zadania WordPressa się nie wykonują?

Najprościej przez WP-CLI: wp cron event list pokaże zdarzenia z datą w przeszłości. Bez WP-CLI pomoże wtyczka typu WP Crontrol, która pokazuje kolejkę zdarzeń w kokpicie.

Czy ten problem dotyczy tylko NinjaFirewall?

Nie. Każda zapora oceniająca user-agenta może zablokować curla: wtyczki bezpieczeństwa WordPressa, zapory hostingu (ModSecurity) i usługi typu Cloudflare. Mechanizm jest ten sam, różni się tylko miejsce, w którym szukasz logu.

Masz ten sam problem? Napisz do nas

Przypomnienia o płatnościach nie wychodzą, subskrypcje odnawiają się z opóźnieniem, a w logach widzisz 403? Opiekujemy się stronami WordPress tak, żeby takie rzeczy wyłapywać, zanim zauważą je klienci – sprawdzamy crony, zaporę i kolejkę zadań na każdej stronie.

Napisz do nas – sprawdzimy Twoją stronę

Szukasz stałej opieki? Zobacz opiekę WordPress albo hosting z opieką.

Grzegorz Rówiński · Kosari

Buduję strony, sklepy i platformy kursowe, automatyzuję procesy i wdrażam AI w firmach. Ten wpis powstał z realnego wdrożenia, nie z teorii.

Napisz do mnie →

Przeczytaj też

Masz podobny problem i nie chcesz go debugować sam?

Napisz krótko co się dzieje. Odpowiadamy konkretnie, bez zobowiązań - powiemy z czego to najpewniej wynika i ile kosztuje naprawa.

Napisz do nas

Wolisz porozmawiać? Zadzwoń: +48 887 255 528

Zadzwoń +48 887 255 528