How to securely store JWT tokens

Last time I worked with some JWT tokens and had to resolve some issues related to security. Right now a lot of web applications and not only, because also mobile apps use these tokens to prove that we are valid and correctly logged-in users. The idea of JWT is clear, right and useful, but there are some dangerous traps during implementation on web applications. The main question is: where should we store JWT tokens to make them secure and still usable?

localStorage and sessionStorage are not good

On mobile clients, it is not a problem, because we can save them in some secure area. The thing is a bit complicated with web applications. If you already thought about this question and looked for some solutions, you probably found a lot of articles about that and some of them recommend using localStorage. During the latest years web developers got a lot of new abilities and localStorage is one of them: simple and very quick to use, it’s just something like:

// Write
localStorage.setItem('foo', 'bar')

// Read
cost myValue = localStorage.get('foo')

The big additional advantage: it is persistent storage, so even if the user closes the browser and opens again, it will still be logged in. Great! So, why not, why not use it just to store our JWT token? The problem with this approach is the possibility of stealing a token using an XSS attack. XSS is an injection and execution of malicious code on a user web browser. If there is any possibility that someone will do that on your web application, then it will be possible to just execute code to steal a JWT token – because it is accessible from JavaScript level. 

Of course, if we allow anyone to make an XSS attack, we have bigger problems… but think about that: in a big, complex web application, can you guarantee that there is no such possibility? We can not think in such a way and just assume that everything will be ok. The better approach is to think that everything will be bad. It is a reason why we do not have to use localStorage for JWT tokens or any sensitive data. 

What about sessionStorage? It is very similar: provides a quick, easy way to store some data, but it is not persistent as localStorage. If the user open a new tab or just close and reopen the browser, there will be no data we saved. It may be good for let say banking web applications, but for most others, it will be a very bad user experience. Also, sessionStorage is still vulnerable to XSS attacks. Maybe store them just in memory? It may be a solution, but you will have additional issues with persistence, and it will be very difficult to resolve. Conclusion: such storage are not a good option. 

Solution: use old school cookies

Yep, cookies come to rescue. Many people think that is old school, not used anymore. But in reality, they are still very good options in such cases. Why? First: they offer persistence. When we set cookies, we can also control expiration time. They are available in next tabs with the same web application, also after reloading and browser restart. Second: if configured correctly, they are not vulnerable for XSS attack. We can set the httpOnly flag on the cookie and after that it will not be visible from JavaScript level anymore. Wait a moment… you can ask: if it will not be available, how can we use a token from such a cookie in the authorization header?

The answer is: it will not be possible, but it is not a problem, because browsers will attach these cookies in each request and your API should just check cookies. In reality, cookies are also header, just different and process is automatic, browser will do that for you without any additional code. So, if the user has been authenticated and the API has set a proper cookie with JWT token, it will be automatically sent back, even in background (XHR, fetch) requests. So your frontend application does not have to handle that anymore.

Third: if configured correctly, it will be limited to only secure connections. Right now, all apps with public access should use secure, encrypted connections. Using a secure flag you can define that cookie should be used only on such connections, so if anyone will try to use your application on standard HTTP, such cookie will be not available and not used. Of course, you should still use different methods to provide HTTPS-only traffic: upgrade / rewrite and also send HSTS headers to inform browsers about rules for your websites. 

What about CSRF/XSRF issues?

We decided to move all things to cookies, our JWT token is not vulnerable to XSS attack, and we are happy… But just for a moment, because we remembered about different attack surfaces related to cookies: CSRF. Our API sets cookies and it looks fine, but an attacker can right now just prepare a special website and command our clients to send unauthorized requests in the background. Browsers will automatically add cookies in such requests as mentioned above, so it looks like the biggest advantage is also the biggest drawback. What can you do in such a situation? The first thing is to provide strict CSRF protection: send a special token from API to the browser, then attach this token to requests and validate on API: it guarantees that only the real client (i.e. our web application) makes all requests. The problem with that option is maybe we will need to change a lot of places in our code, also handling background requests will be not easy. 

Fortunately, there is a second solution, much simpler. We can add another flag to our cookies: sameSite. This option allows us to block sending cookies cross-site. Default option is “None” and it does not block anything, cross-site is possible (also with CSRF possibility). The second option is “Lax” – with such configuration browsers will not send cookies for images and frames, so it will prevent CSRF attacks. If the user clicks the URL to our website on a completely different website (or email), cookies will be added, so everything will work correctly. The latest option is “Strict”: in that case cross-site cookies are completely blocked, the most secure, but in some cases a bit limited option.

Conclusion

Use cookies to store JWT tokens – always secure, always httpOnly, and with the proper same site flag. This configuration will secure your client’s data, it will prevent XSS and CSRF attack and also should simplify web application, because you do not have to care about using tokens manually on frontend code anymore. 

