Cách thêm đúng mã thông báo giả mạo yêu cầu chéo trang (CSRF) bằng PHP


96

Tôi đang cố gắng thêm một số bảo mật cho các biểu mẫu trên trang web của mình. Một trong các biểu mẫu sử dụng AJAX và biểu mẫu kia là biểu mẫu "liên hệ với chúng tôi" đơn giản. Tôi đang cố thêm mã thông báo CSRF. Vấn đề tôi đang gặp phải là đôi khi mã thông báo chỉ hiển thị trong "giá trị" HTML. Phần còn lại của thời gian, giá trị trống. Đây là mã tôi đang sử dụng trên biểu mẫu 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>

Bất kỳ đề xuất?


Chỉ tò mò, những gì token_timeđược sử dụng cho?
zerkms

@zerkms Tôi hiện không sử dụng token_time. Tôi đã định giới hạn thời gian mà mã thông báo hợp lệ, nhưng vẫn chưa triển khai đầy đủ mã. Để rõ ràng, tôi đã xóa nó khỏi câu hỏi ở trên.
Ken

1
@Ken: vậy người dùng có thể gặp trường hợp khi anh ấy mở một biểu mẫu, đăng nó và nhận được mã thông báo không hợp lệ không? (vì nó đã được vô hiệu)
zerkms

@zerkms: Cảm ơn bạn, nhưng tôi hơi bối rối. Bất kỳ cơ hội nào bạn có thể cung cấp cho tôi một ví dụ?
Ken

2
@Ken: chắc chắn. Giả sử mã thông báo hết hạn vào lúc 10:00 sáng. Bây giờ là 09:59 sáng. Người dùng mở biểu mẫu và nhận mã thông báo (vẫn còn hiệu lực). Sau đó, người dùng điền vào biểu mẫu trong 2 phút và gửi nó. Miễn là bây giờ là 10:01 sáng - mã thông báo đang được coi là không hợp lệ, do đó người dùng gặp lỗi biểu mẫu.
zerkms

Câu trả lời:


286

Đối với mã bảo mật, vui lòng không tạo mã thông báo của bạn theo cách này: $token = md5(uniqid(rand(), TRUE));

Thử thứ này đi:

Tạo mã thông báo CSRF

PHP 7

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

Ghi chú bên lề: Một trong những dự án mã nguồn mở của chủ nhân của tôi là một sáng kiến ​​để hỗ trợ random_bytes()random_int()đưa vào các dự án PHP 5. Nó đã được MIT cấp phép và có sẵn trên Github và Composer dưới dạng paragonie / random_compat .

PHP 5.3+ (hoặc với 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'];

Xác minh mã thông báo CSRF

Đừng chỉ sử dụng ==hoặc thậm chí ===, hãy sử dụng hash_equals()(chỉ dành cho PHP 5.6+, nhưng có sẵn cho các phiên bản trước đó với thư viện 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
    }
}

Tiến xa hơn với Token Per-Form

Bạn có thể hạn chế thêm mã thông báo để chỉ có sẵn cho một hình thức cụ thể bằng cách sử dụng hash_hmac(). HMAC là một hàm băm có khóa đặc biệt an toàn để sử dụng, ngay cả với các hàm băm yếu hơn (ví dụ: MD5). Tuy nhiên, tôi khuyên bạn nên sử dụng họ hàm băm SHA-2 để thay thế.

Đầu tiên, tạo mã thông báo thứ hai để sử dụng làm khóa HMAC, sau đó sử dụng logic như sau để hiển thị nó:

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

Và sau đó sử dụng thao tác đồng dư khi xác minh mã thông báo:

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

Các mã thông báo được tạo cho một biểu mẫu không thể được sử dụng lại trong ngữ cảnh khác mà không biết $_SESSION['second_token']. Điều quan trọng là bạn phải sử dụng mã thông báo riêng biệt làm khóa HMAC thay vì mã bạn vừa thả trên trang.

Phần thưởng: Phương pháp tiếp cận lai + Tích hợp cành cây

