Sanityzacja, escaping, Nonce i Capability w przeglądzie wtyczek

Management Summary
Wtyczki WordPressa potrzebują kilku uzupełniających się zabezpieczeń przy zapisie ustawień i wyświetlaniu treści. Uprawnienia określają, które konta mogą wprowadzać zmiany. Kontrola żądań utrudnia działania wywołane przez obce strony, a reguły wejścia odrzucają niewłaściwe dane. Bezpieczne wyświetlanie zapobiega traktowaniu zapisanego tekstu jako niepożądanych poleceń w przeglądarce. Każda ochrona ma inne zadanie; zrozumienie różnic pomaga oceniać jakość wtyczki poza samym wynikiem automatycznego testu.
Spis treści
Wtyczka może odrzucać sfałszowane żądania, a mimo to zapisywać niebezpieczne dane. Może też oczyszczać każde pole, lecz pozwalać niewłaściwemu kontu zmieniać ustawienia. Oddzielne kontrole zapobiegają różnym błędom: sprawdzenie uprawnień, tokenu żądania, poprawności danych i bezpiecznego wyjścia.
Przykłady dotyczą komunikatu z etykietą, opcjonalnym odsyłaczem, ograniczonym HTML i liczbą wierszy. Bloki kodu są współpracującymi przykładami edukacyjnymi, a nie kompletną wtyczką do instalacji: ekran ustawień, nagłówek wtyczki, tłumaczenia, instalacja i obsługa błędów bazy nadal należą do otaczającej wtyczki.
Znaczenie poszczególnych kontroli
| Kontrola | Konkretne pytanie | Przykład |
|---|---|---|
| Capability | Czy konto może zmieniać ustawienia witryny? | current_user_can('manage_options') |
| Nonce | Czy żądanie ma token ważny dla tej akcji i sesji? | wp_verify_nonce() |
| Walidacja | Czy liczba wierszy jest całkowita i mieści się od 1 do 50? | Odrzucenie -1, 1.5 i tablic. |
| Sanityzacja | Które części tekstu lub HTML mogą pozostać? | sanitize_text_field(), wp_kses_post() |
| Escaping | Czy wartość może bezpiecznie trafić w to miejsce HTML? | esc_attr() dla atrybutu. |
Nonce WordPressa nie jest hasłem jednorazowym i nie dowodzi zamiaru osoby ani użycia konkretnego formularza. Pomaga przeciwdziałać Cross-Site Request Forgery (CSRF), gdy obca strona próbuje wywołać działanie z wykorzystaniem istniejącego logowania. Token można ponownie wykorzystać w okresie ważności, domyślnie około 12–24 godzin. Kontrola uprawnień nadal jest konieczna.

