Safety Cribs: JWT



Banyak aplikasi menggunakan JSON Web Tokens (JWT) untuk memungkinkan klien mengidentifikasi diri mereka untuk pertukaran informasi lebih lanjut setelah otentikasi.

JSON Web Token adalah standar terbuka (RFC 7519) yang mendefinisikan cara yang ringkas dan mandiri untuk mentransfer informasi antar pihak secara aman sebagai objek JSON.


Informasi ini diverifikasi dan dapat diandalkan karena ditandatangani secara digital.
JWT dapat ditandatangani menggunakan rahasia (menggunakan algoritma HMAC) atau pasangan kunci publik / pribadi menggunakan RSA atau ECDSA.

JSON Web Token digunakan untuk mengirimkan informasi mengenai identitas dan karakteristik klien. "Wadah" ini ditandatangani oleh server sehingga klien tidak mengganggu dan tidak dapat mengubah, misalnya, data identifikasi atau karakteristik apa pun (misalnya, peran dari pengguna yang sederhana menjadi administrator atau mengubah login klien).

Token ini dibuat jika otentikasi berhasil dan diperiksa oleh server sebelum memulai setiap permintaan klien. Token digunakan oleh aplikasi sebagai "kartu identitas" klien (sebuah wadah dengan semua informasi tentangnya). Server memiliki kemampuan untuk memverifikasi validitas dan integritas token dengan cara yang aman. Ini memungkinkan aplikasi menjadi stateless (aplikasi stateless tidak menyimpan data klien yang dihasilkan dalam satu sesi untuk digunakan dalam sesi berikutnya dengan klien ini (setiap sesi independen)), dan proses otentikasi independen dari layanan yang digunakan (dalam arti bahwa teknologi klien dan server dapat bervariasi, termasuk bahkan saluran transportasi, meskipun HTTP paling sering digunakan).

Pertimbangan untuk Menggunakan JWT


Bahkan jika token JWT mudah digunakan dan memungkinkan Anda untuk menyediakan layanan (terutama REST) ​​tanpa status (stateless), solusi ini tidak cocok untuk semua aplikasi, karena ia datang dengan beberapa peringatan, seperti masalah menyimpan token.

Jika aplikasi tidak harus sepenuhnya tanpa kewarganegaraan, maka Anda dapat mempertimbangkan untuk menggunakan sistem sesi tradisional yang disediakan oleh semua platform web. Namun, untuk aplikasi stateless, JWT adalah opsi yang baik jika diterapkan dengan benar.

Masalah dan Serangan JWT


Menggunakan Algoritma Hash NONE


Serangan serupa terjadi ketika penyerang mengubah token dan juga mengubah algoritma hashing ("alg") untuk menunjukkan melalui kata kunci yang tidak ada integritas integritas token telah diverifikasi. Beberapa perpustakaan melihat token yang ditandatangani menggunakan algoritme tidak ada sebagai token yang valid dengan tanda tangan yang diverifikasi, sehingga penyerang dapat mengubah payload token dan aplikasi akan mempercayai token.

Untuk mencegah serangan, Anda harus menggunakan perpustakaan JWT, yang tidak terpengaruh oleh kerentanan ini. Selain itu, selama validasi token, Anda harus secara eksplisit meminta penggunaan algoritma yang diharapkan.

Contoh Implementasi:

//  HMAC   String   JVM private transient byte[] keyHMAC = ...; ... //        //    HMAC-256 - JWTVerifier verifier = JWT.require(Algorithm.HMAC256(keyHMAC)).build(); //   DecodedJWT decodedToken = verifier.verify(token); 

Intersepsi token


Serangan terjadi ketika token telah dicegat atau dicuri oleh penyerang dan ia menggunakannya untuk mendapatkan akses ke sistem menggunakan kredensial pengguna tertentu.

Perlindungan terdiri dari menambahkan "konteks pengguna" ke token. Konteks pengguna akan terdiri dari informasi berikut:

  1. String acak yang dihasilkan pada tahap otentikasi dan disertakan dalam token, dan juga dikirim ke klien sebagai cookie yang lebih aman (flag: HttpOnly + Secure + SameSite + awalan cookie).
  2. Hash SHA256 dari string acak akan disimpan dalam token sehingga setiap masalah XSS tidak akan memungkinkan penyerang membaca nilai string acak dan mengatur cookie yang diharapkan.

Alamat IP tidak akan digunakan dalam konteks, karena ada situasi di mana alamat IP dapat berubah selama satu sesi, misalnya, ketika pengguna mengakses aplikasi melalui ponselnya. Kemudian alamat IP terus berubah secara sah. Selain itu, menggunakan alamat IP berpotensi menyebabkan masalah pada tingkat kepatuhan dengan GDPR Eropa.

Jika selama verifikasi token token yang diterima tidak mengandung konteks yang benar, itu harus ditolak.
Contoh Implementasi:

