Na blogu dev.WPZlecenia pojawił się wczoraj artykuł sponsorowany pokazujący, jak nauczyć WordPressa przechowywania adresów IP logujących się użytkowników. Ponieważ komentarz do niego może być długi, a będzie miał też całkiem fajną wartość merytoryczną, to postanowiłem opublikować go tutaj. Zobaczymy sobie przy okazji, jak zły kod można krok po kroku poprawić.

Przede wszystkim trudno byÅ‚oby mi zaufać hostingowi, który wsadza mnie na minÄ™ i zapomina wspomnieć o ważnych konsekwencjach. Po pierwsze, jeÅ›li chcemy sobie zapisywać adres IP, to ze wzglÄ™du na RODO musimy poinformować o tym fakcie użytkowników, wpisać to w regulamin i politykÄ™ prywatnoÅ›ci, a przede wszystkim – uzasadnić potrzebÄ™ przechowywania tej wÅ‚aÅ›nie informacji (bo nie możemy zbierać i przetwarzać danych bez uzasadnienia).

Natomiast, jeÅ›li już przebrniemy przez caÅ‚e to prawnicze zamieszanie… Fajnie byÅ‚oby robić to kodem, który ma rÄ™ce i nogi.

Przyjrzyjmy siÄ™ zatem tym fragmentom kodu i zobaczmy, jak wiele da siÄ™ zepsuć w kilku linijkach kodu…

Na poczÄ…tek funkcja zapisujÄ…ca adres IP

We wspomnianym artykule wyglÄ…da ona tak:

function save_users_ip ($login) {
    global $user_id;
    $user = get_userdatabylogin($login);
    update_user_meta($user->ID,'user_ip',$_SERVER['REMOTE_ADDR']);}
add_action('wp_login','save_users_ip');

Wszystko wyglÄ…da tutaj OK, ale… Zauważmy, że na poczÄ…tku funkcji dostajemy siÄ™ do globalnej zmiennej $user_id. Po co? JeÅ›li taka zmienna istnieje, to znaczy, że mamy ID użytkownika i nastÄ™pna linijka jest zbÄ™dna. A jeÅ›li nie istnieje, to po co jÄ… tworzyć, skoro nawet jej nie używamy?

Ale w funkcji tej zaszyty jest także inny błąd, trudniejszy do wyłapania, jeśli się nie wie, że akcja wp_login otrzymuje dwa argumenty, a nie jeden (trac):

do_action( 'wp_login', $user->user_login, $user );

I nagle okazuje się, że pobieranie użytkownika na bazie loginu jest zbędne, bo dostajemy go za darmo, a zatem funkcja ta może wyglądać tak:

function save_users_ip ( $login, $user ) {
    update_user_meta( $user->ID, 'user_ip', $_SERVER['REMOTE_ADDR'] );
}
add_action( 'wp_login', 'save_users_ip', 10, 2 );

Trochę się uprościła, prawda? A do tego nie wykonuje już zbędnego zapytania do bazy danych.

Dodawanie kolumny…

Autor realizuje to takim kodem:

function add_users_ip_column($column) {
    return array_merge( $column, 
        array('user_ip' => __('Adres IP')) );}
add_filter('manage_users_columns','add_users_ip_column'); 

Jednolinijkowiec. Cóż może być źle? A no może… Autor bardzo Å‚adnie użyÅ‚ funkcji internacjonalizacji. Niestety zapomniaÅ‚ o wytycznych WordPressa mówiÄ…cych, że kod piszemy zawsze po angielsku tak, aby każdy programista czy tÅ‚umacz, byÅ‚ w stanie go zrozumieć i przetÅ‚umaczyć na swój jÄ™zyk. No i wiÄ™ksze przewinienie – brak domeny tÅ‚umaczeÅ„ (textdomain), przez co przetÅ‚umaczenie tej frazy bÄ™dzie praktycznie niemożliwe.

A jeśli miałbym się już bardzo czepiać, to zwróciłbym jeszcze uwagę na nazewnictwo zmiennych. Parametr $column nie ma sensu i wprowadza w błąd, że zawiera on informacje o jednej kolumnie. Tymczasem tak nie jest. $columns to znacznie rozsądniejsza nazwa w tym przypadku.