Bất kỳ ai sử dụng công cụ tạo khuôn mẫu Twig đều có thể hưởng lợi từ chiến lược kép được đơn giản hóa bằng cách thêm bộ lọc này vào môi trường Twig của họ:

$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']);
        }
    )
);

Với chức năng Twig này, bạn có thể sử dụng cả hai mã thông báo mục đích chung như vậy:

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

Hoặc biến thể bị khóa:

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

Twig chỉ quan tâm đến việc kết xuất khuôn mẫu; bạn vẫn phải xác nhận đúng các mã thông báo. Theo tôi, chiến lược Twig mang lại sự linh hoạt và đơn giản hơn, trong khi vẫn duy trì khả năng bảo mật tối đa.


Mã thông báo CSRF sử dụng một lần

Nếu bạn có yêu cầu bảo mật rằng mỗi mã thông báo CSRF được phép sử dụng chính xác một lần, thì chiến lược đơn giản nhất sẽ tạo lại nó sau mỗi lần xác thực thành công. Tuy nhiên, làm như vậy sẽ làm mất hiệu lực mọi mã thông báo trước đó vốn không kết hợp tốt với những người duyệt nhiều tab cùng một lúc.

Paragon Initiative Enterprises duy trì một thư viện Anti-CSRF cho những trường hợp góc khuất này. Nó hoạt động với các mã thông báo một lần sử dụng cho mỗi biểu mẫu. Khi đủ số lượng mã thông báo được lưu trữ trong dữ liệu phiên (cấu hình mặc định: 65535), nó sẽ chuyển ra các mã thông báo cũ nhất chưa được quy đổi trước.


tốt, nhưng làm thế nào để thay đổi $ token sau khi người dùng gửi biểu mẫu? trong trường hợp của bạn, một mã thông báo được sử dụng cho phiên người dùng.
Akam

1
Xem kỹ cách triển khai github.com/paragonie/anti-csrf . Các mã thông báo chỉ sử dụng một lần, nhưng nó lưu trữ nhiều.
Scott Arciszewski

@ScottArciszewski Bạn nghĩ gì về việc tạo thông báo thông báo từ id phiên với một bí mật và sau đó so sánh thông báo mã thông báo CSRF đã nhận với việc băm lại id phiên với bí mật trước đó của tôi? Tôi hi vọng bạn hiểu những gì tôi nói.
MNR

1
Tôi có câu hỏi về việc Xác minh Mã thông báo CSRF. Tôi nếu $ _POST ['token'] trống, chúng ta không nên tiếp tục, vì yêu cầu bài đăng này được gửi mà không có mã thông báo, phải không?
Hiroki

1
Bởi vì nó sẽ được lặp lại trong biểu mẫu HTML và bạn muốn nó không thể đoán trước được nên những kẻ tấn công không thể giả mạo nó. Bạn đang thực sự triển khai xác thực phản hồi thử thách ở đây, không chỉ đơn giản là "vâng, biểu mẫu này hợp pháp" bởi vì kẻ tấn công có thể giả mạo điều đó.
Scott Arciszewski

24

Cảnh báo bảo mật : md5(uniqid(rand(), TRUE))không phải là cách an toàn để tạo số ngẫu nhiên. Xem câu trả lời này để biết thêm thông tin và giải pháp thúc đẩy trình tạo số ngẫu nhiên an toàn bằng mật mã.

Có vẻ như bạn cần một cái khác với if của bạn.

if (!isset($_SESSION['token'])) {
    $token = md5(uniqid(rand(), TRUE));
    $_SESSION['token'] = $token;
    $_SESSION['token_time'] = time();
}
else
{
    $token = $_SESSION['token'];
}

11
Lưu ý: Tôi sẽ không tin tưởng vào md5(uniqid(rand(), TRUE));bối cảnh bảo mật.
Scott Arciszewski

2

Biến $tokenkhông được truy xuất từ ​​phiên khi nó ở trong đó

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.