วิธีเพิ่มโทเค็นการปลอมแปลงคำขอข้ามไซต์ (CSRF) อย่างถูกต้องโดยใช้ PHP


100

ฉันกำลังพยายามเพิ่มความปลอดภัยให้กับแบบฟอร์มบนเว็บไซต์ของฉัน รูปแบบหนึ่งใช้ AJAX และอีกแบบเป็นแบบฟอร์ม "ติดต่อเรา" ที่ตรงไปตรงมา ฉันกำลังพยายามเพิ่มโทเค็น CSRF ปัญหาที่ฉันพบคือโทเค็นจะแสดงใน "ค่า" HTML บางครั้งเท่านั้น เวลาที่เหลือค่าว่างเปล่า นี่คือรหัสที่ฉันใช้ในแบบฟอร์ม 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>

ข้อเสนอแนะใด ๆ ?


แค่อยากรู้ว่าtoken_timeใช้ทำอะไร?
zerkms

@zerkms ฉันไม่ได้ใช้งานtoken_timeอยู่ ฉันจะ จำกัด เวลาที่โทเค็นจะถูกต้อง แต่ยังไม่ได้ติดตั้งโค้ดอย่างสมบูรณ์ เพื่อความชัดเจนฉันได้ลบออกจากคำถามด้านบน
Ken

1
@ เคน: ดังนั้นผู้ใช้จะได้รับกรณีที่เขาเปิดแบบฟอร์มโพสต์และได้รับโทเค็นที่ไม่ถูกต้อง? (เนื่องจากไม่ถูกต้อง)
zerkms

@zerkms: ขอบคุณ แต่ฉันสับสนเล็กน้อย มีโอกาสที่คุณจะให้ฉันเป็นตัวอย่างได้หรือไม่?
Ken

2
@ เคน: แน่นอน สมมติว่าโทเค็นหมดอายุเวลา 10.00 น. ตอนนี้เป็นเวลา 09.59 น. ผู้ใช้เปิดแบบฟอร์มและรับโทเค็น (ซึ่งยังใช้ได้) จากนั้นผู้ใช้กรอกแบบฟอร์มเป็นเวลา 2 นาทีแล้วส่ง ตราบเท่าที่ตอนนี้เป็นเวลา 10:01 น. - โทเค็นถูกถือว่าไม่ถูกต้องดังนั้นผู้ใช้จึงได้รับข้อผิดพลาดแบบฟอร์ม
zerkms

คำตอบ:


304

สำหรับรหัสความปลอดภัยโปรดอย่าสร้างโทเค็นของคุณด้วยวิธีนี้: $token = md5(uniqid(rand(), TRUE));

ลองสิ่งนี้:

การสร้างโทเค็น CSRF

PHP 7

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

Sidenote: หนึ่งในโครงการโอเพ่นซอร์สของนายจ้างของฉันคือความคิดริเริ่มในการ backport random_bytes()และrandom_int()ในโครงการ PHP 5 มัน MIT ได้รับอนุญาตและมีอยู่บน Github และนักแต่งเพลงที่เป็นparagonie / random_compat

PHP 5.3+ (หรือ 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'];

การตรวจสอบโทเค็น CSRF

อย่าเพิ่งใช้==หรือแม้กระทั่ง===ใช้hash_equals()(PHP 5.6+ เท่านั้น แต่ใช้ได้กับเวอร์ชันก่อนหน้าที่มีไลบรารีที่เข้ากันได้กับแฮช )

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
    }
}

ก้าวต่อไปด้วย Per-Form Token

คุณสามารถ จำกัด โทเค็นเพิ่มเติมเพื่อให้ใช้ได้เฉพาะในรูปแบบเฉพาะโดยใช้ hash_hmac()ราชสกุลเพียงสามารถใช้ได้สำหรับรูปแบบเฉพาะโดยใช้ HMAC เป็นฟังก์ชันแฮชที่มีคีย์โดยเฉพาะซึ่งปลอดภัยต่อการใช้งานแม้จะมีฟังก์ชันแฮชที่อ่อนแอกว่า (เช่น MD5) อย่างไรก็ตามขอแนะนำให้ใช้ฟังก์ชันแฮชตระกูล SHA-2 แทน

ขั้นแรกให้สร้างโทเค็นที่สองเพื่อใช้เป็นคีย์ HMAC จากนั้นใช้ตรรกะเช่นนี้เพื่อแสดงผล:

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

จากนั้นใช้การดำเนินการที่สอดคล้องกันเมื่อตรวจสอบโทเค็น:

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

$_SESSION['second_token']ราชสกุลที่สร้างขึ้นสำหรับรูปแบบหนึ่งไม่สามารถนำกลับมาใช้ในบริบทอื่นโดยไม่ทราบว่าสิ่งสำคัญคือคุณต้องใช้โทเค็นแยกต่างหากเป็นคีย์ HMAC ไม่ใช่โทเค็นที่คุณเพิ่งวางลงบนเพจ