Kode untuk membuat token setelah otentikasi berhasil:

 //  HMAC   String   JVM private transient byte[] keyHMAC = ...; //    private SecureRandom secureRandom = new SecureRandom(); ... //   ,     byte[] randomFgp = new byte[50]; secureRandom.nextBytes(randomFgp); String userFingerprint = DatatypeConverter.printHexBinary(randomFgp); //    cookie String fingerprintCookie = "__Secure-Fgp=" + userFingerprint + "; SameSite=Strict; HttpOnly; Secure"; response.addHeader("Set-Cookie", fingerprintCookie); // SHA256          // (  )  XSS      //     cookie MessageDigest digest = MessageDigest.getInstance("SHA-256"); byte[] userFingerprintDigest = digest.digest(userFingerprint.getBytes("utf-8")); String userFingerprintHash = DatatypeConverter.printHexBinary(userFingerprintDigest); //      15     Calendar c = Calendar.getInstance(); Date now = c.getTime(); c.add(Calendar.MINUTE, 15); Date expirationDate = c.getTime(); Map<String, Object> headerClaims = new HashMap<>(); headerClaims.put("typ", "JWT"); String token = JWT.create().withSubject(login) .withExpiresAt(expirationDate) .withIssuer(this.issuerID) .withIssuedAt(now) .withNotBefore(now) .withClaim("userFingerprint", userFingerprintHash) .withHeader(headerClaims) .sign(Algorithm.HMAC256(this.keyHMAC)); 


Kode untuk memverifikasi validitas token:
 //  HMAC   String   JVM private transient byte[] keyHMAC = ...; ... //     cookie String userFingerprint = null; if (request.getCookies() != null && request.getCookies().length > 0) { List<Cookie> cookies = Arrays.stream(request.getCookies()).collect(Collectors.toList()); Optional<Cookie> cookie = cookies.stream().filter(c -> "__Secure-Fgp" .equals(c.getName())).findFirst(); if (cookie.isPresent()) { userFingerprint = cookie.get().getValue(); } } //  SHA256      cookie  //       MessageDigest digest = MessageDigest.getInstance("SHA-256"); byte[] userFingerprintDigest = digest.digest(userFingerprint.getBytes("utf-8")); String userFingerprintHash = DatatypeConverter.printHexBinary(userFingerprintDigest); //      JWTVerifier verifier = JWT.require(Algorithm.HMAC256(keyHMAC)) .withIssuer(issuerID) .withClaim("userFingerprint", userFingerprintHash) .build(); //   DecodedJWT decodedToken = verifier.verify(token); 

Pencabutan token oleh pengguna secara eksplisit


Karena token menjadi tidak valid hanya setelah kedaluwarsa, pengguna tidak memiliki fungsi bawaan yang memungkinkan Anda untuk membatalkan token secara eksplisit. Jadi, dalam kasus pencurian, pengguna tidak dapat menarik token sendiri dan kemudian memblokir penyerang.

Salah satu metode perlindungan adalah pengenalan daftar hitam token, yang akan cocok untuk mensimulasikan fungsi "log out" yang ada dalam sistem sesi tradisional.

Koleksi (dalam pengkodean SHA-256 dalam HEX) dari token dengan tanggal pembatalan, yang harus melebihi periode validitas token yang diterbitkan, akan disimpan dalam daftar hitam.

Ketika pengguna ingin "logout", ia memanggil layanan khusus yang menambahkan token pengguna yang disediakan ke daftar hitam, yang mengarah pada pembatalan langsung token untuk digunakan lebih lanjut dalam aplikasi.

Contoh Implementasi:

Repositori daftar hitam:
Untuk penyimpanan daftar hitam terpusat, sebuah database dengan struktur berikut akan digunakan:

 create table if not exists revoked_token(jwt_token_digest varchar(255) primary key, revokation_date timestamp default now()); 

Manajemen pencabutan token:

 //    (logout). //  ,      //         . public class TokenRevoker { //    @Resource("jdbc/storeDS") private DataSource storeDS; //      public boolean isTokenRevoked(String jwtInHex) throws Exception { boolean tokenIsPresent = false; if (jwtInHex != null && !jwtInHex.trim().isEmpty()) { //   byte[] cipheredToken = DatatypeConverter.parseHexBinary(jwtInHex); //  SHA256   MessageDigest digest = MessageDigest.getInstance("SHA-256"); byte[] cipheredTokenDigest = digest.digest(cipheredToken); String jwtTokenDigestInHex = DatatypeConverter.printHexBinary(cipheredTokenDigest); //     try (Connection con = this.storeDS.getConnection()) { String query = "select jwt_token_digest from revoked_token where jwt_token_digest = ?"; try (PreparedStatement pStatement = con.prepareStatement(query)) { pStatement.setString(1, jwtTokenDigestInHex); try (ResultSet rSet = pStatement.executeQuery()) { tokenIsPresent = rSet.next(); } } } } return tokenIsPresent; } //    HEX      public void revokeToken(String jwtInHex) throws Exception { if (jwtInHex != null && !jwtInHex.trim().isEmpty()) { //   byte[] cipheredToken = DatatypeConverter.parseHexBinary(jwtInHex); //  SHA256   MessageDigest digest = MessageDigest.getInstance("SHA-256"); byte[] cipheredTokenDigest = digest.digest(cipheredToken); String jwtTokenDigestInHex = DatatypeConverter.printHexBinary(cipheredTokenDigest); //             //   if (!this.isTokenRevoked(jwtInHex)) { try (Connection con = this.storeDS.getConnection()) { String query = "insert into revoked_token(jwt_token_digest) values(?)"; int insertedRecordCount; try (PreparedStatement pStatement = con.prepareStatement(query)) { pStatement.setString(1, jwtTokenDigestInHex); insertedRecordCount = pStatement.executeUpdate(); } if (insertedRecordCount != 1) { throw new IllegalStateException("Number of inserted record is invalid," + " 1 expected but is " + insertedRecordCount); } } } } } 