Set-Cookie: jwt=OUR_TOKEN_CONTENT; secure; httpOnly; sameSite=Lax;

What about refresh tokens?

Exactly the same thing – they are also super important because they allow users to generate new JWT. If you use this token to determine: is a user logged in or not, you can stop doing that. Just save such information in localStorage (simple bool) if you need. It is not a problem: if the token expires, your API will inform the frontend about unauthorized requests, and then you can call the proper service to refresh the token. If the refresh token is expired or invalid, this service will inform you about that, so you will know that the user should be redirected to the login page. Yep, all these things without using tokens manually, it will be completely transparent from web application level.

Authorization service on different domain

Bonus case: what if our authorization service is on a completely different domain or subdomain? Let’s say we use oauth.mydomain to display and handle login pages, but our users will use different websites like app.mydomain. In this scenario, cookies will not work correctly, so what can we do? There are two options. First is to move login into the app subdomain or just the main domain (then cookies will be available on all subdomains – you can still control that) – may in many cases it may be not possible or just very, very complicated because we want to support many systems. Second option: just provide a proper URL on your app and use it like a “middleware” to call authorization service, so it will be Web App ⇔ API ⇔ Authorization Service. If oauth respond with correct data, you can set proper cookies for your current domain. 

Szyfrowanie SSD — fakty i mity

Dyski SSD coraz intensywniej wkraczają w powszechne użycie. W ich wypadku stosunek pojemności do ceny nadal jest bardzo niekorzystny względem HDD, jednakże wydajność jaką ze sobą niosą, bardzo często to usprawiedliwia — ktoś, kto spróbuje SSD nie chce wracać do dysków magnetycznych, bo to zupełnie inny komfort pracy dzięki niesamowicie niskim czasom dostępu, ale także ciszy. Producenci tymczasem prowadzą ciągły wyścig na kolejne MB/s w specyfikacjach. Coraz częściej widzi się również informację o sprzętowym szyfrowaniu danych przy użyciu AESa. Co to oznacza, jak tego używać, no i czy czasem nie jest to jedynie marketingowy bełkot o bezpieczeństwie danych, naszych danych?

Po co szyfrować i dlaczego na poziomie dysku?

Na początek tradycyjnie trzeba sobie zadać pytanie: po co szyfrować i czy ja, jako użytkownik, na pewno tego potrzebuję. Ogólnie mówiąc, zaszyfrowane dane w wypadku kradzieży urządzenia, np. laptopa będą bezpieczne. Oczywiście z pewnymi wyjątkami: chronić nie może ich jakieś banalne hasło, a w wypadku laptopów nie powinniśmy używać hibernacji i usypiania aby ustrzec się przed próbami odzyskania kluczy szyfrujących np. z pamięci RAM. Jakiś czas temu, w innym artykule opisywałem różnicę w wydajności oprogramowania do szyfrowania w postaci BitLockera oraz TrueCrypta. Czym to się różni od szyfrowania bezpośrednio na poziomie dysku?

Wymienione programy robiły to w sposób programowy. Potrafiły do tego celu wykorzystać np. instrukcje przyśpieszające szyfrowanie AES zaszyte w nowych procesorach, ale cały czas działały tak naprawdę na strukturze logicznej dysku tj. istniejących partycjach. Ma to pewną wadę. Załóżmy, że obok Windowsa mamy jakąś dystrybucję Linuksa. W tym wypadku musimy szyfrować je zupełnie osobno, oprogramowaniem dla nich przewidzianym.

Szyfrowanie na poziomie dysku oznacza, że napęd posiada dedykowany układ zajmujący się szyfrowaniem i robi to na całym dysku niezależnie od tego, jaki system i dane na nim posiadamy. Z informacji które udało mi się znaleźć wynika, że w przypadku szyfrowania na dyskach SSD, mamy do czynienia z ciągłym szyfrowaniem w locie. Kupujemy dysk, wkładamy do komputera, cokolwiek byśmy nie robili, dane są zapisywane w postaci szyfrowanej. Poza tym, działa to o wiele szybciej niż szyfrowanie programowe, nie wpływa również na obciążenie procesora głównego, bo szyfrowaniem zajmuje się wyspecjalizowany do tego układ. Jest jeszcze jedna zaleta, przy operacji Secure Erase, która ma na celu bezpieczne usunięcie danych z dysku, wystarczy zmiana kluczy szyfrujących… Jeżeli ich nie ma, zawartość dysku zamienia się w śmieci. Można więc naprawdę ekspresowo „usuwać dane”.

Jak to powinno być zrobione?

