Cara menambahkan token pemalsuan permintaan lintas situs (CSRF) dengan benar menggunakan PHP

96

Saya mencoba menambahkan beberapa keamanan ke formulir di situs web saya. Salah satu formulir menggunakan AJAX dan yang lainnya adalah formulir "hubungi kami" langsung. Saya mencoba menambahkan token CSRF. Masalah yang saya hadapi adalah bahwa token hanya muncul di "nilai" HTML beberapa waktu. Sisa waktunya, nilainya kosong. Berikut adalah kode yang saya gunakan pada formulir AJAX:

PHP:

if (!isset($_SESSION)) {
    session_start();
$_SESSION['formStarted'] = true;
}
if (!isset($_SESSION['token']))
{$token = md5(uniqid(rand(), TRUE));
$_SESSION['token'] = $token;

}

HTML

 <form>
//...
<input type="hidden" name="token" value="<?php echo $token; ?>" />
//...
</form>

Ada saran?

Ken
sumber
Penasaran saja, untuk apa token_timedigunakan?
zerkms
@zerkms Saat ini saya tidak menggunakan token_time. Saya akan membatasi waktu di mana token valid, tetapi belum sepenuhnya menerapkan kode. Demi kejelasan, saya telah menghapusnya dari pertanyaan di atas.
Ken
1
@Ken: jadi pengguna bisa mendapatkan kasus ketika dia membuka formulir, mempostingnya dan mendapatkan token yang tidak valid? (karena telah dibatalkan)
zerkms
@zerkms: Terima kasih, tapi saya agak bingung. Adakah kesempatan Anda bisa memberi saya contoh?
Ken
2
@En: pasti. Misalkan token kedaluwarsa pada 10:00 pagi. Sekarang jam 09:59. Pengguna membuka formulir dan mendapatkan token (yang masih valid). Kemudian pengguna mengisi formulir selama 2 menit, dan mengirimkannya. Selama jam 10:01 sekarang - token dianggap tidak valid, sehingga pengguna mendapatkan kesalahan formulir.
zerkms

Jawaban:

290

Untuk kode keamanan, jangan buat token Anda dengan cara ini: $token = md5(uniqid(rand(), TRUE));

Coba ini:

Menghasilkan Token CSRF

PHP 7

session_start();
if (empty($_SESSION['token'])) {
    $_SESSION['token'] = bin2hex(random_bytes(32));
}
$token = $_SESSION['token'];

Catatan: Salah satu proyek open source perusahaan saya adalah inisiatif untuk mendukung random_bytes()dan random_int()menjadi proyek PHP 5. Ini berlisensi MIT dan tersedia di Github dan Composer sebagai paragonie / random_compat .

PHP 5.3+ (atau dengan ext-mcrypt)

session_start();
if (empty($_SESSION['token'])) {
    if (function_exists('mcrypt_create_iv')) {
        $_SESSION['token'] = bin2hex(mcrypt_create_iv(32, MCRYPT_DEV_URANDOM));
    } else {
        $_SESSION['token'] = bin2hex(openssl_random_pseudo_bytes(32));
    }
}
$token = $_SESSION['token'];

Memverifikasi Token CSRF

Jangan hanya menggunakan ==atau bahkan ===, gunakan hash_equals()(hanya PHP 5.6+, tetapi tersedia untuk versi sebelumnya dengan pustaka hash-compat ).

if (!empty($_POST['token'])) {
    if (hash_equals($_SESSION['token'], $_POST['token'])) {
         // Proceed to process the form data
    } else {
         // Log this as a warning and keep an eye on these attempts
    }
}

Melangkah Lebih Jauh dengan Token Per-Form

Anda selanjutnya dapat membatasi token agar hanya tersedia untuk formulir tertentu dengan menggunakan hash_hmac(). HMAC adalah fungsi hash berkunci tertentu yang aman digunakan, bahkan dengan fungsi hash yang lebih lemah (misalnya MD5). Namun, saya merekomendasikan menggunakan keluarga SHA-2 dari fungsi hash sebagai gantinya.

Pertama, buat token kedua untuk digunakan sebagai kunci HMAC, lalu gunakan logika seperti ini untuk merendernya:

<input type="hidden" name="token" value="<?php
    echo hash_hmac('sha256', '/my_form.php', $_SESSION['second_token']);
?>" />

Dan kemudian menggunakan operasi yang kongruen saat memverifikasi token:

$calc = hash_hmac('sha256', '/my_form.php', $_SESSION['second_token']);
if (hash_equals($calc, $_POST['token'])) {
    // Continue...
}