1. Typy danych, oczyszczanie i reguły wartości
Pole link może nadejść jako tablica, czyli zbiór wartości, przez link[]=wartosc. Funkcje przeznaczone dla tekstu nie powinny otrzymywać takiej wartości bez kontroli. Poniższa funkcja pomocnicza oczekuje czterech potrzebnych pól jako ciągów znaków, pomija dodatkowe pola i zwraca oczyszczoną tablicę albo WP_Error. Dla danych formularza WordPressa oczekuje wcześniejszego użycia wp_unslash().
function lw_nb_review_label( $raw ) {
if ( ! is_string( $raw ) ) {
return new WP_Error( 'invalid_label', 'Label must be text.', array( 'status' => 400 ) );
}
$label = sanitize_text_field( $raw );
if ( 1 !== preg_match( '/^.{1,100}$/us', $label ) ) {
return new WP_Error( 'invalid_label', 'Label must contain 1 to 100 characters after cleaning.', array( 'status' => 400 ) );
}
return $label;
}
function lw_nb_review_values( $input ) {
if ( ! is_array( $input ) ) {
return new WP_Error( 'invalid_input', 'Expected form fields.', array( 'status' => 400 ) );
}
foreach ( array( 'label', 'link', 'message', 'rows' ) as $key ) {
if ( ! isset( $input[ $key ] ) || ! is_string( $input[ $key ] ) ) {
return new WP_Error( 'invalid_input', 'A required text field is missing or malformed.', array( 'status' => 400 ) );
}
}
$label = lw_nb_review_label( $input['label'] );
if ( is_wp_error( $label ) ) {
return $label;
}
if ( 1 !== preg_match( '/^(?:[1-9]|[1-4][0-9]|50)$/D', $input['rows'] ) ) {
return new WP_Error( 'invalid_rows', 'Rows must be an integer from 1 to 50.', array( 'status' => 400 ) );
}
$link = sanitize_url( $input['link'], array( 'http', 'https' ) );
$parts = wp_parse_url( $input['link'] );
if ( '' !== $input['link'] && (
'' === $link || false === $parts || empty( $parts['host'] ) ||
empty( $parts['scheme'] ) ||
! in_array( strtolower( $parts['scheme'] ), array( 'http', 'https' ), true )
) ) {
return new WP_Error( 'invalid_link', 'Link must be empty or an absolute HTTP(S) URL.', array( 'status' => 400 ) );
}
return array(
'label' => $label,
'link' => $link,
'message' => wp_kses_post( $input['message'] ),
'rows' => (int) $input['rows'],
);
}wp_unslash() usuwa dodatkowe ukośniki odwrotne dodawane przez WordPress do danych formularza. Nie jest kontrolą bezpieczeństwa. Parametry REST nie wymagają tego kroku właściwego formularzom. Również absint() nie jest walidacją: może zmienić -5 w 5 i obciąć część ułamkową. Wzorzec liczby wierszy zamiast tego odrzuca wartości spoza wybranego formatu.
Etykieta jest sprawdzana także po oczyszczeniu. Na przykład <b></b> zawiera wcześniej znaki, ale później staje się puste. Funkcja odrzuca ten przypadek. Limit 100 znaków liczy punkty kodowe Unicode, niekoniecznie 100 widocznych symboli; litera z łączącym akcentem może zajmować dwa punkty kodowe. Reguła URL dopuszcza pustą wartość lub bezwzględny adres HTTP(S) z hostem. Nie potwierdza zaufania do celu ani bezpieczeństwa pobierania przez serwer.
2. Ochrona samego handlera zapisu
Ograniczenie dostępu do menu ustawień nie chroni osobnego handlera admin_post_.... Handler musi ponownie sprawdzić uprawnienie. Formularz używa method="post", kieruje do admin_url('admin-post.php') zabezpieczonego przez esc_url(), zawiera ukrytą akcję lw_nb_review_save oraz wynik wp_nonce_field('lw_nb_review_save', 'lw_nb_nonce'). Poniższy handler przed zapisem sprawdza metodę, uprawnienie i token:
add_action( 'admin_post_lw_nb_review_save', 'lw_nb_review_save' );
function lw_nb_review_save() {
if ( 'POST' !== ( $_SERVER['REQUEST_METHOD'] ?? '' ) ) {
wp_die( 'POST required.', '', array( 'response' => 405 ) );
}
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( 'Permission denied.', '', array( 'response' => 403 ) );
}
$nonce = isset( $_POST['lw_nb_nonce'] ) && is_string( $_POST['lw_nb_nonce'] )
? sanitize_text_field( wp_unslash( $_POST['lw_nb_nonce'] ) )
: '';
if ( ! wp_verify_nonce( $nonce, 'lw_nb_review_save' ) ) {
wp_die( 'Invalid request token.', '', array( 'response' => 403 ) );
}
$values = lw_nb_review_values( wp_unslash( $_POST ) );
if ( is_wp_error( $values ) ) {
wp_die( esc_html( $values->get_error_message() ), '', array( 'response' => 400 ) );
}
update_option( 'lw_nb_review_notice', $values );
wp_safe_redirect( admin_url( 'options-general.php' ) );
exit;
}Capability oznacza konkretne uprawnienie, a nie nazwę roli. Dla edycji konkretnego wpisu właściwa jest kontrola uwzględniająca obiekt, na przykład current_user_can('edit_post', $post_id). Własny poprawny token subskrybenta nie nadaje uprawnienia manage_options. Token wykradziony z innego konta również nie jest automatycznie ważny dla sesji subskrybenta.
3. Zabezpieczenie końcowego HTML
Zapisane dane nadal wymagają ostrożności: import, starsza wersja wtyczki lub inna ścieżka zapisu mogły zmienić wartości. Escaping należy wykonać przy ostatecznym tworzeniu HTML, także w ciągu zwracanym przez shortcode. Funkcja zależy od miejsca wyjścia:
| Miejsce | Funkcja |
|---|---|
| Tekst między znacznikami | esc_html() |
| Wartość atrybutu w cudzysłowie | esc_attr() |
| URL w href, src lub action | esc_url() |
| Zawartość textarea | esc_textarea() |
| Ograniczone HTML jak w treści wpisów | wp_kses_post() |
Na przykład esc_html($label) wyświetla etykietę jako tekst, natomiast wp_kses_post($message) zachowuje dozwolone HTML w komunikacie. Te zadania nie są wymienne. sanitize_url() oczyszcza URL do zapisu, lecz nie zastępuje esc_url() przy tworzeniu HTML. Odpowiedź JSON jest innym formatem wyjścia: zabezpieczenie HTML należy wykonać tam, gdzie klient później wstawia wartość do HTML.
4. SQL: symbole zastępcze i dozwolone nazwy
Oczyszczanie tekstu nie zabezpiecza sklejania ciągów w SQL. Poniższy przykład zakłada istnienie własnej tabeli dziennika. Dopuszcza tylko trzy kolumny sortowania i ogranicza liczbę wyników. Zmienne pochodzą ze sprawdzonych wartości, a nie bezpośrednio z parametrów żądania.
function lw_nb_review_get_log( $orderby, $limit ) {
global $wpdb;
$allowed = array( 'saved_at', 'label', 'user_id' );
if ( ! is_string( $orderby ) || ! in_array( $orderby, $allowed, true ) ) {
$orderby = 'saved_at';
}
if ( ! is_int( $limit ) || $limit < 1 || $limit > 50 ) {
$limit = 10;
}
return $wpdb->get_results(
$wpdb->prepare(
'SELECT user_id, label, saved_at FROM %i ORDER BY %i DESC LIMIT %d',
$wpdb->prefix . 'lw_nb_log',
$orderby,
$limit
)
);
}%i zabezpiecza identyfikator tabeli lub kolumny i wymaga WordPressa co najmniej 6.2. Lista dozwolonych wartości rozstrzyga, który identyfikator jest dopuszczalny; sam symbol zastępczy tego nie ustala. Wartości używają symboli takich jak %s i %d albo listy formatów w $wpdb->insert(). Dodana kolumna bazy nie trafia automatycznie na listę dozwolonego sortowania.
5. Uprawnienia REST i kontrola oczyszczonej wartości
Trasa REST to adres, pod którym program żąda danych lub zmian. Takie trasy potrzebują jawnego permission_callback. Brak callbacku może pozostawić trasę publiczną mimo ostrzeżenia WordPressa. Dla celowo publicznych danych __return_true uwidacznia tę decyzję. Poniższa trasa zapisu dopuszcza tylko konta z manage_options i wyłącznie metodę POST.
add_action( 'rest_api_init', function () {
register_rest_route( 'lw-nb-review/v1', '/notice', array(
'methods' => WP_REST_Server::CREATABLE,
'permission_callback' => function () {
return current_user_can( 'manage_options' );
},
'args' => array(
'label' => array(
'type' => 'string',
'required' => true,
'minLength' => 1,
'maxLength' => 100,
'validate_callback' => 'rest_validate_request_arg',
'sanitize_callback' => 'lw_nb_review_label',
),
),
'callback' => function ( WP_REST_Request $request ) {
$label = lw_nb_review_label( $request->get_param( 'label' ) );
if ( is_wp_error( $label ) ) {
return $label;
}
$stored = get_option( 'lw_nb_review_notice', array() );
$stored = is_array( $stored ) ? $stored : array();
$stored['label'] = $label;
update_option( 'lw_nb_review_notice', $stored );
return rest_ensure_response( array( 'label' => $label ) );
},
) );
} );Jawna walidacja schematu kontroluje typ i długość wejścia. Własna sanityzacja sprawdza także gotową etykietę, zapobiegając pustym wartościom po usunięciu znaczników. Przy uwierzytelnianiu ciasteczkiem rdzeń sprawdza Nonce dla akcji wp_rest z X-WP-Nonce albo _wpnonce. Bez tego tokenu takie żądania REST są traktowane jako nieuwierzytelnione. Inne metody, na przykład hasła aplikacji, mają własny przebieg uwierzytelniania; kontrola uprawnień nadal obowiązuje.
Co potwierdzają wykonane próby
Funkcje pomocnicze przeszły 41 oddzielnych przypadków z WordPressem 7.1.3 i PHP 8.4.27: tablice, brakujące pola, ujemne i ułamkowe liczby wierszy, puste oczyszczone etykiety, protokoły URL oraz dozwolone HTML. Kontrola składni PHP funkcji pomocniczych, handlera formularza i przykładu REST również zakończyła się powodzeniem. Handler zapisu i trasa REST nie zostały wykonane na ustawieniach działającej witryny. Wyniki nie oznaczają pełnego audytu wtyczki ani czystego raportu Plugin Check.
Pełna integracja wymaga także prób z kontem uprawnionym, kontem o mniejszych prawach i bez logowania, z brakującymi lub nieaktualnymi tokenami, z błędami bazy oraz wyjściem HTML w przeglądarce. Plugin Check dostarcza przydatnych wskazówek, ale pusty raport nie dowodzi właściwej ochrony każdej operacji.
Pytania i odpowiedzi
Czy parametr sortowania tylko do odczytu potrzebuje Nonce?
Sama zmiana kolejności wyświetlania nie modyfikuje zapisanych danych. Nadal potrzebna jest kontrola typu i lista dozwolonych wartości, a prywatne wyniki wymagają autoryzacji. Nonce nie zastępuje żadnej z tych czynności.
Czy Nonce zatrzymuje powtórne wysłanie?
Nie. Tokeny WordPressa można wykorzystywać ponownie w okresie ważności. Płatności i inne operacje do wykonania tylko raz potrzebują osobnego mechanizmu ochrony przed duplikatami.
Czy jednorazowe oczyszczanie wystarcza?
Nie. Reguły wejścia, kontrola uprawnień, parametryzowane SQL i ochrona wyjścia mają różne zadania. Oczyszczona etykieta może nadal zawierać cudzysłów wymagający zabezpieczenia w atrybucie HTML.