Jakim cudem to więc działa i nie wymaga żadnej autoryzacji ze strony użytkownika? Wbrew pozorom, w dosyć prosty sposób. Klucze szyfrujące są zapisane w kontrolerze dysku i domyślnie po prostu dostępne. Po uruchomieniu komputera kontroler z nich korzysta, również w locie deszyfruje dane i dlatego mamy do nich dostęp. Tutaj przechodzimy do całego dylematu związanego z tym, co mówią nam marketingowcy, a jak wygląda rzeczywistość. Producenci zapewniają, że szyfrowanie na poziomie dysku pozwala na zabezpieczenie naszych danych, ale nikt nie kwapi się z wyjaśnieniem, jak to w ogóle zastosować – przecież jak napisałem, zaraz po zakupie nie ma faktycznej ochrony i przy kradzieży zupełnie nic nam to nie da. Odpowiedzią na pytanie jest HDD password / ATA password, a dokładnie, jego rozszerzona wersja w BIOSach z rozszerzeniem ATASX.

Dopiero ustawienie hasła do dysku pozwala faktycznie aktywować ochronę oferowaną przez wbudowane funkcje szyfrujące, bo wtedy klucze są szyfrowane i chronione właśnie tym hasłem. Niestety, pojawia się kolejny problem. Czy każdy z producentów odpowiednio z tego korzysta? Pomyślny: skoro szyfrowanie odbywa się z użyciem kluczy, to co powinno robić hasło, oprócz blokady dysku? Powinno jak wspomniałem zabezpieczyć klucze szyfrujące, aby nie były one łatwe do odczytania, poprzez ich zaszyfrowanie. Samo hasło powinno być natomiast zapisane jako nieodwracalny hash – weryfikowane przy wprowadzaniu i jeżeli poprawne, używane już te podane przez użytkownika do odszyfrowania kluczy. Okazuje się, że nie każdy producent robi to w odpowiedni sposób, co więcej, w większości wypadków jest naprawdę trudno znaleźć konkretne informacje na ten temat.

A jak to robią producenci?

Sprawę komplikuje fakt, że nie każdy laptop pozwala na wpisanie pełnego, 32-znakowego hasła. Sam miałem takie do dyspozycji dopiero po graniu nieoficjalnego, zmodyfikowanego BIOSu, a na innym laptopie Acera spotkałem się z możliwością ustawienia hasła jedynie 8-znakowego, które nie jest odpowiednim zabezpieczeniem. Wśród producentów bardzo dobrze sprawę opisuje Intel, który w swoich dyskach 320 oraz 520 właśnie poprzez to hasło zabezpiecza klucze szyfrujące. W tym wypadku mamy pewność, że wszystko gra, że dane są odpowiednio zabezpieczone. Plextor oferuje szyfrowanie w dyskach M5P (Pro) i do niedawna nie było wiadomo dokładnie jak to realizuje – obrazek na stronie producenta sugerował ochronę hasłem. Niestety wypowiedź jednego z techników firmy rozwiała te nadzieje, klucze nie są chronione hasłem.

Popularny Samsung 840 Pro również teoretycznie używa hasła HDD, ale nie ma żadnego potwierdzenia tych słów, tym bardziej że mówi się o wymogu stosowania płyty głównej z układem TPM, a to nie jest wymagane w standardzie (a więc Samsung robi to po swojemu). Na pewno źle jest to realizowane w dyskach OCZ – tak Agility jak i Vertex 2/3/4 ze względu na rozwiązanie w kontrolerach SandForce, które nie dość, że szyfruje tak naprawdę w AES-128, to na dodatek wiadomo, że hasło HDD nie zabezpiecza kluczy szyfrujących. W wypadku Kingstona V+200 mamy do czynienia z szyfrowaniem AES-128 zamiast AES-256, do tego nie wiadomo jak realizowanego.

Podsumowanie — co robić?

Co więc powinniśmy zrobić, skoro tyle tutaj niepewności? Przede wszystkim, BIOS komputera powinien pozwolić nam ustawić pełne, 32-znakowe hasło dysku. Bez tego nie ma możliwości o poprawnej implementacji tej funkcji. Dalej, wybór producenta… no i tutaj pewność mamy tylko w wypadku Intela. Nie chcę faworyzować jednej firmy, sam używam Plextora, no ale taka jest prawda, a informacji za mało. Innym wyjściem jest używanie szyfrowania programowego. Tutaj jednak pojawia się pytanie, czy możemy takim rozwiązaniem zaufać – wystarczy wspomnieć o ostatnich słowach jednego z pracowników Microsoftu, który wyznał, że był proszony o stworzenie backdoora dla BitLockera już w 2005 roku.