โบนัส: Hybrid Approach + Twig Integration

ใครก็ตามที่ใช้เครื่องมือสร้างเทมเพลต Twigจะได้รับประโยชน์จากกลยุทธ์คู่ที่เรียบง่ายโดยการเพิ่มตัวกรองนี้ในสภาพแวดล้อม Twig:

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

ด้วยฟังก์ชั่น Twig นี้คุณสามารถใช้โทเค็นเอนกประสงค์ทั้งสองแบบดังนี้:

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

หรือตัวแปรที่ถูกล็อค:

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

Twig เกี่ยวข้องกับการแสดงเทมเพลตเท่านั้น คุณยังต้องตรวจสอบโทเค็นอย่างถูกต้อง ในความคิดของฉันกลยุทธ์ Twig ให้ความยืดหยุ่นและความเรียบง่ายมากขึ้นในขณะที่ยังคงรักษาความเป็นไปได้เพื่อความปลอดภัยสูงสุด


โทเค็น CSRF แบบใช้ครั้งเดียว

หากคุณมีข้อกำหนดด้านความปลอดภัยว่าโทเค็น CSRF แต่ละรายการได้รับอนุญาตให้ใช้งานได้เพียงครั้งเดียวกลยุทธ์ที่ง่ายที่สุดจะสร้างใหม่หลังจากการตรวจสอบความถูกต้องสำเร็จแต่ละครั้ง อย่างไรก็ตามการทำเช่นนี้จะทำให้โทเค็นก่อนหน้าทุกรายการไม่ถูกต้องซึ่งไม่สามารถใช้ร่วมกับผู้ที่เรียกดูหลายแท็บพร้อมกันได้

Paragon Initiative Enterprises ดูแลห้องสมุดต่อต้าน CSRFสำหรับกรณีมุมเหล่านี้ ใช้งานได้กับโทเค็นแบบใช้ครั้งเดียวต่อรูปแบบโดยเฉพาะ เมื่อโทเค็นถูกเก็บไว้เพียงพอในข้อมูลเซสชัน (การกำหนดค่าเริ่มต้น: 65535) จะหมุนเวียนโทเค็นที่เก่าที่สุดที่ไม่ได้แลกออกมาก่อน


ดี แต่จะเปลี่ยนโทเค็น $ หลังจากที่ผู้ใช้ส่งแบบฟอร์มได้อย่างไร? ในกรณีของคุณหนึ่งโทเค็นที่ใช้สำหรับเซสชันผู้ใช้
Akam

1
ดูวิธีการใช้งานgithub.com/paragonie/anti-csrfอย่างใกล้ชิด โทเค็นเป็นแบบใช้ครั้งเดียว แต่เก็บไว้หลายรายการ
Scott Arciszewski

@ScottArciszewski คุณคิดอย่างไรเกี่ยวกับการสร้างการแยกข้อความจากรหัสเซสชันด้วยความลับจากนั้นเปรียบเทียบโทเค็น CSRF ที่ได้รับกับการแฮชรหัสเซสชันอีกครั้งกับความลับก่อนหน้าของฉัน ฉันหวังว่าคุณจะเข้าใจสิ่งที่ฉันหมายถึง
MNR

1
ฉันมีคำถามเกี่ยวกับการตรวจสอบโทเค็น CSRF ฉันถ้า $ _POST ['token'] ว่างเปล่าเราไม่ควรดำเนินการต่อเนื่องจากคำขอโพสต์นี้ถูกส่งโดยไม่มีโทเค็นใช่ไหม
Hiroki

1
เนื่องจากจะสะท้อนออกมาในรูปแบบ HTML และคุณต้องการให้ไม่สามารถคาดเดาได้ดังนั้นผู้โจมตีจึงไม่สามารถปลอมแปลงได้ คุณกำลังใช้การรับรองความถูกต้องเพื่อตอบสนองต่อการท้าทายที่นี่ไม่ใช่แค่ "ใช่แบบฟอร์มนี้ถูกต้อง" เพราะผู้โจมตีสามารถปลอมแปลงได้
Scott Arciszewski

24

คำเตือนด้านความปลอดภัย : md5(uniqid(rand(), TRUE))ไม่ใช่วิธีที่ปลอดภัยในการสร้างตัวเลขสุ่ม ดูคำตอบนี้สำหรับข้อมูลเพิ่มเติมและโซลูชันที่ใช้ประโยชน์จากตัวสร้างตัวเลขสุ่มที่ปลอดภัยในการเข้ารหัส

ดูเหมือนว่าคุณต้องการสิ่งอื่นกับ if ของคุณ

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

11
หมายเหตุ: ฉันไม่ไว้วางใจmd5(uniqid(rand(), TRUE));สำหรับบริบทด้านความปลอดภัย
Scott Arciszewski

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.