Pengungkapan Token


Serangan ini terjadi ketika penyerang mendapatkan akses ke token (atau satu set token) dan mengekstrak informasi yang tersimpan di dalamnya (informasi tentang token JWT dikodekan menggunakan base64) untuk mendapatkan informasi tentang sistem. Informasi dapat berupa, misalnya, peran keamanan, format login, dll.

Metode perlindungannya cukup jelas dan terdiri dalam mengenkripsi token. Penting juga untuk melindungi data terenkripsi dari serangan menggunakan cryptanalysis. Untuk mencapai semua tujuan ini, algoritma AES-GCM digunakan, yang menyediakan Enkripsi terotentikasi dengan Data Terkait (AEAD). AEAD primitive menyediakan fungsionalitas enkripsi simetris yang diautentikasi. Implementasi primitif ini dilindungi dari serangan adaptif berdasarkan ciphertext yang dipilih. Saat mengenkripsi plaintext, Anda dapat menentukan data terkait yang harus diautentikasi tetapi tidak dienkripsi.

Artinya, enkripsi dengan data yang relevan memastikan keaslian dan integritas data, tetapi tidak kerahasiaannya.

Namun, harus dicatat bahwa enkripsi ditambahkan terutama untuk menyembunyikan informasi internal, tetapi sangat penting untuk mengingat bahwa perlindungan awal terhadap pemalsuan token JWT adalah tanda tangan, oleh karena itu, tanda tangan token dan verifikasi harus selalu digunakan.

Penyimpanan token sisi klien


Jika aplikasi menyimpan token sehingga satu atau lebih situasi berikut terjadi:

  • token secara otomatis dikirim oleh browser (penyimpanan cookie);
  • token diperoleh bahkan jika browser dihidupkan ulang (menggunakan wadah localStorage browser);
  • token diperoleh dalam kasus serangan XSS (cookie tersedia untuk kode JavaScript atau token yang disimpan di localStorage atau sessionStorage).

Untuk mencegah serangan:

  1. Simpan token di browser menggunakan wadah sessionStorage.
  2. Tambahkan ke tajuk Otorisasi menggunakan skema Bearer. Judulnya harus seperti ini:

     Authorization: Bearer <token> 
  3. Tambahkan informasi sidik jari ke token.

Dengan menyimpan token dalam wadah sessionStorage, ia menyediakan token untuk pencurian dalam kasus XSS. Namun, sidik jari yang ditambahkan ke token mencegah penyerang menggunakan kembali token yang dicuri di komputernya. Untuk menutup area penggunaan maksimum untuk penyerang, tambahkan Kebijakan Keamanan Konten untuk membatasi konteks eksekusi.

Masih ada kasus di mana penyerang menggunakan konteks penelusuran pengguna sebagai server proxy untuk menggunakan aplikasi target melalui pengguna yang sah, tetapi Kebijakan Keamanan Konten dapat mencegah komunikasi dengan domain yang tidak terduga.

Dimungkinkan juga untuk mengimplementasikan layanan otentikasi sehingga token dikeluarkan di dalam cookie aman, tetapi dalam hal ini, perlindungan terhadap CSRF harus dilaksanakan.

Menggunakan kunci yang lemah untuk membuat token


Jika rahasia yang digunakan dalam kasus algoritma HMAC-SHA256, yang diperlukan untuk menandatangani token, lemah, maka dapat diretas (diambil dengan menggunakan serangan brute force). Akibatnya, seorang penyerang dapat memalsukan token yang valid dan sewenang-wenang dalam hal tanda tangan.

Untuk mencegah masalah ini, Anda harus menggunakan kunci rahasia yang kompleks: alfanumerik (campuran huruf) + karakter khusus.

Karena kunci hanya diperlukan untuk perhitungan komputer, ukuran kunci rahasia dapat melebihi 50 posisi.

Sebagai contoh:

 A&'/}Z57M(2hNg=;LE?~]YtRMS5(yZ<vcZTA3N-($>2j:ZeX-BGftaVk`)jKP~q?,jk)EMbgt*kW' 

Untuk menilai kompleksitas kunci rahasia yang digunakan untuk penandatanganan token Anda, Anda dapat menerapkan serangan kamus kata sandi ke token dalam kombinasi dengan JWT API.

Source: https://habr.com/ru/post/id457090/


All Articles