Wyświetlanie przechowywanego adresu IP

Zostało nam jeszcze pokazanie adresów IP w tej kolumnie:

function show_users_ip($val,$column,$user_id) {
   $user = get_userdata($user_id);
   switch ($column) { 
      case 'user_ip' : return $user->user_ip; break;
   default: }
   return $return; }
add_filter('manage_users_custom_column','show_users_ip',10,3);

I cóż miaÅ‚ tu autor na myÅ›li? Zacznijmy od koÅ„ca. JeÅ›li $column jest różne od 'user_ip’, to zwracana jest wartość zmiennej $return. Tyle, że taka zmienna nigdzie nie istnieje. Oznacza to, że nasza funkcja skutecznie zadba o to, aby wszystkie dodatkowe kolumny byÅ‚y puste.

Dodatkowo kod ten za każdym razem pobiera dane użytkownika. Przy czym przecież filtr ten jest uruchamiany dla każdej kolumny, a dane te potrzebne są tylko dla naszej.

Jeśli natomiast skupimy się nieco dłużej, do odkryjemy, że autor dość bezrefleksyjnie użył tu kalki, która ma sens, jeśli dodajemy wiele własnych kolumn. Cała ta konstrukcja switch może być sprowadzona do:

function show_users_ip( $output, $column_name, $user_id ) {
   if ( 'user_ip' === $column_name ) { 
       $user = get_userdata( $user_id );
       return $user->user_ip;
   }

   return $output;
}
add_filter( 'manage_users_custom_column', 'show_users_ip', 10, 3 );

Nieco czytelniej, prawda? Przy okazji zmieniÅ‚em nazwy parametrów na zgodne z wywoÅ‚aniem w kodzie WordPressa. I teraz Å‚atwo zauważyć jeszcze jeden, bardzo poważny błąd… Mamy zmiennÄ… $output, która nie jest w żaden sposób escape’owana. JeÅ›li spojrzymy w kod WordPressa, gdzie ten filtr jest wywoÅ‚ywany, to zauważymy, że wartość zwrócona przez naszÄ… funkcjÄ™ zostanie po prostu sklejona z wypisywanÄ… treÅ›ciÄ… tabeli. Oznacza to, że to na nas spoczywa obowiÄ…zek poprawnego escape’owania wyniku tej funkcji. Zatem jej poprawna i bezpieczna wersja wyglÄ…da tak:

function show_users_ip( $output, $column_name, $user_id ) {
   if ( 'user_ip' === $column_name ) { 
       $user = get_userdata( $user_id );
       return esc_html($user->user_ip);
   }

   return $output;
}
add_filter( 'manage_users_custom_column', 'show_users_ip', 10, 3 );

Podsumowanie

Jak widać nawet piszÄ…c kilkanaÅ›cie linijek kodu, można popeÅ‚nić wiele błędów. Niektóre bÄ™dÄ… niegroźne (jak wielokrotne i zbÄ™dne pobieranie tej samej informacji z bazy danych), a inne narażą stronÄ™ na ataki (brak escape’owania pozwala w tym przypadku na wprowadzenie do bazy danych szkodliwego kodu JS, a nastÄ™pnie wykonanie go w panelu z uprawnieniami administracyjnymi – nie jest to nawet trudne, bo wartość REMODTE_ADDR atakujÄ…cy może dość Å‚atwo przekÅ‚amać).

Na tym mniej wiÄ™cej polega wÅ‚aÅ›nie code review, czyli solidny i spokojny przeglÄ…d kodu. Po części pozwala sprawić, że kod jest czystszy, bardziej czytelny i taÅ„szy w utrzymaniu (bo kto za dwa lata bÄ™dzie pamiÄ™taÅ‚, co autor danego fragmentu spaghetti miaÅ‚ na myÅ›li). Ten przykÅ‚ad jednak Å›wietnie pokazuje, że przeglÄ…d kodu pozwala wyÅ‚apać błędy w kodzie, który „zostaÅ‚ przetestowany i dziaÅ‚a”, bo to, że kod „dziaÅ‚a”, wcale nie znaczy, że nie powoduje błędów lub, co gorsze, nie naraża strony na ataki. Róbcie code review, a jeÅ›li sami nie umiecie – zlecajcie – zapraszam 😉.

Change consents