MySQLowy autoincrement jest bez wątpienia potężnym i wygodnym mechanizmem. Jest też bardzo popularny. I trudno się dziwić ;) ustaw - zapomnij. Wygodne prawda? Jednak takie podejście niesie za sobą pewne nieciekawe konsekwencje.

Korzystając z interfejsu RESTowego jesteśmy zmuszeni (aczkolwiek raczej nikomu to nie przeszkadza ;) ) do używania identyfikatorów na zasadzie: artykuly/1234/komentarze/2156 gdzie mamy id artykułu i id komentarza.

Ale co w tym złego?

Niby nic - liczby jak każde inne. Jednak tutaj zaczyna się problem, ponieważ liczby te po pierwsze mogą nam powiedzieć dość dużo same z siebie - kiedy ustawione są na autoincrement numerowane są w kolejności powstania. Dzięki temu ktoś uważny jest w stanie ocenić jak dużo mamy komentarzy/logów/wpisów/zdjęć w systemie, nawet jeśli część nie jest dla niego dostępna.

Ciekawostka: Takiej metody użyli alianci żeby ocenić poziom produkcji czołgów niemieckich. Numery seryjne czołgów były właśnie inkrementowane.

Kolejną rzeczą jest bezpieczeństwo. Oczywiście stosujemy white listy i inne metody weryfikacji uprawnień użytkowników. Stosujemy też testy żeby sprawdzić czy wszystko jest należycie zabezpieczone. Niemniej jednak nie sposób uchronić się od wszystkich błędów.

Sprytny użytkownik

Załóżmy że mamy logi systemowe - część logów może przeglądać każdy użytkownik (te które dotyczą właśnie jego) a część tylko admin. No i implementujemy sobie podgląd danego logu w akcji show, gdzie w URLu podajemy ID.

Oczywistym rozwiązaniem byłoby zastosowanie filtra, który sprawdzałby uprawnienia i ew. odnotował że ten i ten user stara się uzyskać dostęp do zasobów chronionych. Jednak przez nieuwagę zapomnieliśmy to zrobić.

Co robi sprytny użytkownik?

Widząc że jego wpisy idą w miarę po kolei (np. 1, 5, 9,21, 23, ...) modyfikuje ręcznie URL i wchodzi na log o ID 2, 3, itd przeglądając rzeczy które teoretycznie powinny być dostępne tylko dla admina.

Inny sprytny pomysł

Dawno, dawno temu zamknięto serwis napisy.org i w polskim internecie zapanował strach wśród fanów anime że zamkną i świetny serwis animesub.info. Nie będę ukrywał że i ja się bałem ;) a że nie lubię siedzieć i czekać postanowiłem coś z tym zrobić. Za tamtych czasów serwis ten nie miał praktycznie żadnych zabezpieczeń, a pliki ściągać można było będąc niezalogowanym. Stosowano tam właśnie autoincrement dla plików. W bazie było ID pliku które jednocześnie było jego nazwą na dysku oraz był wpis w bazie do jakiego anime i odcinka jest ten plik.

Wystarczyło więc (wtedy jeszcze w delphi) napisać prosty program który będzie próbował pobrać wszystkie pliki od 0 do ostatniego (najnowszego). Metoda okazała się bardzo skuteczna. Aby nie wzbudzać podejrzeń pliki pobierane były z losowym delayem kilkusekundowym. Po dobie lokalna kopia ich bazy napisów była u mnie. Napisy usunąłem jak tylko potwierdzono że animesub zostaje :)

Na pocieszenie dodam że adminów animesub powiadomiłem i w miarę szybko zastosowali odpowiednie zabezpieczenia które skutecznie zabezpieczają obecnie ten system przed tego typu atakami.

Co zatem zrobić?

Osobiście znam dwa rozwiązania. Pierwszym z nich jest stosowanie GUIDów (Globally Unique Identifier) które wyglądają mniej więcej tak: 25892e17-80f6-415f-9c65-7395632f0223.

Jest to losowy ciąg cyfer oraz liter który generowany jest z jakiegoś skomplikowanego wzorca. W takim przypadku nie ma możliwości "jechania" po kolejnych wartościach bo jest ich po prostu za dużo.

Innym rozwiązaniem (które i ja stosuję troszkę częściej niż GUIDy) jest zrandomizowanie klucza głównego.

Implementacja tego rozwiązania jest bardzo prosta niezależnie od wybranego języka. Po prostu zamiast polegać na autoincremencie - losujemy ID z dużego zakresu (np. z miliona), sprawdzamy czy taki wpis już istnieje (na wszelki wypadek) i jeśli nie to dodajemy wpis. Jeśli tak to losujemy, sprawdzamy, dodajemy.

A jak to zrobić w Ruby on Rails?

A bardzo prosto. Pokusiłem się i napisałem nawet plugin który sam w sobie składa się z kilkunastu linijek, jednak według mnie jest bardzo wygodny. Nazwałem go acts_as_random co chyba oddaje w miarę sensownie jego funkcję.

Kod pluginu na licencji MIT (czyli rób z tym co chcesz ale jak coś nie będzie działać to też się nie czepiaj) dostępny do pobrania na końcu artykułu.

Jego użycie jest bardzo proste. Rozpakowujemy go do katalogu /vendor/plugins naszej aplikacji i od teraz możemy używać go w każdym modelu dziedziczącym po ActiveRecord.

Przykład:

class CoolClass < ActiveRecord::Base
    acts_as_random
end

Do naszego modelu dopisujemy tylko acts_as_random i już cała magia działa :) nic więcej nie trzeba robić a ID są generowane losowo, co można sprawdzić zapisując obiekt i wyświetlając jego id.

Oczywiście rozwiązanie moje nie jest wolne od wad. Przede wszystkim przy każdym zapisaniu obiektu następuje odpytanie czy obiekt o takim ID już nie istnieje (czyli +1 zapytanie do każdego save). Jeśli mamy system który generuje bardzo dużo danych warto pomyśleć nad GUIDami.

Kolejnym minusem jest to że jeśli będziemy mieli przykładowo 100 000 wpisów w danej tabeli a losujemy liczbę z przedziało 0..1 000 000 to mamy 10% szans że wylosujemy taką która już jest. Sprawia to że proces losowania musi być ponowiony i musi nastąpić kolejna weryfikacja ID (już +2 w tym wypadku do save'a).

W większości wypadków jednak losowanie z 1 000 000 w zupełności starcza a jeśli nie to zawsze można poprawić mój kod i zamiast z 1 000 000 losować z większego zakresu.

Rozwiązanie to jest wygodne (jedna linijka w modelu) i działa w większości przypadków - nie przeszkadzając przy tym w aplikacji (no chyba że ktoś korzysta z order by id).

Nie chroni ono w 100% jednak znacząco utrudnia życie potencjalnym "użyszkodnikom". Bo mając wpis o ID = 142145 następny może mieć wartość 235652, przez co szansa "trafienia" drastycznie spada w porównaniu z autoincrement.

Stosując to rozwiązanie oraz weryfikację uprawnień raczej nie powinno być problemów.

Jeśli więc tworzysz obiekty dużo rzadziej niż je wyświetlasz to to rozwiązanie jest dla Ciebie :)

Kod pluginu: acts_as_random