WordPress ma wbudowaną listę kontroli dostępu, a prawie nikt z niej nie korzysta. Najczęstszy błąd uprawnień na stronach klientów to nie niezałatana wtyczka - to redaktor treści z rolą Administrator, bo ktoś raz potrzebował przestawić menu. Ten przewodnik pokazuje, co API ról i uprawnień robi na poziomie źródeł: gdzie leżą dane, dlaczego add_role() milcząco ignoruje drugie wywołanie, do czego naprawdę sprowadza się sprawdzenie nazwy roli i które capabilities są szersze, niż sugeruje nazwa.
Jedna zmiana robi większość roboty. Przestań nazywać role w warunkach i nazwij capability, której kod naprawdę potrzebuje. Reszta przewodnika to koszt tej decyzji w praktyce.
1. Koncepcje: rola vs uprawnienie (capability)
Capability to pojedynczy ciąg uprawnienia: edit_posts, publish_pages, install_plugins. Rola to nazwany worek tych ciągów. WordPress przechowuje worek, a ewaluuje ciąg.
Domyślne worki są mniejsze, niż się zakłada. Według dokumentacji ról i uprawnień WordPress Editor ma 26 capabilities, a Author siedem: read, upload_files, edit_posts, edit_published_posts, publish_posts, delete_posts, delete_published_posts. Contributor ma trzy: read, edit_posts, delete_posts. Contributor nie może wgrać pliku - stąd ticket o bibliotece mediów po „dajcie mu Contributor”.
Editor niesie unfiltered_html na single-site (surowy <script> w treści). W multisite map_meta_cap() odmawia go wszystkim poza super adminami:
if ( defined( 'DISALLOW_UNFILTERED_HTML' ) && DISALLOW_UNFILTERED_HTML ) {
$caps[] = 'do_not_allow';
} elseif ( is_multisite() && ! is_super_admin( $user_id ) ) {
$caps[] = 'do_not_allow';
} else {
$caps[] = 'unfiltered_html';
}Czego Editor nie ma: edit_theme_options, list_users, edit_users, manage_options, niczego związanego z wtyczkami.
Dlaczego sprawdzenie nazwy roli wydaje się działać
Sprawdzanie nazwy roli to nie błąd składni. Jest gorzej: zwraca poprawną odpowiedź w jedynym przypadku, który przetestowałeś.
// Źle
if ( current_user_can( 'administrator' ) ) { ... }
// Dobrze
if ( current_user_can( 'manage_options' ) ) { ... }Pierwsza linia zwraca true dla administratora przez merge na końcu WP_User::get_role_caps():
$this->allcaps = array();
foreach ( (array) $this->roles as $role ) {
$the_role = $wp_roles->get_role( $role );
$this->allcaps = array_merge( (array) $this->allcaps, (array) $the_role->capabilities );
}
$this->allcaps = array_merge( (array) $this->allcaps, (array) $this->caps );$this->caps to surowa meta wp_capabilities, a jej kluczami są nazwy ról. Stąd administrator => true ląduje w allcaps obok prawdziwych uprawnień i przechodzi sprawdzenie przypadkiem. Dokumentacja current_user_can() mówi, że sprawdzanie ról zamiast capabilities jest częściowo wspierane, ale odradzane, bo daje niepewne wyniki.
Trzy scenariusze z żywych wdrożeń:
- Budujesz własną rolę z
manage_options, żeby klient wszedł na stronę ustawień.current_user_can('administrator')jestfalse. Twoja własna strona admina zwraca 403 użytkownikowi, dla którego ją zrobiłeś. - Capability nadane bezpośrednio na obiekcie użytkownika, bez roli, przechodzi każde sprawdzenie capability i pada przy każdym sprawdzeniu roli.
- W multisite
WP_User::has_cap()dla super adminów kończy się wcześniej, zanim zajrzy doallcaps, i zwracatruedla wszystkiego pozado_not_allow. Sprawdzenie roli nic tam nie rozróżnia.
Sprawdzenia capability przeżywają wszystkie trzy, bo capability to to, czego kod naprawdę potrzebuje.
2. Tworzenie własnej roli
Zanim napiszesz własną, sprawdź, czy platforma już ją dostarcza. WooCommerce przy instalacji dodaje shop_manager i customer. Według dokumentacji ról WooCommerce Shop Manager i tak zarządza produktami, zamówieniami, kuponami i kontami klientów. Równoległy „manager sklepu” dubluje rolę aktualizowaną w cudzym cyklu wydań.
Gdy naprawdę potrzebujesz własnej:
add_role(
'editorial_lead',
'Szef redakcji',
[
'read' => true,
'upload_files' => true,
'edit_posts' => true,
'edit_others_posts' => true,
'edit_published_posts' => true,
'publish_posts' => true,
'delete_posts' => true,
'moderate_comments' => true,
]
);Dwie rzeczy z tej sygnatury nie wynikają z dokumentacji API.
Zapisuje raz i potem Cię ignoruje. Handbook wtyczek mówi, że po pierwszym wywołaniu rola i capabilities są w bazie, a „kolejne wywołania nic nie robią: także przy zmianie listy uprawnień”. Edytujesz tablicę, wdrażasz - nic się nie zmienia. Każda zmiana capabilities wymaga migracji:
const EDITORIAL_LEAD_VERSION = 3;
function wppoland_sync_editorial_lead() {
if ( (int) get_option( 'wppoland_editorial_lead_version' ) === EDITORIAL_LEAD_VERSION ) {
return;
}
remove_role( 'editorial_lead' );
add_role( 'editorial_lead', 'Szef redakcji', [ /* ... */ ] );
update_option( 'wppoland_editorial_lead_version', EDITORIAL_LEAD_VERSION );
}
register_activation_hook( __FILE__, 'wppoland_sync_editorial_lead' );
add_action( 'admin_init', 'wppoland_sync_editorial_lead' );Kopia na admin_init ma znaczenie w multisite i przy deployach, które nie odpalają activation hooka. Bramka wersji trzyma to poza ścieżką krytyczną; handbook ostrzega, że bezwarunkowe usuwanie i ponowne dodawanie „mocno degraduje wydajność”.
remove_role() nie rusza użytkowników. Usuwa klucz w options; meta wp_capabilities nadal trzyma nazwę roli, a WP_Roles::is_role() już zwraca false - ciąg nic nie daje. Przypisz użytkowników przed usunięciem albo zaraz po.
Gdzie leży wiersz. WP_Roles::for_site() buduje klucz $wpdb->get_blog_prefix( $this->site_id ) . 'user_roles' (wp_user_roles na single-site, wp_3_user_roles na subsite 3). Role tylko w kodzie: global $wp_user_roles w wp-config.php - wtedy opcja nie jest aktualizowana ani używana.
3. Dodawanie uprawnień do istniejących ról
get_role() plus add_cap() to mniejszy diff, gdy domyślna rola jest prawie dobra:
$role = get_role( 'editor' );
if ( $role ) {
$role->add_cap( 'edit_theme_options' );
}Ta sama bramka wersji z sekcji 2 obowiązuje. WP_Role::add_cap( $cap, $grant = true ) ma dwa parametry; drugi to grant/deny, nie „czy zapisać”. Zapis idzie przez WP_Roles::add_cap() i update_option() przy każdym wywołaniu, gdy use_db jest true - add_cap() na init bez bramki to zapis options przy każdym requeście.
Większy problem to zakres. edit_theme_options przy klasycznym motywie otwiera mniej więcej Widgety, Menu i Customize. Przy motywie blokowym dokumentacja core nazywa je głównym capability Site Editora i ostrzega, że nie ogranicza się do niego. Editor dostaje szablony, style i nawigację - więcej niż switch_themes.
Pełny Site Editor: edit_theme_options plus edit_posts, edit_pages, edit_others_posts, read, upload_files. Bez edit_others_posts panel się ładuje, a zapis pada - czytaj Network pod 401/403; kod REST nazywa brakujące uprawnienie.
4. Odzyskiwanie dostępu: resetowanie ról
Skrypt resetu z parametrem GET, który krążył też we wcześniejszych edycjach tego artykułu, to zdalne podniesienie uprawnień:
// Nie wdrażaj tego.
if ( ! isset( $_GET['reset_roles_secret_key'] ) ) return;
require_once( ABSPATH . 'wp-admin/includes/schema.php' );
populate_roles();isset() sprawdza obecność parametru, nie poprawność. Każdy niezalogowany gość z ?reset_roles_secret_key na dowolnym URL przywraca domyślne capabilities - w tym to, co celowo ściąłeś z Editor lub Author.
Użyj WP-CLI. wp role reset korzysta z tej samej funkcji core i jest uwierzytelniony dostępem do shella:
wp role reset --all
wp role reset editorRobi też więcej niż samo populate_roles(), i różnica ma znaczenie przy debugowaniu. populate_roles() woła populate_roles_160() przez populate_roles_300(), a te funkcje tylko add_role() i add_cap(). Nie ma w nich remove_cap(). Wywołane bezpośrednio przywracają usunięte uprawnienia i zostawiają każde dodane przez wtyczkę. WP-CLI robi na tym diff względem czystej instalacji, stąd output w stylu:
Restored 1 capability to and removed 0 capabilities from 'administrator' role.Własne role żaden z tych ścieżek nie rusza. Jeśli problemem jest popsuta własna rola, reset jej nie naprawi.
Zanim zresetujesz, podejrzyj stan - to jeden wiersz options:
wp option get wp_user_roles --format=json | jq '.editor.capabilities'
wp cap list editor
wp user list --field=user_login --role=administratorOstatnie polecenie warto odpalić na każdej stronie, którą dziedziczysz. Liczba administratorów, która Cię zaskakuje, jest już diagnozą.
5. Najlepsze praktyki bezpieczeństwa 2026
Przytnij listę Administratorów, nie tylko dodawaj role
Najmniejsze uprawnienie to nie nowa rola - to krótsza lista administratorów. Odpal wp user list --role=administrator na stronach, które utrzymujesz. Konta agencji, odejście freelancera i login wsparcia wtyczki sprzed dwóch lat to typowi „lokatorzy”.
Enumeracja to nie hardening, ale nic nie kosztuje
Instalator pyta o login od WordPress 3.0, więc zostaje enumeracja: REST użytkowników bez auth zwraca m.in. slug (user_nicename). roles i capabilities są w kontekście edit. To lista loginów, nie breach - rate limiting formularza waży więcej.
Zamknij ścieżki plików stałymi, nie capabilities
Usunięcie edit_themes z Administratora to zmiana capability, którą ktoś może cofnąć. Stała w wp-config.php jest sprawdzana w map_meta_cap() i nie da się jej obejść grantem:
define( 'DISALLOW_FILE_EDIT', true );DISALLOW_FILE_EDIT mapuje edit_files, edit_plugins i edit_themes na do_not_allow. DISALLOW_FILE_MODS idzie dalej: blokuje instalację i aktualizację wtyczek oraz motywów z panelu i wyłącza edytor plików jako efekt uboczny. Na stronie wdrażanej z Gita DISALLOW_FILE_MODS jest uczciwym ustawieniem, bo aktualizacje i tak idą pipeline’em.
DISALLOW_UNFILTERED_HTML działa tak samo i warto ją ustawić na stronie redakcyjnej, gdzie Editorzy nie muszą wklejać skryptów.
Mapuj meta capabilities na własnych typach treści
register_post_type() domyślnie ustawia map_meta_cap na null, nie false. WP_Post_Type::set_props() potem zamienia null na true, gdy capabilities jest puste i capability_type to post lub page (kompatybilność wsteczna z 3.0, ticket #14122), a w pozostałych przypadkach spada do false. Własny capability_type jak poniżej ląduje więc na false, o ile tego nie powiesz. Ustaw oba argumenty:
register_post_type( 'book', [
'capability_type' => [ 'book', 'books' ],
'map_meta_cap' => true,
] );Forma tablicowa nie jest ozdobą. Przy zwykłym stringu WordPress dokleja s do liczby mnogiej - stąd storys dla story. Referencja register_post_type() wskazuje tablicę jako sposób podania alternatywnej liczby mnogiej.
capability_type => 'book' daje meta edit_book / read_book / delete_book i prymitywy w liczbie mnogiej. Kolejne sześć (m.in. delete_others_books, edit_published_books) powstaje tylko przy map_meta_cap => true. Bez flagi użytkownik z edit_books edytuje książki wszystkich. create_posts domyślnie mapuje się na edit_books - rola „edytuj, nie twórz” wymaga jawnego wpisu w capabilities.
Sprawdzaj capability w punkcie decyzji
Sprawdzenie należy obok operacji, nie przy menu. add_menu_page() z capability chowa link. Nie chroni handlera. Każdy admin-post, callback AJAX i trasa REST potrzebuje własnego current_user_can(), a REST w permission_callback, nie w handlerze - inaczej WordPress loguje _doing_it_wrong().
Dla operacji na konkretnym obiekcie przekaż obiekt: current_user_can( 'edit_post', $post_id ) idzie przez map_meta_cap() i rozstrzyga własność. current_user_can( 'edit_posts' ) pyta o typ treści, nie o ten wpis.
W multisite super admin omija allcaps wcześniej niż admin witryny - to samo rozróżnienie z sekcji 1.
Podsumowanie
Role to magazyn. Capabilities to decyzja. Cały przewodnik wynika z tego podziału:
- Sprawdzaj capabilities, nigdy nazw ról -
current_user_can('administrator')przechodzi przez merge kluczy meta użytkownika i zwraca false dla każdej własnej roli, którą zbudujesz. - Traktuj
add_role()iadd_cap()jak migracje wiersza w bazie, z bramką wersji, a nie jak konfigurację, którą edytujesz i wdrażasz. - Czytaj realny zakres capability przed grantem.
edit_theme_optionsprzy motywie blokowym to Site Editor, nie tylko ekran menu. - Odzyskuj przez
wp role reset --all, nigdy przez nieuwierzytelniony parametr w URL, i pamiętaj, że własne role zostawia w spokoju. - Ustaw
map_meta_cap => truena każdym własnym typie treści z więcej niż jednym autorem.
Audyt listy administratorów i mapy uprawnień: audyt bezpieczeństwa WordPress albo kontakt.