Oczywiście to, że w wypadku niektórych dysków klucze szyfrujące nie są chronione hasłem nie oznacza, że dane nie są bezpieczne. Po prostu nie wiadomo, jakie rozwiązania są stosowane, a w tym wypadku ciężko mówić o pełni bezpieczeństwa – nie wiadomo, czy kiedyś z tych firm nie wyciekną informacje pozwalające na uzyskanie dostępu do naszych danych. Nie wiadomo, czy takie dane nie znajdują się gdzieś na biurkach organizacji takich jak NSA…

Suma kontrolna pliku w PHP na przykładzie md5

W ostatnim czasie tworzyłem skrypt, który musiał badać sumy kontrolne plików i porównywać je z gotową listą – po prostu weryfikacja, czy pliki są oby na pewno w porządku, z użyciem hashy md5. Wszystko byłoby bardzo proste, gdyby nie fakt, że pliki bywały dosyć spore (nawet ponad 250 MB), a nie miałem żadnego wpływu na ustawienia z php.ini. Dlaczego o tym wspominam? Ano dlatego, że pierwsze co przyszło na myśl to użycie funkcji md5_file. W sieci można znaleźć również inne specyficzne rozwiązanie tj. wczytanie zawartości pliku, a następnie obliczenie przy pomocy funkcji md5() hasha dla tych danych:

$checksum = md5(file_get_contents($filename));

Niestety przy sporych plikach momentalnie przekraczamy limit zajętej pamięci, bo przecież ich zawartość leci właśnie tam. Takie rozwiązanie odpada. Lepszym wydawało się użycie wspomnianej funkcji md5_file, która pamięci nam tak nie zawala:

$checksum = md5_file($filename);

To niestety również nie było satysfakcjonującym wynikiem, bo o ile skrypt nie przekraczał limitu pamięci, o tyle często nie był w stanie wyrobić się z obliczeniem sumy w czasie jaki miał do dyspozycji (np. 60 sekund). Po prostu obliczenie sumy md5 za pośrednictwem PHP było za wolne. Kolejne rozwiązanie – sięgnięcie do shella:

$result = explode('= ',exec("openssl md5 $filename"));
$checksum = $result[1];

W tym wypadku obliczanie sumy kontrolnej trwało… ponad 3-4 razy krócej! Było to już więc całkiem niezłe rozwiązanie, oczywiście do pewnego rozmiaru. Okazało się jednak, że istnieje jeszcze szybsza metoda, skracająca ten czas jeszcze dwukrotnie, czasami nawet więcej. Zamiast używać openssl md5, można od razu użyć md5sum, dodatkowo z parametrem “b” tj. czytanie w trybie binarnym. Na koniec przedstawiam więc metodę, która w moim wypadku okazała się najszybsza, dodatkowo jest ona “skompletowana” poprzez przefiltrowanie zmiennej która idzie do funkcji shell_exec:

$result = explode(' ', exec('md5sum -b ' . escapeshellarg($filename)));
$checksum = $result[0];

Warto zwrócić uwagę na różnicę w pierwszym argumencie dla explode oraz sam dostęp do elementu tablicy z sumą kontrolną – oba polecenia zwracają dane w nieco inny sposób (oraz kolejności). To by by było na tyle. Dlaczego nie SHA albo CRC? Pierwszy z nich okazał się sporo wolniejszy, drugi nie odbiegał od md5 ale zwracał sumy w formacie dwuczłonowym, a mi zależało na jak najszybszym i najkrótszym otrzymaniu sum kontrolnych. Jeżeli znacie metody lepsze / szybsze, lub też gdzieś popełniłem błąd, poprawcie mnie.

Generator haseł – metodologia wyboru z zestawów

Dosyć często używam generatorów haseł. Jakoś nie mam talentu ani sposobu na wymyślanie jakichś finezyjnych, więc polegam na losowo wygenerowanych ciągach zawierających różne zestawy danych. Tradycyjnie takimi zestawami są np: małe litery, duże litery, cyfry, znaki interpunkcyjne oraz inne znaki specjalne. Używając kilku z narzędzi do generowania haseł dostępnych z poziomu www zauważyłem pewną “prawidłowość” – losowanie najprawdopodobniej wykonywane jest na sumie wybranych zbiorów. Przedstawienie bardziej-po-ludzku: załóżmy, że chcemy hasło 9 znakowe, zawierające duże/małe litery oraz cyfry. Przy wielokrotnym generowaniu hasła zauważamy, że cyfry występują nad wyraz rzadko. Często mają miejsce sytuacje, gdy nie jest wylosowana żadna, albo tylko jedna. Czasami trafiają się dwie, zaś większa ilość to istna egzotyka. Statystycznie średnio powinny być jednak 3 cyfry, bo mamy do dyspozycji 3 zbiory i 9 miejsc do wypełnienia. Tak się jednak nie dzieje…
Continue reading “Generator haseł – metodologia wyboru z zestawów”