Token yang dihasilkan untuk satu formulir tidak dapat digunakan kembali dalam konteks lain tanpa diketahui $_SESSION['second_token']. Anda harus menggunakan token terpisah sebagai kunci HMAC daripada yang baru saja Anda jatuhkan di halaman.

Bonus: Pendekatan Hibrid + Integrasi Ranting

Siapa pun yang menggunakan mesin template Twig dapat memanfaatkan strategi ganda yang disederhanakan dengan menambahkan filter ini ke lingkungan Twig mereka:

$twigEnv->addFunction(
    new \Twig_SimpleFunction(
        'form_token',
        function($lock_to = null) {
            if (empty($_SESSION['token'])) {
                $_SESSION['token'] = bin2hex(random_bytes(32));
            }
            if (empty($_SESSION['token2'])) {
                $_SESSION['token2'] = random_bytes(32);
            }
            if (empty($lock_to)) {
                return $_SESSION['token'];
            }
            return hash_hmac('sha256', $lock_to, $_SESSION['token2']);
        }
    )
);

Dengan fungsi Twig ini, Anda dapat menggunakan kedua token tujuan umum seperti:

<input type="hidden" name="token" value="{{ form_token() }}" />

Atau varian yang dikunci:

<input type="hidden" name="token" value="{{ form_token('/my_form.php') }}" />

Twig hanya berhubungan dengan rendering template; Anda masih harus memvalidasi token dengan benar. Menurut pendapat saya, strategi Twig menawarkan fleksibilitas dan kesederhanaan yang lebih besar, sambil mempertahankan kemungkinan untuk keamanan maksimum.


Token CSRF Sekali Pakai

Jika Anda memiliki persyaratan keamanan bahwa setiap token CSRF diizinkan untuk digunakan tepat sekali, strategi paling sederhana membuatnya kembali setelah setiap validasi berhasil. Namun, hal itu akan membatalkan setiap token sebelumnya yang tidak cocok dengan orang-orang yang menjelajahi banyak tab sekaligus.

Paragon Initiative Enterprises memiliki perpustakaan Anti-CSRF untuk kasus sudut ini. Ia bekerja dengan token sekali pakai per bentuk, secara eksklusif. Ketika cukup token disimpan dalam data sesi (konfigurasi default: 65535), itu akan menggilir token tertua yang belum ditebus terlebih dahulu.

Scott Arciszewski
sumber
bagus, tetapi bagaimana cara mengubah $ token setelah pengguna mengirimkan formulir? dalam kasus Anda, satu token digunakan untuk sesi pengguna.
Akam
1
Perhatikan dengan saksama bagaimana github.com/paragonie/anti-csrf diterapkan. Tokennya hanya sekali pakai, tetapi menyimpan banyak.
Scott Arciszewski
@ScottArciszewski Apa pendapat Anda tentang membuat intisari pesan dari id sesi dengan rahasia dan kemudian membandingkan intisari token CSRF yang diterima dengan hashing lagi id sesi dengan rahasia saya sebelumnya? Saya harap Anda mengerti apa yang saya maksud.
MNR
1
Saya memiliki pertanyaan tentang Memverifikasi Token CSRF. Saya jika $ _POST ['token'] kosong, sebaiknya jangan lanjutkan, karena permintaan posting ini dikirim tanpa token, bukan?
Hiroki
1
Karena itu akan di-echo-kan ke dalam bentuk HTML, dan Anda ingin itu tidak dapat diprediksi sehingga penyerang tidak bisa hanya memalsukannya. Anda benar-benar menerapkan otentikasi tantangan-respons di sini, bukan hanya "ya, formulir ini sah" karena penyerang dapat memalsukannya.
Scott Arciszewski
24

Peringatan Keamanan : md5(uniqid(rand(), TRUE))bukanlah cara yang aman untuk menghasilkan nomor acak. Lihat jawaban ini untuk informasi lebih lanjut dan solusi yang memanfaatkan generator nomor acak yang aman secara kriptografis.

Sepertinya Anda membutuhkan yang lain dengan if.

if (!isset($_SESSION['token'])) {
    $token = md5(uniqid(rand(), TRUE));
    $_SESSION['token'] = $token;
    $_SESSION['token_time'] = time();
}
else
{
    $token = $_SESSION['token'];
}
dataage
sumber
11
Catatan: Saya tidak akan mempercayai md5(uniqid(rand(), TRUE));konteks keamanan.
Scott Arciszewski
2

Variabel $tokentidak diambil dari sesi saat berada di sana

Dani
sumber