ฉันจะป้องกันการฉีด SQL ใน PHP ได้อย่างไร?


2773

หากการป้อนข้อมูลของผู้ใช้ถูกแทรกโดยไม่มีการดัดแปลงลงในแบบสอบถาม SQL แอปพลิเคชันจะเสี่ยงต่อการฉีด SQLเช่นในตัวอย่างต่อไปนี้:

$unsafe_variable = $_POST['user_input']; 

mysql_query("INSERT INTO `table` (`column`) VALUES ('$unsafe_variable')");

นั่นเป็นเพราะผู้ใช้สามารถป้อนสิ่งที่ชอบvalue'); DROP TABLE table;--และแบบสอบถามจะกลายเป็น:

INSERT INTO `table` (`column`) VALUES('value'); DROP TABLE table;--')

สิ่งที่สามารถทำได้เพื่อป้องกันไม่ให้สิ่งนี้เกิดขึ้น?

คำตอบ:


8958

ใช้คำสั่งที่เตรียมไว้และแบบสอบถามแบบมีพารามิเตอร์ เหล่านี้คือคำสั่ง SQL ที่ส่งและวิเคราะห์โดยเซิร์ฟเวอร์ฐานข้อมูลแยกจากพารามิเตอร์ใด ๆ วิธีนี้เป็นไปไม่ได้ที่ผู้โจมตีจะฉีด SQL ที่เป็นอันตราย

โดยทั่วไปคุณมีสองตัวเลือกเพื่อให้บรรลุสิ่งนี้:

  1. ใช้PDO (สำหรับไดรเวอร์ฐานข้อมูลที่รองรับ):

    $stmt = $pdo->prepare('SELECT * FROM employees WHERE name = :name');
    
    $stmt->execute([ 'name' => $name ]);
    
    foreach ($stmt as $row) {
        // Do something with $row
    }
    
  2. ใช้MySQLi (สำหรับ MySQL):

    $stmt = $dbConnection->prepare('SELECT * FROM employees WHERE name = ?');
    $stmt->bind_param('s', $name); // 's' specifies the variable type => 'string'
    
    $stmt->execute();
    
    $result = $stmt->get_result();
    while ($row = $result->fetch_assoc()) {
        // Do something with $row
    }
    

หากคุณกำลังเชื่อมต่อกับฐานข้อมูลอื่นนอกเหนือจาก MySQL มีตัวเลือกที่สองเฉพาะไดรเวอร์ที่คุณสามารถอ้างถึงได้ (ตัวอย่างเช่นpg_prepare()และpg_execute()สำหรับ PostgreSQL) PDO เป็นตัวเลือกสากล


ตั้งค่าการเชื่อมต่ออย่างถูกต้อง

หมายเหตุว่าเมื่อใช้PDOในการเข้าถึงฐานข้อมูล MySQL จริงงบเตรียมจะไม่ได้ใช้โดยค่าเริ่มต้น ในการแก้ไขปัญหานี้คุณต้องปิดใช้งานการจำลองคำสั่งที่เตรียมไว้ ตัวอย่างของการสร้างการเชื่อมต่อโดยใช้ PDO คือ:

$dbConnection = new PDO('mysql:dbname=dbtest;host=127.0.0.1;charset=utf8', 'user', 'password');

$dbConnection->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
$dbConnection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

ในตัวอย่างข้างต้นโหมดข้อผิดพลาดที่ไม่จำเป็นอย่างเคร่งครัดแต่ก็ควรที่จะเพิ่ม วิธีนี้สคริปต์จะไม่หยุดFatal Errorเมื่อมีสิ่งผิดปกติ และทำให้นักพัฒนามีโอกาสที่จะcatchเกิดข้อผิดพลาดใด ๆ ซึ่งเป็นthrown เป็นPDOExceptions

สิ่งที่จำเป็นคือsetAttribute()บรรทัดแรกซึ่งบอกให้ PDO ปิดการใช้งานข้อความที่เตรียมไว้แล้วและใช้คำสั่งที่เตรียมไว้จริง สิ่งนี้ทำให้มั่นใจได้ว่าคำสั่งและค่าจะไม่ถูกแยกวิเคราะห์โดย PHP ก่อนที่จะส่งไปยังเซิร์ฟเวอร์ MySQL (ทำให้ผู้โจมตีไม่สามารถฉีด SQL ที่เป็นอันตรายได้)

แม้ว่าคุณสามารถตั้งค่าcharsetในตัวเลือกของ Constructor ได้ แต่สิ่งสำคัญคือให้สังเกตว่า PHP 'รุ่นเก่า' (ก่อนหน้า 5.3.6) ละเว้นพารามิเตอร์ charsetใน DSN


คำอธิบาย

คำสั่ง SQL ที่คุณส่งไปจะprepareถูกวิเคราะห์และรวบรวมโดยเซิร์ฟเวอร์ฐานข้อมูล โดยการระบุพารามิเตอร์ ( ?หรือพารามิเตอร์ที่มีชื่อเช่น:nameในตัวอย่างด้านบน) คุณบอกกลไกฐานข้อมูลที่คุณต้องการกรอง จากนั้นเมื่อคุณโทรexecuteคำสั่งที่เตรียมไว้จะถูกรวมเข้ากับค่าพารามิเตอร์ที่คุณระบุ

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

พารามิเตอร์ใด ๆ ที่คุณส่งเมื่อใช้คำสั่งที่เตรียมไว้จะถือว่าเป็นสตริง (แม้ว่าเอ็นจิ้นฐานข้อมูลอาจทำการปรับให้เหมาะสมบางอย่างดังนั้นพารามิเตอร์อาจสิ้นสุดลงด้วยตัวเลขเช่นกัน) ในตัวอย่างข้างต้นถ้า$nameตัวแปรที่มี'Sarah'; DELETE FROM employeesผลก็จะค้นหาสตริง"'Sarah'; DELETE FROM employees"และคุณจะไม่จบลงด้วยตารางว่าง

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

โอ้และเมื่อคุณถามเกี่ยวกับวิธีการแทรกนี่คือตัวอย่าง (ใช้ PDO):

$preparedStatement = $db->prepare('INSERT INTO table (column) VALUES (:column)');

$preparedStatement->execute([ 'column' => $unsafeValue ]);

สามารถใช้งบที่เตรียมไว้สำหรับคิวรีแบบไดนามิกได้หรือไม่

ในขณะที่คุณยังสามารถใช้คำสั่งที่เตรียมไว้สำหรับพารามิเตอร์แบบสอบถามโครงสร้างของแบบสอบถามแบบไดนามิกไม่สามารถ parametrized และคุณลักษณะแบบสอบถามบางอย่างไม่สามารถ parametrized

สำหรับสถานการณ์เฉพาะเหล่านี้สิ่งที่ดีที่สุดที่ต้องทำคือใช้ตัวกรองรายการที่อนุญาตที่ จำกัด ค่าที่เป็นไปได้

// Value whitelist
// $dir can only be 'DESC', otherwise it will be 'ASC'
if (empty($dir) || $dir !== 'DESC') {
   $dir = 'ASC';
}

86
เพียงเพิ่มเพราะฉันไม่เห็นที่ใดที่นี่อีกแนวป้องกันคือไฟร์วอลล์โปรแกรมประยุกต์บนเว็บ (WAF) ที่สามารถตั้งกฎเพื่อค้นหาการโจมตีการฉีด sql:
jkerak

37
นอกจากนี้เอกสารอย่างเป็นทางการของ mysql_query อนุญาตให้เรียกใช้งานหนึ่งแบบสอบถามเท่านั้นดังนั้นแบบสอบถามอื่น ๆ นอกเหนือจาก; จะถูกละเว้น แม้ว่าสิ่งนี้จะถูกคัดค้านแล้วมีระบบจำนวนมากภายใต้ PHP 5.5.0 และอาจใช้ฟังก์ชันนี้ได้ php.net/manual/en/function.mysql-query.php
Randall Valenciano

12
นี่เป็นนิสัยที่ไม่ดี แต่เป็นวิธีการแก้ปัญหาหลังเลิก: ไม่เพียง แต่สำหรับการฉีด SQL แต่สำหรับการฉีดทุกประเภท (เช่นมีรูการฉีดเทมเพลตมุมมองใน F3 framework v2) หากคุณมีเว็บไซต์หรือแอปเก่า ๆ จากข้อบกพร่องในการฉีดหนึ่งในวิธีการแก้ปัญหาคือการกำหนดค่าของ vars supperglobal ที่กำหนดไว้ล่วงหน้าของคุณเช่น $ _POST ด้วยค่า Escape ที่ bootstrap โดย PDO ยังคงเป็นไปได้ที่จะหลบหนี (สำหรับเฟรมเวิร์กวันนี้): substr ($ pdo-> quote ($ str, \ PDO :: PARAM_STR), 1, -1)
Alix

15
คำตอบนี้ไม่มีคำอธิบายว่าอะไรคือคำสั่งที่เตรียมไว้ - สิ่งหนึ่ง - มันเป็นผลการปฏิบัติงานหากคุณใช้คำสั่งที่เตรียมไว้จำนวนมากในระหว่างการร้องขอของคุณ กรณีที่ดีกว่านี้คือใช้ PDO โดยปิดพารามิเตอร์การโยง แต่การเตรียมคำสั่งปิด
donis

6
การใช้ PDO ดีกว่าในกรณีที่คุณใช้การสืบค้นโดยตรงตรวจสอบให้แน่ใจว่าคุณใช้ mysqli :: escape_string
Kassem Itani

1652

คำเตือนเลิกใช้: รหัสตัวอย่างของคำตอบนี้ (เช่นรหัสตัวอย่างของคำถาม) ใช้MySQLส่วนขยายของ PHP ซึ่งเลิกใช้แล้วใน PHP 5.5.0 และลบออกทั้งหมดใน PHP 7.0.0

คำเตือนความปลอดภัย : คำตอบนี้ไม่สอดคล้องกับแนวทางปฏิบัติด้านความปลอดภัยที่ดีที่สุด การหลบหนีไม่เพียงพอเพื่อป้องกันการฉีด SQLให้ใช้คำสั่งที่เตรียมไว้แทน ใช้กลยุทธ์ที่ระบุไว้ด้านล่างด้วยความเสี่ยงของคุณเอง (นอกจากนี้mysql_real_escape_string()ถูกลบออกใน PHP 7)

หากคุณใช้ PHP เวอร์ชันล่าสุดmysql_real_escape_stringตัวเลือกที่ระบุไว้ด้านล่างจะไม่สามารถใช้งานได้อีกต่อไป (แม้ว่าmysqli::escape_stringจะเทียบเท่าสมัยใหม่) วันนี้mysql_real_escape_stringตัวเลือกจะเหมาะสมสำหรับรหัสดั้งเดิมใน PHP เวอร์ชันเก่าเท่านั้น


คุณมีสองตัวเลือก - หนีอักขระพิเศษในของคุณunsafe_variableหรือใช้การค้นหาแบบกำหนดพารามิเตอร์ ทั้งสองจะปกป้องคุณจากการฉีด SQL แบบสอบถามแบบใช้พารามิเตอร์ถือเป็นแนวปฏิบัติที่ดีกว่า แต่จะต้องเปลี่ยนเป็นส่วนขยาย MySQL รุ่นใหม่กว่าใน PHP ก่อนจึงจะสามารถใช้งานได้

เราจะกล่าวถึงสตริงผลกระทบที่ต่ำกว่าที่จะหลบหนีก่อน

//Connect

$unsafe_variable = $_POST["user-input"];
$safe_variable = mysql_real_escape_string($unsafe_variable);

mysql_query("INSERT INTO table (column) VALUES ('" . $safe_variable . "')");

//Disconnect

ดูเพิ่มเติมรายละเอียดของmysql_real_escape_stringฟังก์ชั่น

ในการใช้เคียวรีแบบกำหนดพารามิเตอร์คุณต้องใช้MySQLiแทนฟังก์ชันMySQL ในการเขียนตัวอย่างของคุณเราจำเป็นต้องมีสิ่งต่อไปนี้

<?php
    $mysqli = new mysqli("server", "username", "password", "database_name");

    // TODO - Check that connection was successful.

    $unsafe_variable = $_POST["user-input"];

    $stmt = $mysqli->prepare("INSERT INTO table (column) VALUES (?)");

    // TODO check that $stmt creation succeeded

    // "s" means the database expects a string
    $stmt->bind_param("s", $unsafe_variable);

    $stmt->execute();

    $stmt->close();

    $mysqli->close();
?>

ฟังก์ชั่นหลักที่คุณจะต้องอ่านต่อไปmysqli::prepareคือ

นอกจากนี้ยังเป็นคนอื่นได้ชี้ให้เห็นคุณอาจพบว่ามีประโยชน์ / ง่ายที่จะก้าวขึ้นชั้นของนามธรรมที่มีบางสิ่งบางอย่างเช่นPDO

โปรดทราบว่ากรณีที่คุณถามเป็นเรื่องที่ค่อนข้างง่ายและกรณีที่ซับซ้อนกว่านั้นอาจต้องใช้วิธีการที่ซับซ้อนกว่า โดยเฉพาะอย่างยิ่ง:

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

1
ใช้mysql_real_escape_stringเพียงพอหรือไม่ฉันต้องใช้พารามิเตอร์ด้วยหรือไม่
peiman F.

5
@peimanF รักษาแนวปฏิบัติที่ดีในการใช้ข้อความค้นหาแบบ Parametrized แม้แต่ในโครงการท้องถิ่น ด้วยการสืบค้นแบบ Parametrized คุณรับประกันได้ว่าจะไม่มีการฉีด SQL แต่โปรดจำไว้ว่าคุณควรฆ่าเชื้อข้อมูลเพื่อหลีกเลี่ยงการดึงข้อมูลปลอม (เช่นการฉีด XSS เช่นการใส่รหัส HTML ในข้อความ) htmlentitiesตัวอย่างเช่น
Goufalite

2
@peimanF แนวปฏิบัติที่ดีในการค้นหาแบบ Parametrized และค่าผูก แต่สตริงการหลบหนีที่แท้จริงเป็นสิ่งที่ดีสำหรับตอนนี้
Richard

ฉันเข้าใจการรวมmysql_real_escape_string()เพื่อความสมบูรณ์ แต่ไม่ใช่แฟนของการแสดงวิธีที่ผิดพลาดได้ง่ายที่สุดก่อน ผู้อ่านอาจคว้าตัวอย่างแรกได้อย่างรวดเร็ว สิ่งที่ดีเลิกใช้แล้วในตอนนี้ :)
Steen Schütt

1
@ SteenSchütt - mysql_*ฟังก์ชั่นทั้งหมดจะเลิกใช้ พวกเขาถูกแทนที่โดยที่คล้ายกัน ฟังก์ชั่นเช่นmysqli_* mysqli_real_escape_string
Rick James

1073

ทุกคำตอบที่นี่ครอบคลุมเพียงบางส่วนของปัญหา ในความเป็นจริงมีสี่ส่วนแบบสอบถามที่แตกต่างกันซึ่งเราสามารถเพิ่มลงใน SQL แบบไดนามิก: -

  • สตริง
  • หมายเลข
  • ตัวระบุ
  • คำหลักไวยากรณ์

และงบที่เตรียมไว้ครอบคลุมเพียงสองของพวกเขา

แต่บางครั้งเราต้องทำให้ข้อความค้นหาของเรามีชีวิตชีวายิ่งขึ้นเพิ่มตัวดำเนินการหรือตัวระบุเช่นกัน ดังนั้นเราจะต้องใช้เทคนิคการป้องกันที่แตกต่างกัน

โดยทั่วไปเช่นวิธีการป้องกันอยู่บนพื้นฐานยกเว้น

ในกรณีนี้ทุกพารามิเตอร์แบบไดนามิกควรจะฮาร์ดโค้ดในสคริปต์ของคุณและเลือกจากชุดนั้น ตัวอย่างเช่นในการสั่งซื้อแบบไดนามิก:

$orders  = array("name", "price", "qty"); // Field names
$key = array_search($_GET['sort'], $orders)); // if we have such a name
$orderby = $orders[$key]; // If not, first one will be set automatically. 
$query = "SELECT * FROM `table` ORDER BY $orderby"; // Value is safe

เพื่อให้กระบวนการง่ายขึ้นฉันได้เขียนฟังก์ชันตัวช่วยที่อนุญาตพิเศษที่ทำงานทั้งหมดในบรรทัดเดียว:

$orderby = white_list($_GET['orderby'], "name", ["name","price","qty"], "Invalid field name");
$query  = "SELECT * FROM `table` ORDER BY `$orderby`"; // sound and safe

มีวิธีอื่นในการรักษาความปลอดภัยตัวระบุ - การหลบหนี แต่ฉันค่อนข้างติดอยู่กับบัญชีขาวเป็นวิธีที่แข็งแกร่งและชัดเจน แต่ตราบใดที่คุณมีตัวระบุที่อ้างถึงคุณสามารถหลีกเลี่ยงอักขระอ้างเพื่อให้ปลอดภัย ตัวอย่างเช่นโดยการเริ่มต้นสำหรับ MySQL คุณต้องเป็นสองเท่าของตัวละครที่อ้างเพื่อหนีมัน สำหรับกฎการหลบหนีอื่น ๆ ของ DBMS จะแตกต่างกัน

ยังคงมีปัญหากับคำหลักไวยากรณ์ SQL (เช่นAND, DESCและเช่น) แต่สีขาวรายชื่อดูเหมือนว่าวิธีการเฉพาะในกรณีนี้

ดังนั้นคำแนะนำทั่วไปอาจใช้ถ้อยคำเป็น

  • ตัวแปรใด ๆ ที่แสดงถึงตัวอักษรข้อมูล SQL (หรือเพื่อใส่อย่างง่าย - สตริง SQL หรือตัวเลข) ต้องถูกเพิ่มผ่านคำสั่งที่เตรียมไว้ ไม่มีข้อยกเว้น.
  • ส่วนแบบสอบถามอื่น ๆ เช่นคำหลัก SQL ตารางหรือชื่อเขตข้อมูลหรือตัวดำเนินการ - ต้องถูกกรองผ่านรายการสีขาว

ปรับปรุง

แม้ว่าจะมีข้อตกลงทั่วไปเกี่ยวกับแนวปฏิบัติที่ดีที่สุดเกี่ยวกับการป้องกันการฉีด SQL แต่ก็ยังมีแนวปฏิบัติที่ไม่ดีมากมายเช่นกัน และบางคนก็หยั่งรากลึกในใจของผู้ใช้ PHP ตัวอย่างเช่นในหน้านี้มี (แม้ว่าผู้เข้าชมส่วนใหญ่จะมองไม่เห็น) มากกว่า 80 คำตอบที่ถูกลบ - ทั้งหมดถูกลบโดยชุมชนเนื่องจากคุณภาพไม่ดีหรือส่งเสริมการปฏิบัติที่ไม่ดีและล้าสมัย ยิ่งไปกว่านั้นคำตอบที่ไม่ดีบางข้อยังไม่ถูกลบ แต่กลับมีความเจริญรุ่งเรือง

ตัวอย่างเช่นมี (1) คือ (2) ยัง (3) คำตอบจำนวนมาก (4) (5)รวมถึงคำตอบที่ upvoted อันดับที่สองแนะนำให้คุณใช้สตริงหนีด้วยตนเอง - วิธีที่ล้าสมัยที่พิสูจน์แล้วว่าไม่ปลอดภัย

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

ฉันคิดว่าทั้งหมดนี้เป็นเพราะความเชื่อโชคลางที่เก่าแก่มากซึ่งได้รับการสนับสนุนจากหน่วยงานเช่นOWASPหรือคู่มือ PHPซึ่งประกาศความเท่าเทียมกันระหว่างสิ่งที่ "หลบหนี" และการป้องกันจากการฉีด SQL

ไม่ว่าคู่มือ PHP นั้นจะพูดถึงนานแค่ไหนก็*_escape_stringไม่ได้ทำให้ข้อมูลปลอดภัยและไม่เคยมีจุดประสงค์ นอกเหนือจากการไม่มีประโยชน์สำหรับส่วน SQL อื่นใดที่ไม่ใช่สตริงการยกเว้นด้วยตนเองนั้นไม่ถูกต้องเพราะเป็นคู่มือที่ตรงกันข้ามกับระบบอัตโนมัติ

และ OWASP ทำให้แย่ลงโดยเน้นไปที่การหลีกเลี่ยงการป้อนข้อมูลของผู้ใช้ซึ่งเป็นเรื่องไร้สาระที่สุด: ไม่ควรมีคำดังกล่าวในบริบทของการป้องกันการฉีด ตัวแปรทุกตัวอาจมีอันตราย - ไม่ว่าจะมาจากแหล่งใด! หรือกล่าวอีกนัยหนึ่งว่าตัวแปรทุกตัวจะต้องมีการจัดรูปแบบที่เหมาะสมเพื่อใส่ลงในคิวรีโดยไม่คำนึงถึงแหล่งที่มาอีกครั้ง มันเป็นจุดหมายปลายทางที่สำคัญ ขณะที่ผู้พัฒนาเริ่มแยกแกะออกจากแพะ (คิดว่าตัวแปรบางตัวมีความ "ปลอดภัย" หรือไม่) เขา / เธอจะก้าวเข้าสู่หายนะครั้งแรก ไม่ต้องพูดถึงว่าแม้ถ้อยคำจะบ่งบอกถึงการหลบหนีจำนวนมากที่จุดเริ่มต้นซึ่งคล้ายกับคุณลักษณะคำพูดที่วิเศษมาก - ดูแล้วคัดค้านเลิกใช้แล้วลบออก

ดังนั้นซึ่งแตกต่างจากสิ่งที่ "หนี" งบเตรียมคือมาตรการที่แน่นอนป้องกันจากการฉีด SQL (เมื่อมี)


848

ฉันขอแนะนำให้ใช้PDO (วัตถุข้อมูล PHP) เพื่อเรียกใช้แบบสอบถาม SQL ที่กำหนดพารามิเตอร์

สิ่งนี้ไม่เพียงป้องกันการฉีด SQL เท่านั้น แต่ยังช่วยเพิ่มความเร็วการสืบค้นอีกด้วย

และโดยใช้ PDO มากกว่าmysql_, mysqli_และpgsql_ฟังก์ชั่นที่คุณทำให้ใบสมัครของคุณน้อยใจลอยเพิ่มเติมจากฐานข้อมูลในเหตุการณ์ที่หายากที่คุณจะต้องให้บริการฐานข้อมูลสวิทช์


619

ใช้PDOและจัดทำแบบสอบถาม

( $connเป็นPDOวัตถุ)

$stmt = $conn->prepare("INSERT INTO tbl VALUES(:id, :name)");
$stmt->bindValue(':id', $id);
$stmt->bindValue(':name', $name);
$stmt->execute();

549

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

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

แนวทางของฉัน:

  • หากคุณคาดว่าอินพุตจะเป็นจำนวนเต็มตรวจสอบให้แน่ใจว่าเป็นจำนวนเต็มจริงๆ ในภาษาประเภทตัวแปรเช่น PHP มันเป็นสิ่งสำคัญมาก คุณสามารถใช้ตัวอย่างเช่นโซลูชันที่เรียบง่าย แต่ทรงพลังนี้:sprintf("SELECT 1,2,3 FROM table WHERE 4 = %u", $input);

  • ถ้าคุณคาดหวังอะไรอย่างอื่นจากจำนวนเต็มฐานสิบหกมัน หากคุณ hex คุณจะหลบหนีการป้อนข้อมูลทั้งหมด ใน C / C ++ มีฟังก์ชั่นที่เรียกว่าmysql_hex_string()ใน PHP bin2hex()คุณสามารถใช้

    ไม่ต้องกังวลว่าสตริงที่ Escape จะมีขนาดความยาวเท่าเดิมเพราะถึงแม้ว่าคุณจะใช้mysql_real_escape_stringPHP ก็ต้องจัดสรรความจุเดียวกัน((2*input_length)+1)ซึ่งก็เหมือนกัน

  • วิธีการเลขฐานสิบหกนี้มักจะใช้เมื่อคุณถ่ายโอนข้อมูลไบนารี แต่ฉันเห็นเหตุผลที่ไม่ใช้ในข้อมูลทั้งหมดเพื่อป้องกันการโจมตี SQL injection โปรดทราบว่าคุณต้องเสริมข้อมูลด้วย0xหรือใช้ฟังก์ชัน MySQL UNHEXแทน

ตัวอย่างเช่นเคียวรี:

SELECT password FROM users WHERE name = 'root'

จะกลายเป็น:

SELECT password FROM users WHERE name = 0x726f6f74

หรือ

SELECT password FROM users WHERE name = UNHEX('726f6f74')

Hex คือการหลบหนีที่สมบูรณ์แบบ ไม่มีวิธีการฉีด

ความแตกต่างระหว่างฟังก์ชั่น UNHEX และคำนำหน้า 0x

มีการอภิปรายในความคิดเห็นดังนั้นในที่สุดฉันก็ต้องการทำให้ชัดเจน วิธีการทั้งสองนี้คล้ายกันมาก แต่มีความแตกต่างกันเล็กน้อยในบางวิธี:

** ที่ 0x ** คำนำหน้าเท่านั้นที่สามารถใช้สำหรับคอลัมน์ข้อมูลเช่นถ่าน, varchar, ข้อความ, บล็อกไบนารี ฯลฯ
นอกจากนี้การใช้งานมีความซับซ้อนเล็กน้อยหากคุณกำลังจะแทรกสตริงว่าง คุณจะต้องแทนที่ด้วย''ทั้งหมดหรือคุณจะได้รับข้อผิดพลาด

UNHEX ()ทำงานในคอลัมน์ใดก็ได้ คุณไม่ต้องกังวลกับสตริงว่าง


วิธี Hex มักใช้เป็นการโจมตี

โปรดทราบว่าวิธีการฐานสิบหกนี้มักจะใช้การโจมตีแบบ SQL injection mysql_real_escape_stringที่จำนวนเต็มเป็นเหมือนสตริงและหนีออกมาเพียงกับ จากนั้นคุณสามารถหลีกเลี่ยงการใช้คำพูด

ตัวอย่างเช่นถ้าคุณทำอะไรแบบนี้:

"SELECT title FROM article WHERE id = " . mysql_real_escape_string($_GET["id"])

การโจมตีสามารถฉีดมากได้อย่างง่ายดาย พิจารณาโค้ดที่ฉีดคืนซึ่งส่งคืนจากสคริปต์ของคุณ:

SELECT ... WHERE id = -1 union เลือก table_name ทั้งหมดจาก information_schema.tables

และตอนนี้ก็แยกโครงสร้างตาราง:

SELECT ... WHERE id = -1 union เลือกทั้งหมด column_name จาก information_schema.column โดยที่ table_name = 0x61727469636c65

จากนั้นเพียงเลือกข้อมูลที่ต้องการ มันเจ๋งใช่มั้ย

แต่ถ้า coder ของไซต์ที่สามารถทำการฉีดได้จะทำให้เป็น hex ไม่สามารถทำการฉีดได้เนื่องจากแบบสอบถามจะมีลักษณะดังนี้: SELECT ... WHERE id = UNHEX('2d312075...3635')


6
@SumitGupta ใช่คุณทำ MySQL ไม่ concatenate ด้วยแต่มี+ CONCATและต่อประสิทธิภาพการทำงาน: ฉันไม่คิดว่ามันจะส่งผลกระทบต่อประสิทธิภาพการทำงานเพราะ mysql ต้องแยกวิเคราะห์ข้อมูลและไม่สำคัญว่าต้นกำเนิดเป็นสตริงหรือฐานสิบหก
Zaffy

11
@ YourCommonSense คุณไม่เข้าใจแนวคิด ... ถ้าคุณต้องการให้มีสตริงใน mysql คุณพูดแบบนี้'root'หรือคุณสามารถ hex มัน0x726f6f74แต่ถ้าคุณต้องการตัวเลขและส่งเป็นสตริงคุณอาจจะเขียน '42' ไม่ใช่ CHAR (42 ) ... '42' ใน hex จะ0x3432ไม่0x42
Zaffy

7
@ YourCommonSense ฉันไม่มีอะไรจะพูด ... เพียงแค่ lol ... หากคุณยังต้องการลอง hex บนฟิลด์ตัวเลขดูความคิดเห็นที่สอง ฉันพนันกับคุณว่ามันจะได้ผล
Zaffy

2
คำตอบระบุไว้อย่างชัดเจนว่ามันจะไม่ทำงานอย่างนั้นกับค่าจำนวนเต็มเหตุผลที่ bin2hex แปลงค่าที่ส่งผ่านไปยังสตริง (และ bin2hex (0) คือ 0x30 และไม่ใช่ 0x03) - นั่นอาจเป็นส่วนที่ทำให้คุณสับสน . ถ้าคุณทำตามนั้นมันทำงานได้อย่างสมบูรณ์ (อย่างน้อยในเว็บไซต์ของฉันทดสอบกับรุ่น mysql ที่แตกต่างกัน 4 รุ่นบนเครื่องเดเบียน 5.1.x ถึง 5.6.x) ท้ายที่สุดแล้วเลขฐานสิบหกเป็นเพียงวิธีการแทนค่าไม่ใช่)
กริฟฟิน

5
@ YourCommonSense คุณยังไม่เข้าใจใช่ไหม คุณไม่สามารถใช้ 0x และ concat ได้เพราะหากสตริงว่างคุณจะจบด้วยข้อผิดพลาด หากคุณต้องการทางเลือกที่ง่ายสำหรับการสืบค้นลองใช้ตัวเลือกนี้SELECT title FROM article WHERE id = UNHEX(' . bin2hex($_GET["id"]) . ')
Zaffy

499

คำเตือนเลิกใช้: รหัสตัวอย่างของคำตอบนี้ (เช่นรหัสตัวอย่างของคำถาม) ใช้MySQLส่วนขยายของ PHP ซึ่งเลิกใช้แล้วใน PHP 5.5.0 และลบออกทั้งหมดใน PHP 7.0.0

คำเตือนความปลอดภัย : คำตอบนี้ไม่สอดคล้องกับแนวทางปฏิบัติด้านความปลอดภัยที่ดีที่สุด การหลบหนีไม่เพียงพอเพื่อป้องกันการฉีด SQLให้ใช้คำสั่งที่เตรียมไว้แทน ใช้กลยุทธ์ที่ระบุไว้ด้านล่างด้วยความเสี่ยงของคุณเอง (นอกจากนี้mysql_real_escape_string()ถูกลบออกใน PHP 7)

สิ่งสำคัญ

วิธีที่ดีที่สุดเพื่อป้องกัน SQL Injection คือการใช้งบเตรียม แทนการหลบหนีเป็นคำตอบที่ได้รับการยอมรับแสดงให้เห็นถึง

มีห้องสมุดเช่นAura.SqlและEasyDBที่ช่วยให้นักพัฒนาสามารถใช้งบที่เตรียมไว้ได้ง่ายขึ้น ต้องการเรียนรู้เพิ่มเติมเกี่ยวกับสาเหตุที่งบเตรียมจะดีกว่าที่หยุดฉีด SQLอ้างถึงนี้mysql_real_escape_string()บายพาสและเมื่อเร็ว ๆ นี้ได้รับการแก้ไขช่องโหว่ Unicode SQL Injection ใน WordPress

การป้องกันการฉีด - mysql_real_escape_string ()

PHP มีฟังก์ชั่นพิเศษเพื่อป้องกันการโจมตีเหล่านี้ สิ่งที่คุณต้องทำคือใช้ฟังก์ชั่นหนึ่งคำ, mysql_real_escape_string.

mysql_real_escape_stringใช้สตริงที่จะใช้ในแบบสอบถาม MySQL และส่งคืนสตริงเดียวกันกับการพยายามฉีด SQL ทั้งหมดอย่างปลอดภัย โดยพื้นฐานแล้วมันจะแทนที่คำพูดที่ยุ่งยาก (') ที่ผู้ใช้อาจป้อนด้วยตัวสำรองที่ปลอดภัยสำหรับ MySQL

หมายเหตุ:คุณจะต้องเชื่อมต่อกับฐานข้อมูลเพื่อใช้ฟังก์ชั่นนี้!

// เชื่อมต่อกับ MySQL

$name_bad = "' OR 1'"; 

$name_bad = mysql_real_escape_string($name_bad);

$query_bad = "SELECT * FROM customers WHERE username = '$name_bad'";
echo "Escaped Bad Injection: <br />" . $query_bad . "<br />";


$name_evil = "'; DELETE FROM customers WHERE 1 or username = '"; 

$name_evil = mysql_real_escape_string($name_evil);

$query_evil = "SELECT * FROM customers WHERE username = '$name_evil'";
echo "Escaped Evil Injection: <br />" . $query_evil;

คุณสามารถค้นหารายละเอียดเพิ่มเติมในMySQL - ป้องกัน


30
นี่คือสิ่งที่ดีที่สุดที่คุณสามารถทำได้ด้วยนามสกุล mysql แบบเดิม สำหรับรหัสใหม่คุณควรเปลี่ยนมาใช้ mysqli หรือ PDO
ÁlvaroGonzález

7
ฉันไม่เห็นด้วยกับ 'ฟังก์ชั่นพิเศษที่ทำขึ้นเพื่อป้องกันการโจมตีเหล่านี้' ฉันคิดว่าmysql_real_escape_stringวัตถุประสงค์นั้นอนุญาตให้สร้างแบบสอบถาม SQL ที่ถูกต้องสำหรับทุกสายข้อมูล การป้องกัน sql-injection เป็นผลข้างเคียงของฟังก์ชั่นนี้
sectus

4
คุณไม่ต้องใช้ฟังก์ชั่นในการเขียนสตริงข้อมูลอินพุตที่ถูกต้อง คุณเพียงแค่เขียนสิ่งที่ถูกต้องที่ไม่ต้องการหลบหนีหรือหนีไปแล้ว mysql_real_escape_string () อาจได้รับการออกแบบโดยมีวัตถุประสงค์ที่คุณกล่าวถึงในใจ แต่ค่าเดียวของมันคือการป้องกันการฉีด
Nazca

17
คำเตือน! ไม่ได้เป็นความผิดmysql_real_escape_string()
eggyal

9
mysql_real_escape_stringเลิกใช้แล้วดังนั้นจึงไม่มีตัวเลือกที่ใช้การได้อีกต่อไป มันจะถูกลบออกในอนาคตจาก PHP เป็นการดีที่สุดที่จะไปยังสิ่งที่ PHP หรือ MySQL แนะนำ
jww

462

คุณสามารถทำสิ่งพื้นฐานเช่นนี้

$safe_variable = mysqli_real_escape_string($_POST["user-input"], $dbConnection);
mysqli_query($dbConnection, "INSERT INTO table (column) VALUES ('" . $safe_variable . "')");

นี่จะไม่แก้ปัญหาทุกปัญหา แต่มันเป็นก้าวที่ดีมาก ฉันออกจากรายการที่ชัดเจนเช่นการตรวจสอบการมีอยู่ของตัวแปรรูปแบบ (ตัวเลขตัวอักษร ฯลฯ )


28
ฉันลองตัวอย่างของคุณแล้วและมันก็ใช้ได้ดีสำหรับฉันคุณชัดเจนว่า "นี่จะไม่แก้ปัญหาทุกปัญหา"
Chinook

14
หากคุณไม่ได้อ้างสตริงมันยังคงสามารถฉีดได้ ใช้$q = "SELECT col FROM tbl WHERE x = $safe_var";ตัวอย่างเช่น การตั้งค่า$safe_varการ1 UNION SELECT password FROM usersทำงานในกรณีนี้เนื่องจากการขาดคำพูด นอกจากนี้ยังเป็นไปได้ที่จะฉีดเข้าไปในสตริงแบบสอบถามใช้และCONCAT CHR
พหุนาม

4
@ Polynomial ถูกต้อง แต่ฉันเห็นว่านี่เป็นการใช้ที่ผิด ตราบใดที่คุณใช้อย่างถูกต้องมันจะทำงานได้อย่างแน่นอน
glglgl

22
คำเตือน! ไม่ได้เป็นความผิดmysql_real_escape_string()
eggyal

8
mysql_real_escape_stringเลิกใช้แล้วดังนั้นจึงไม่มีตัวเลือกที่ใช้การได้อีกต่อไป มันจะถูกลบออกในอนาคตจาก PHP เป็นการดีที่สุดที่จะไปยังสิ่งที่ PHP หรือ MySQL แนะนำ
jww

380

ไม่ว่าคุณจะใช้งานแบบใดก็ตามตรวจสอบให้แน่ใจว่าคุณได้ตรวจสอบข้อมูลที่คุณยังไม่ได้ถูก mangled โดยmagic_quotesหรือขยะที่มีความหมายดีอื่น ๆ และหากจำเป็นให้เรียกใช้ผ่านstripslashesหรืออะไรก็ตามที่จะทำให้บริสุทธิ์


11
แท้จริง; การเปิดใช้งานด้วย magic_quotes เปิดอยู่เพียงแค่สนับสนุนการปฏิบัติที่ไม่ดี อย่างไรก็ตามบางครั้งคุณไม่สามารถควบคุมสภาพแวดล้อมในระดับนั้นได้เสมอไม่ว่าคุณจะไม่มีสิทธิ์เข้าถึงเพื่อจัดการเซิร์ฟเวอร์หรือแอปพลิเคชันของคุณต้องอยู่ร่วมกับแอปพลิเคชันที่ (ตัวสั่น) ขึ้นอยู่กับการกำหนดค่าดังกล่าว ด้วยเหตุผลเหล่านี้จึงเป็นการดีที่จะเขียนแอปพลิเคชันแบบพกพา - แม้ว่าความพยายามจะสูญเปล่าหากคุณควบคุมสภาพแวดล้อมในการปรับใช้เช่นเนื่องจากเป็นแอปพลิเคชันในบ้านหรือจะใช้ในสภาพแวดล้อมเฉพาะของคุณเท่านั้น
Rob

24
ในฐานะของ PHP 5.4 สิ่งที่น่าสะอิดสะเอียนที่เรียกว่า 'คำพูดมายากล' ได้รับการฆ่าตาย และการกำจัดขยะที่ดี
BryanH

363

คำเตือนเลิกใช้: รหัสตัวอย่างของคำตอบนี้ (เช่นรหัสตัวอย่างของคำถาม) ใช้MySQLส่วนขยายของ PHP ซึ่งเลิกใช้แล้วใน PHP 5.5.0 และลบออกทั้งหมดใน PHP 7.0.0

คำเตือนความปลอดภัย : คำตอบนี้ไม่สอดคล้องกับแนวทางปฏิบัติด้านความปลอดภัยที่ดีที่สุด การหลบหนีไม่เพียงพอเพื่อป้องกันการฉีด SQLให้ใช้คำสั่งที่เตรียมไว้แทน ใช้กลยุทธ์ที่ระบุไว้ด้านล่างด้วยความเสี่ยงของคุณเอง (นอกจากนี้mysql_real_escape_string()ถูกลบออกใน PHP 7)

Parameterized query และการตรวจสอบอินพุตเป็นวิธีที่จะไป มีหลายสถานการณ์ภายใต้การฉีด SQL ที่อาจเกิดขึ้นแม้ว่าจะmysql_real_escape_string()ถูกนำมาใช้

ตัวอย่างเหล่านี้มีความเสี่ยงต่อการฉีด SQL:

$offset = isset($_GET['o']) ? $_GET['o'] : 0;
$offset = mysql_real_escape_string($offset);
RunQuery("SELECT userid, username FROM sql_injection_test LIMIT $offset, 10");

หรือ

$order = isset($_GET['o']) ? $_GET['o'] : 'userid';
$order = mysql_real_escape_string($order);
RunQuery("SELECT userid, username FROM sql_injection_test ORDER BY `$order`");

ในทั้งสองกรณีคุณไม่สามารถใช้'เพื่อป้องกันการห่อหุ้ม

แหล่งที่มา : การฉีด SQL ที่ไม่คาดคิด (เมื่อหนีไม่เพียงพอ)


2
คุณสามารถป้องกันการฉีด SQL ได้ถ้าคุณใช้เทคนิคการตรวจสอบความถูกต้องของข้อมูลที่ผู้ใช้ป้อนเข้าจะได้รับการตรวจสอบกับชุดของกฎที่กำหนดไว้สำหรับความยาวประเภทและไวยากรณ์และกฎทางธุรกิจ
Josip Ivic

312

ในความคิดของฉันวิธีที่ดีที่สุดในการป้องกันการฉีด SQL ในแอปพลิเคชั่น PHP ของคุณ (หรือแอปพลิเคชันเว็บใด ๆ ก็ตาม) คือการคิดถึงสถาปัตยกรรมแอปพลิเคชันของคุณ หากวิธีเดียวที่จะป้องกันการฉีด SQL คือต้องจำไว้ว่าให้ใช้วิธีการหรือฟังก์ชั่นพิเศษที่ทำสิ่งที่ถูกต้องทุกครั้งที่คุณพูดคุยกับฐานข้อมูลคุณกำลังทำผิด ด้วยวิธีนี้มันเป็นเรื่องของเวลาจนกว่าคุณจะลืมจัดรูปแบบการสืบค้นของคุณอย่างถูกต้อง ณ จุดหนึ่งในรหัสของคุณ

การใช้รูปแบบ MVC และกรอบงานเช่นCakePHPหรือCodeIgniterน่าจะเป็นวิธีที่ถูกต้อง: งานทั่วไปเช่นการสร้างการสืบค้นฐานข้อมูลที่ปลอดภัยได้รับการแก้ไขและนำไปใช้งานจากส่วนกลางในกรอบดังกล่าว พวกเขาช่วยให้คุณจัดระเบียบเว็บแอปพลิเคชันของคุณอย่างเหมาะสมและทำให้คุณคิดเกี่ยวกับการโหลดและบันทึกวัตถุมากกว่าการสร้างแบบสอบถาม SQL เดียวอย่างปลอดภัย


5
ฉันคิดว่าย่อหน้าแรกของคุณมีความสำคัญ ความเข้าใจเป็นกุญแจสำคัญ นอกจากนี้ทุกคนไม่ได้ทำงานให้กับ บริษัท สำหรับแนวใหญ่ของผู้คน, กรอบจริงไปกับความคิดของความเข้าใจ การสนิทสนมกับปัจจัยพื้นฐานอาจไม่คุ้มค่าในขณะที่ทำงานภายใต้กำหนดเวลา แต่ผู้ที่ทำมันเองจะสนุกกับการทำให้มือสกปรก นักพัฒนากรอบการทำงานไม่ได้รับสิทธิพิเศษอย่างที่ทุกคนต้องคำนับและคิดว่าพวกเขาไม่เคยทำผิดพลาด พลังในการตัดสินใจยังคงสำคัญ ใครจะพูดว่ากรอบการทำงานของฉันจะไม่แทนที่โครงการอื่น ๆ ในอนาคต?
Anthony Rutledge

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

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

3
@AththonyRutledge ฉันเห็นด้วย! ฉันคิดว่ากรณีการใช้งานสร้างความแตกต่างด้วย: ฉันกำลังสร้างแกลเลอรี่รูปภาพสำหรับโฮมเพจส่วนตัวของฉันหรือฉันกำลังสร้างเว็บแอปพลิเคชันธนาคารออนไลน์ ในกรณีหลังมันสำคัญมากที่จะต้องเข้าใจรายละเอียดของความปลอดภัยและวิธีการที่ฉันใช้กรอบการจัดการที่อยู่เหล่านั้น
Johannes Fahrenkrug

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

298

ฉันชอบวิธีการที่เก็บไว้ ( MySQL ได้สนับสนุนขั้นตอนการจัดเก็บตั้งแต่ 5.0 ) จากมุมมองความปลอดภัย - ข้อดีคือ -

  1. ฐานข้อมูลส่วนใหญ่ (รวมถึงMySQL ) เปิดใช้งานการ จำกัด การเข้าถึงของผู้ใช้ การควบคุมการเข้าถึงการรักษาความปลอดภัยที่ละเอียดจะมีประโยชน์ในการป้องกันการเพิ่มของการโจมตีสิทธิพิเศษ สิ่งนี้จะช่วยป้องกันไม่ให้แอปพลิเคชันที่ถูกบุกรุกสามารถเรียกใช้ SQL กับฐานข้อมูลได้โดยตรง
  2. พวกเขาสรุปแบบสอบถาม SQL ดิบจากแอปพลิเคชันเพื่อให้ข้อมูลน้อยลงของโครงสร้างฐานข้อมูลพร้อมใช้งานกับแอปพลิเคชัน สิ่งนี้ทำให้ผู้คนเข้าใจโครงสร้างพื้นฐานของฐานข้อมูลและการออกแบบการโจมตีที่เหมาะสมได้ยากขึ้น
  3. พวกเขายอมรับพารามิเตอร์เท่านั้นดังนั้นข้อดีของแบบสอบถามแบบมีพารามิเตอร์จึงมี แน่นอน - IMO คุณยังต้องฆ่าเชื้ออินพุตของคุณโดยเฉพาะถ้าคุณใช้ SQL แบบไดนามิกภายในโพรซีเดอร์ที่เก็บไว้

ข้อเสียคือ -

  1. พวกเขา (ขั้นตอนการเก็บ) ยากที่จะรักษาและมีแนวโน้มที่จะทวีคูณอย่างรวดเร็ว ทำให้การจัดการปัญหาพวกเขา
  2. พวกเขาไม่เหมาะสำหรับการค้นหาแบบไดนามิก - หากพวกเขาถูกสร้างขึ้นเพื่อรับรหัสแบบไดนามิกเป็นพารามิเตอร์แล้วข้อดีจำนวนมากจะถูกปฏิเสธ

297

มีหลายวิธีในการป้องกัน SQL injections และแฮ็ก SQL อื่น ๆ คุณสามารถค้นหาได้จากอินเทอร์เน็ต (การค้นหาโดย Google) แน่นอนPDO เป็นหนึ่งในโซลูชั่นที่ดี แต่ฉันอยากจะแนะนำให้คุณป้องกันการเชื่อมโยงที่ดีจากการฉีด SQL

SQL injection คืออะไรและจะป้องกันได้อย่างไร

คู่มือ PHP สำหรับการฉีด SQL

คำอธิบายของ Microsoft เกี่ยวกับการฉีด SQL และการป้องกันใน PHP

และบางส่วนอื่น ๆ เช่นการป้องกันการฉีด SQL กับ MySQL และ PHP

ทีนี้ทำไมคุณต้องป้องกันคิวรี่จากการฉีด SQL?

ฉันต้องการแจ้งให้คุณทราบ: เหตุใดเราจึงพยายามป้องกันการฉีด SQL ด้วยตัวอย่างสั้น ๆ ด้านล่าง:

ข้อความค้นหาการจับคู่การรับรองความถูกต้องเข้าสู่ระบบ:

$query="select * from users where email='".$_POST['email']."' and password='".$_POST['password']."' ";

ตอนนี้ถ้าใครบางคน (แฮ็กเกอร์) ทำให้

$_POST['email']= admin@emali.com' OR '1=1

และรหัสผ่านอะไร ....

แบบสอบถามจะถูกแยกวิเคราะห์ลงในระบบได้มากถึง:

$query="select * from users where email='admin@emali.com' OR '1=1';

ส่วนอื่น ๆ จะถูกยกเลิก แล้วจะเกิดอะไรขึ้น? ผู้ใช้ที่ไม่ได้รับอนุญาต (แฮ็กเกอร์) จะสามารถเข้าสู่ระบบในฐานะผู้ดูแลระบบโดยไม่ต้องมีรหัสผ่านของเขา / เธอ ตอนนี้เขา / เธอสามารถทำอะไรก็ได้ที่ผู้ดูแลระบบ / บุคคลอีเมลสามารถทำได้ ดูสิมันอันตรายมากถ้าไม่ป้องกันการฉีด SQL


267

ฉันคิดว่าถ้ามีคนต้องการใช้ PHP และ MySQL หรือเซิร์ฟเวอร์ dataBase อื่น ๆ :

  1. คิดเกี่ยวกับการเรียนรู้PDO (PHP Data Objects) - เป็นเลเยอร์การเข้าถึงฐานข้อมูลที่ให้วิธีการเข้าถึงฐานข้อมูลที่หลากหลายอย่างสม่ำเสมอ
  2. คิดเกี่ยวกับการเรียนรู้MySQLi
  3. ใช้ฟังก์ชั่น PHP พื้นเมืองที่ชอบ: strip_tags , mysql_real_escape_string(int)$fooหรือถ้าเป็นตัวเลขตัวแปรเพียง อ่านข้อมูลเพิ่มเติมเกี่ยวกับประเภทของตัวแปรใน PHP ที่นี่ หากคุณใช้ไลบรารีเช่น PDO หรือ MySQLi ให้ใช้PDO :: quote ()และmysqli_real_escape_string ()เสมอ

ตัวอย่างห้องสมุด:

---- PDO

----- ไม่มีตัวยึด - สุกสำหรับการฉีด SQL! มันไม่ดี

$request = $pdoConnection->("INSERT INTO parents (name, addr, city) values ($name, $addr, $city)");

----- ตัวยึดที่ไม่มีชื่อ

$request = $pdoConnection->("INSERT INTO parents (name, addr, city) values (?, ?, ?);

----- ตัวยึดที่ระบุชื่อ

$request = $pdoConnection->("INSERT INTO parents (name, addr, city) value (:name, :addr, :city)");

--- MySQLi

$request = $mysqliConnection->prepare('
       SELECT * FROM trainers
       WHERE name = ?
       AND email = ?
       AND last_login > ?');

    $query->bind_param('first_param', 'second_param', $mail, time() - 3600);
    $query->execute();

ป.ล. :

PDO ชนะการต่อสู้นี้ได้อย่างง่ายดาย ด้วยการสนับสนุนไดรเวอร์ฐานข้อมูลที่แตกต่างกันสิบสองตัวและพารามิเตอร์ที่มีชื่อเราสามารถเพิกเฉยต่อประสิทธิภาพที่สูญเสียไปเล็กน้อยและคุ้นเคยกับ API ของมัน จากมุมมองด้านความปลอดภัยทั้งคู่มีความปลอดภัยตราบเท่าที่ผู้พัฒนาใช้วิธีที่ควรจะใช้

แต่ในขณะที่ทั้ง PDO และ MySQLi นั้นค่อนข้างรวดเร็ว แต่ MySQLi นั้นทำงานได้เร็วกว่ามาตรฐานอย่างไม่มีนัยสำคัญ - ~ 2.5% สำหรับข้อความที่ไม่ได้จัดทำและ ~ 6.5% สำหรับข้อความที่เตรียมไว้

และโปรดทดสอบทุกคำถามในฐานข้อมูลของคุณ - เป็นวิธีที่ดีกว่าในการป้องกันการฉีด


mysqli นั้นไม่ถูกต้อง พารามิเตอร์แรกแสดงประเภทข้อมูล
mickmackusa

257

ถ้าเป็นไปได้ให้โยนประเภทของพารามิเตอร์ของคุณ แต่มันจะทำงานกับประเภทง่าย ๆ เช่น int, bool และ float

$unsafe_variable = $_POST['user_id'];

$safe_variable = (int)$unsafe_variable ;

mysqli_query($conn, "INSERT INTO table (column) VALUES ('" . $safe_variable . "')");

3
นี่เป็นหนึ่งในไม่กี่กรณีที่ฉันจะใช้ "ค่าที่ใช้ Escape" แทนคำสั่งที่เตรียมไว้ และการแปลงชนิดจำนวนเต็มนั้นมีประสิทธิภาพอย่างมาก
HoldOffHunger

233

หากคุณต้องการใช้ประโยชน์จากเอ็นจิ้นแคชเช่นRedisหรือMemcached DALMP อาจเป็นทางเลือกก็ได้ มันใช้บริสุทธิ์MySQLi ตรวจสอบสิ่งนี้: DALMP Database Abstraction Layer สำหรับ MySQL โดยใช้ PHP

นอกจากนี้คุณสามารถ 'เตรียม' ข้อโต้แย้งของคุณก่อนที่จะจัดทำแบบสอบถามของคุณเพื่อให้คุณสามารถสร้างแบบสอบถามแบบไดนามิกและในตอนท้ายมีแบบสอบถามคำสั่งที่เตรียมไว้อย่างสมบูรณ์ DALMP ฐานข้อมูล Abstraction Layer สำหรับ MySQL โดยใช้ PHP


224

สำหรับผู้ที่ไม่แน่ใจเกี่ยวกับวิธีการใช้ PDO (มาจากmysql_ฟังก์ชั่น) ฉันได้สร้างแผ่นคลุม PDO ที่ง่ายมากซึ่งเป็นไฟล์เดียว มันแสดงให้เห็นว่ามันง่ายที่จะทำสิ่งต่าง ๆ ที่แอพพลิเคชั่นทั่วไปต้องทำ ทำงานร่วมกับ PostgreSQL, MySQL และ SQLite

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

ฉันต้องการคอลัมน์เดียว

$count = DB::column('SELECT COUNT(*) FROM `user`);

ฉันต้องการผลอาร์เรย์ (key => value) (เช่นสำหรับการเลือก box)

$pairs = DB::pairs('SELECT `id`, `username` FROM `user`);

ฉันต้องการผลลัพธ์แถวเดี่ยว

$user = DB::row('SELECT * FROM `user` WHERE `id` = ?', array($user_id));

ฉันต้องการผลลัพธ์มากมาย

$banned_users = DB::fetch('SELECT * FROM `user` WHERE `banned` = ?', array(TRUE));

222

การใช้ฟังก์ชั่น PHP นี้mysql_escape_string()คุณจะได้รับการป้องกันที่ดีอย่างรวดเร็ว

ตัวอย่างเช่น:

SELECT * FROM users WHERE name = '".mysql_escape_string($name_from_html_form)."'

mysql_escape_string - หนีสตริงสำหรับใช้ใน mysql_query

สำหรับการป้องกันเพิ่มเติมคุณสามารถเพิ่มในตอนท้าย ...

wHERE 1=1   or  LIMIT 1

ในที่สุดคุณจะได้รับ:

SELECT * FROM users WHERE name = '".mysql_escape_string($name_from_html_form)."' LIMIT 1

220

แนวทางบางประการสำหรับการหลีกเลี่ยงอักขระพิเศษในคำสั่ง SQL

อย่าใช้MySQL ส่วนขยายนี้เลิกใช้แล้ว ใช้MySQLiหรือPDOแทน

MySQLi

สำหรับการหลีกเลี่ยงอักขระพิเศษในสตริงด้วยตนเองคุณสามารถใช้ฟังก์ชันmysqli_real_escape_string ฟังก์ชั่นจะไม่ทำงานอย่างถูกต้องเว้นแต่ชุดตัวอักษรที่ถูกต้องมีการตั้งค่ากับmysqli_set_charset

ตัวอย่าง:

$mysqli = new mysqli('host', 'user', 'password', 'database');
$mysqli->set_charset('charset');

$string = $mysqli->real_escape_string($string);
$mysqli->query("INSERT INTO table (column) VALUES ('$string')");

สำหรับการหลีกเลี่ยงค่าโดยอัตโนมัติด้วยข้อความสั่งที่เตรียมไว้ให้ใช้mysqli_prepareและmysqli_stmt_bind_paramโดยที่ประเภทของตัวแปรผูกที่เกี่ยวข้องจะต้องถูกจัดเตรียมไว้สำหรับการแปลงที่เหมาะสม:

ตัวอย่าง:

$stmt = $mysqli->prepare("INSERT INTO table (column1, column2) VALUES (?,?)");

$stmt->bind_param("is", $integer, $string);

$stmt->execute();

ไม่ว่าคุณจะใช้คำสั่งที่เตรียมไว้หรือmysqli_real_escape_stringคุณต้องรู้ประเภทของข้อมูลที่คุณกำลังใช้อยู่เสมอ

ดังนั้นหากคุณใช้คำสั่งที่เตรียมไว้คุณจะต้องระบุประเภทของตัวแปรสำหรับmysqli_stmt_bind_paramฟังก์ชั่น

และการใช้งานmysqli_real_escape_stringนั้นมีไว้สำหรับตามที่ชื่อกล่าวว่าการหลีกเลี่ยงอักขระพิเศษในสตริงดังนั้นมันจะไม่ทำให้จำนวนเต็มปลอดภัย วัตถุประสงค์ของฟังก์ชั่นนี้คือการป้องกันไม่ให้แบ่งสตริงในคำสั่ง SQL และความเสียหายต่อฐานข้อมูลที่อาจทำให้เกิด เป็นฟังก์ชันที่มีประโยชน์เมื่อใช้อย่างถูกต้องโดยเฉพาะอย่างยิ่งเมื่อรวมกับmysqli_real_escape_stringsprintf

ตัวอย่าง:

$string = "x' OR name LIKE '%John%";
$integer = '5 OR id != 0';

$query = sprintf( "SELECT id, email, pass, name FROM members WHERE email ='%s' AND id = %d", $mysqli->real_escape_string($string), $integer);

echo $query;
// SELECT id, email, pass, name FROM members WHERE email ='x\' OR name LIKE \'%John%' AND id = 5

$integer = '99999999999999999999';
$query = sprintf("SELECT id, email, pass, name FROM members WHERE email ='%s' AND id = %d", $mysqli->real_escape_string($string), $integer);

echo $query;
// SELECT id, email, pass, name FROM members WHERE email ='x\' OR name LIKE \'%John%' AND id = 2147483647

3
คำถามทั่วไปมาก คำตอบที่ดีบางข้อด้านบน แต่ส่วนใหญ่จะแนะนำข้อความที่เตรียมไว้ MySQLi async ไม่รองรับคำสั่งที่เตรียมไว้ดังนั้น sprintf จึงดูเหมือนเป็นตัวเลือกที่ดีสำหรับสถานการณ์นี้
ดัสตินเกรแฮม

183

ทางเลือกที่ง่ายสำหรับปัญหานี้สามารถแก้ไขได้ด้วยการให้สิทธิ์ที่เหมาะสมในฐานข้อมูลเอง ตัวอย่างเช่น: หากคุณใช้ฐานข้อมูล MySQL ให้ป้อนฐานข้อมูลผ่านเทอร์มินัลหรือ UI ที่ให้ไว้และเพียงทำตามคำสั่งนี้:

 GRANT SELECT, INSERT, DELETE ON database TO username@'localhost' IDENTIFIED BY 'password';

การดำเนินการนี้จะ จำกัด ผู้ใช้ให้รับเฉพาะการสืบค้นที่ระบุเท่านั้น ลบสิทธิ์การลบและข้อมูลจะไม่ถูกลบออกจากการสืบค้นจากหน้า PHP สิ่งที่สองที่ต้องทำคือการล้างสิทธิ์เพื่อให้ MySQL ฟื้นฟูสิทธิ์และการปรับปรุง

FLUSH PRIVILEGES; 

ข้อมูลเพิ่มเติมเกี่ยวกับการล้าง

หากต้องการดูสิทธิ์ปัจจุบันของผู้ใช้จะดำเนินการค้นหาต่อไปนี้

select * from mysql.user where User='username';

เรียนรู้เพิ่มเติมเกี่ยวGRANT


25
คำตอบนี้ผิดโดยหลักแล้วมันไม่ได้ช่วยป้องกันการฉีด แต่เพียงพยายามทำให้ผลที่ตามมาอ่อนลง เปล่า ๆ
สามัญสำนึกของคุณ

1
ใช่มันไม่ได้มีทางออก แต่เป็นสิ่งที่คุณทำได้ก่อนมือเพื่อหลีกเลี่ยงสิ่งต่าง ๆ
Apurv Nerlekar

1
@Aurv หากเป้าหมายของฉันคือการอ่านข้อมูลส่วนตัวจากฐานข้อมูลของคุณดังนั้นการไม่มีสิทธิ์ DELETE ก็ไม่ได้หมายความว่าอะไร
Alex Holsgrove

1
@AlexHolsgrove: เข้าใจง่ายฉันแค่เสนอแนวทางปฏิบัติที่ดีเพื่อลดผลที่จะตามมา
Apurv Nerlekar

2
@Aurv คุณไม่ต้องการที่จะ "ลดผลกระทบ" คุณต้องการทำทุกอย่างที่เป็นไปได้เพื่อป้องกันมัน เพื่อความเป็นธรรมการตั้งค่าการเข้าถึงของผู้ใช้ที่ถูกต้องเป็นสิ่งสำคัญ แต่ไม่ใช่สิ่งที่ OP ต้องการ
Alex Holsgrove

177

เกี่ยวกับคำตอบที่มีประโยชน์มากมายฉันหวังว่าจะเพิ่มคุณค่าให้กับเธรดนี้

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

ตอนนี้จุดของเราคือการป้องกันภัยคุกคามด้านความปลอดภัยเช่นการโจมตีการฉีด SQL คำถามที่ถาม (วิธีการป้องกันการโจมตีการฉีด SQL โดยใช้ PHP) เป็นจริงมากขึ้นการกรองข้อมูลหรือการล้างข้อมูลอินพุตเป็นกรณีเมื่อใช้ข้อมูลผู้ใช้ภายใน แบบสอบถามดังกล่าวการใช้ PHP หรือภาษาการเขียนโปรแกรมอื่น ๆ ไม่ใช่กรณีหรือตามคำแนะนำของผู้คนจำนวนมากให้ใช้เทคโนโลยีที่ทันสมัยเช่นคำสั่งที่เตรียมไว้หรือเครื่องมืออื่น ๆ ที่สนับสนุนการป้องกันการฉีด SQL ในปัจจุบันพิจารณาว่าเครื่องมือเหล่านี้ไม่มีอีกแล้ว? คุณรักษาความปลอดภัยแอปพลิเคชันของคุณอย่างไร?

วิธีการของฉันกับการฉีด SQL คือ: การล้างข้อมูลที่ผู้ใช้ป้อนก่อนที่จะส่งไปยังฐานข้อมูล (ก่อนที่จะใช้ภายในแบบสอบถามใด ๆ )

การกรองข้อมูลสำหรับ (การแปลงข้อมูลที่ไม่ปลอดภัยเป็นข้อมูลที่ปลอดภัย)

พิจารณาว่าPDOและMySQLiไม่พร้อมใช้งาน คุณจะรักษาความปลอดภัยใบสมัครของคุณได้อย่างไร? คุณบังคับให้ฉันใช้พวกเขาหรือไม่? แล้วภาษาอื่นที่ไม่ใช่ PHP ล่ะ ฉันชอบที่จะเสนอแนวคิดทั่วไปเนื่องจากสามารถใช้สำหรับเส้นขอบที่กว้างกว่าไม่ใช่เฉพาะภาษาที่เฉพาะเจาะจง

  1. ผู้ใช้ SQL (จำกัด สิทธิ์ของผู้ใช้): การดำเนินการ SQL ทั่วไปส่วนใหญ่คือ (SELECT, UPDATE, INSERT) แล้วทำไมจึงให้สิทธิ์ UPDATE แก่ผู้ใช้ที่ไม่ต้องการ ตัวอย่างเช่นหน้าการเข้าสู่ระบบและการค้นหาใช้เพียง SELECT จากนั้นเหตุใดจึงใช้ผู้ใช้ DB ในหน้าเหล่านี้ด้วยสิทธิ์ระดับสูง

RULE: ห้ามสร้างผู้ใช้ฐานข้อมูลหนึ่งรายสำหรับสิทธิ์ทั้งหมด สำหรับการดำเนินงาน SQL ทั้งหมดคุณสามารถสร้างแบบแผนของคุณเช่น (ตัวเลือกตัวเลือกตัวปรับปรุง) เป็นชื่อผู้ใช้เพื่อการใช้งานที่ง่าย

ดูหลักการของสิทธิน้อย

  1. การกรองข้อมูล: ก่อนสร้างการป้อนข้อมูลผู้ใช้แบบสอบถามใด ๆ ควรตรวจสอบและกรอง สำหรับโปรแกรมเมอร์มันเป็นสิ่งสำคัญที่จะกำหนดคุณสมบัติบางอย่างสำหรับแต่ละตัวแปรที่ใช้การป้อนข้อมูล: ชนิดข้อมูลรูปแบบข้อมูลและความยาวของข้อมูล เขตข้อมูลที่เป็นตัวเลขระหว่าง (x และ y) จะต้องได้รับการตรวจสอบความถูกต้องโดยใช้กฎที่แน่นอนและสำหรับเขตข้อมูลที่เป็นสตริง (ข้อความ): รูปแบบเป็นกรณีตัวอย่างเช่นชื่อผู้ใช้จะต้องมีอักขระบางตัวเท่านั้น พูด [a-zA-Z0-9_-.] ความยาวแตกต่างกันระหว่าง (x และ n) โดยที่ x และ n (จำนวนเต็ม, x <= n) กฎ: การสร้างตัวกรองและกฎการตรวจสอบที่ถูกต้องเป็นแนวทางปฏิบัติที่ดีที่สุดสำหรับฉัน

  2. ใช้เครื่องมืออื่น ๆ : ที่นี่ฉันจะเห็นด้วยกับคุณว่าคำสั่งที่เตรียมไว้ (แบบสอบถามแบบใช้พารามิเตอร์) และขั้นตอนการจัดเก็บ ข้อเสียของที่นี่คือวิธีเหล่านี้ต้องการทักษะขั้นสูงซึ่งไม่มีอยู่สำหรับผู้ใช้ส่วนใหญ่ แนวคิดพื้นฐานที่นี่คือการแยกความแตกต่างระหว่างแบบสอบถาม SQL และข้อมูลที่ใช้ภายใน ทั้งสองวิธีสามารถใช้ได้แม้กับข้อมูลที่ไม่ปลอดภัยเนื่องจากข้อมูลที่ผู้ใช้ป้อนที่นี่ไม่ได้เพิ่มสิ่งใด ๆ ลงในคิวรีดั้งเดิมเช่น (ใด ๆ หรือ x = x)

สำหรับข้อมูลเพิ่มเติมโปรดอ่านOWASP SQL Injection ป้องกันโกงแผ่น

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

ในที่สุดลองพิจารณาว่าผู้ใช้ส่งข้อความด้านล่างนี้แทนที่จะป้อนชื่อผู้ใช้ของเขา / เธอ:

[1] UNION SELECT IF(SUBSTRING(Password,1,1)='2',BENCHMARK(100000,SHA1(1)),0) User,Password FROM mysql.user WHERE User = 'root'

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

จุดสุดท้ายคือการตรวจจับพฤติกรรมที่ไม่คาดคิดซึ่งต้องใช้ความพยายามและความซับซ้อนมากขึ้น ไม่แนะนำให้ใช้กับเว็บแอปพลิเคชันทั่วไป

พฤติกรรมที่ไม่คาดคิดในอินพุตผู้ใช้ด้านบนคือ SELECT, UNION, IF, SUBSTRING, BENCHMARK, SHA และ root เมื่อตรวจพบคำเหล่านี้คุณสามารถหลีกเลี่ยงการป้อนข้อมูล

อัปเดต 1:

ผู้ใช้แสดงความคิดเห็นว่าโพสต์นี้ไร้ประโยชน์ OK! นี่คือสิ่งที่OWASP.ORG จัดหาให้ :

ป้องกันหลัก:

# 1 ตัวเลือก: การใช้งบเตรียม (Parameterized แบบสอบถาม)
ตัวเลือกที่ 2: การใช้วิธีการจัดเก็บ
ตัวเลือก # 3: หนีทั้งหมดผู้ใช้ที่ป้อนข้อมูล

การป้องกันเพิ่มเติม:

นอกจากนี้ยังบังคับใช้: Privilege น้อย
นอกจากนี้ยังดำเนินการ: รายชื่อสีขาวป้อนข้อมูลการตรวจสอบ

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

อัปเดต 2:

จากคู่มือ PHP, PHP: ข้อความสั่งเตรียมการ - คู่มือ :

การหลบหนีและการฉีด SQL

ตัวแปรที่ถูกผูกไว้จะหนีโดยอัตโนมัติโดยเซิร์ฟเวอร์ เซิร์ฟเวอร์จะแทรกค่า Escape ที่ตำแหน่งที่เหมาะสมลงในเทมเพลตคำสั่งก่อนดำเนินการ ต้องให้คำแนะนำแก่เซิร์ฟเวอร์สำหรับประเภทของตัวแปรที่ผูกไว้เพื่อสร้างการแปลงที่เหมาะสม ดูฟังก์ชัน mysqli_stmt_bind_param () สำหรับข้อมูลเพิ่มเติม

บางครั้งการหลีกเลี่ยงค่าภายในเซิร์ฟเวอร์จะถือเป็นคุณลักษณะด้านความปลอดภัยเพื่อป้องกันการฉีด SQL การรักษาความปลอดภัยระดับเดียวกันสามารถทำได้ด้วยคำสั่งที่ไม่ได้เตรียมไว้หากค่าอินพุตถูกหลีกเลี่ยงอย่างถูกต้อง

อัปเดต 3:

ฉันสร้างกรณีทดสอบเพื่อทราบว่า PDO และ MySQLi ส่งแบบสอบถามไปยังเซิร์ฟเวอร์ MySQL อย่างไรเมื่อใช้คำสั่งที่เตรียมไว้:

PDO:

$user = "''1''"; // Malicious keyword
$sql = 'SELECT * FROM awa_user WHERE userame =:username';
$sth = $dbh->prepare($sql, array(PDO::ATTR_CURSOR => PDO::CURSOR_FWDONLY));
$sth->execute(array(':username' => $user));

บันทึกการสืบค้น:

    189 Query SELECT * FROM awa_user WHERE userame ='\'\'1\'\''
    189 Quit

MySQLi:

$stmt = $mysqli->prepare("SELECT * FROM awa_user WHERE username =?")) {
$stmt->bind_param("s", $user);
$user = "''1''";
$stmt->execute();

บันทึกการสืบค้น:

    188 Prepare   SELECT * FROM awa_user WHERE username =?
    188 Execute   SELECT * FROM awa_user WHERE username ='\'\'1\'\''
    188 Quit

เป็นที่ชัดเจนว่าคำสั่งที่เตรียมไว้นั้นกำลังหลบหนีข้อมูลอยู่

ดังที่ได้กล่าวไว้ในข้อความข้างต้น

บางครั้งการหลีกเลี่ยงค่าภายในเซิร์ฟเวอร์จะถือเป็นคุณลักษณะด้านความปลอดภัยเพื่อป้องกันการฉีด SQL การรักษาความปลอดภัยระดับเดียวกันสามารถทำได้ด้วยคำสั่งที่ไม่ได้เตรียมไว้หากค่าอินพุตถูกหลบหนีอย่างถูกต้อง

ดังนั้นนี่เป็นการพิสูจน์ว่าการตรวจสอบข้อมูลเช่นเป็นintval()ความคิดที่ดีสำหรับค่าจำนวนเต็มก่อนที่จะส่งแบบสอบถามใด ๆ นอกจากนี้การป้องกันข้อมูลของผู้ใช้ที่เป็นอันตรายก่อนที่จะส่งแบบสอบถามเป็นวิธีการที่ถูกต้องและถูกต้อง

โปรดดูคำถามนี้สำหรับรายละเอียดเพิ่มเติม: PDO ส่งแบบสอบถามดิบไปยัง MySQL ในขณะที่ Mysqli ส่งแบบสอบถามที่เตรียมไว้ทั้งคู่ให้ผลลัพธ์เดียวกัน

อ้างอิง:

  1. แผ่นโกง SQL Injection
  2. การฉีด SQL
  3. ความปลอดภัยของข้อมูล
  4. หลักการรักษาความปลอดภัย
  5. การตรวจสอบข้อมูล

175

คำเตือนความปลอดภัย : คำตอบนี้ไม่สอดคล้องกับแนวทางปฏิบัติด้านความปลอดภัยที่ดีที่สุด การหลบหนีไม่เพียงพอเพื่อป้องกันการฉีด SQLให้ใช้คำสั่งที่เตรียมไว้แทน ใช้กลยุทธ์ที่ระบุไว้ด้านล่างด้วยความเสี่ยงของคุณเอง (นอกจากนี้mysql_real_escape_string()ถูกลบออกใน PHP 7)

คำเตือนเลิกใช้ : ส่วนขยาย mysql ถูกคัดค้านในขณะนี้ เราขอแนะนำให้ใช้ส่วนขยาย PDO

ฉันใช้สามวิธีที่แตกต่างกันเพื่อป้องกันไม่ให้แอปพลิเคชันบนเว็บของฉันเสี่ยงต่อการฉีด SQL

  1. การใช้mysql_real_escape_string()ซึ่งเป็นฟังก์ชั่นที่กำหนดไว้ล่วงหน้าในPHPและรหัสนี้เพิ่มเครื่องหมายตัวอักษรต่อไปนี้: \x00, \n, \r, \, ', และ" \x1aส่งค่าอินพุตเป็นพารามิเตอร์เพื่อลดโอกาสในการฉีด SQL
  2. วิธีที่ทันสมัยที่สุดคือการใช้ PDO

ฉันหวังว่านี่จะช่วยคุณได้

พิจารณาคำถามต่อไปนี้:

$iId = mysql_real_escape_string("1 OR 1=1"); $sSql = "SELECT * FROM table WHERE id = $iId";

mysql_real_escape_string () จะไม่ปกป้องที่นี่ หากคุณใช้เครื่องหมายคำพูดเดี่ยว ('') รอบ ๆ ตัวแปรภายในข้อความค้นหาของคุณคือสิ่งที่ปกป้องคุณจากสิ่งนี้ นี่คือวิธีแก้ปัญหาด้านล่างนี้:

$iId = (int) mysql_real_escape_string("1 OR 1=1"); $sSql = "SELECT * FROM table WHERE id = $iId";

คำถามนี้มีคำตอบที่ดีเกี่ยวกับเรื่องนี้

ฉันแนะนำให้ใช้ PDO เป็นตัวเลือกที่ดีที่สุด

แก้ไข:

mysql_real_escape_string()เลิกใช้ตั้งแต่ PHP 5.5.0 ใช้ mysqli หรือ PDO

อีกทางเลือกหนึ่งสำหรับ mysql_real_escape_string () คือ

string mysqli_real_escape_string ( mysqli $link , string $escapestr )

ตัวอย่าง:

$iId = $mysqli->real_escape_string("1 OR 1=1");
$mysqli->query("SELECT * FROM table WHERE id = $iId");

169

วิธีง่ายๆคือใช้กรอบ PHP เช่นCodeIgniterหรือLaravelซึ่งมีคุณสมบัติ inbuilt เช่นการกรองและแอคทีฟเรคคอร์ดเพื่อให้คุณไม่ต้องกังวลกับความแตกต่างเหล่านี้


7
ฉันคิดว่าประเด็นทั้งหมดของคำถามคือการทำสิ่งนี้ให้สำเร็จโดยไม่ใช้กรอบดังกล่าว
Sanke

147

คำเตือน: วิธีการที่อธิบายไว้ในคำตอบนี้จะใช้กับสถานการณ์ที่เฉพาะเจาะจงเท่านั้นและไม่ปลอดภัยเนื่องจากการโจมตีด้วยการฉีด SQL ไม่เพียง แต่พึ่งพาความสามารถในการฉีดX=Yเท่านั้น

หากผู้โจมตีพยายามแฮ็คลงในแบบฟอร์มผ่าน$_GETตัวแปรของ PHP หรือด้วยสตริงการสืบค้นของ URL คุณจะสามารถตรวจจับพวกเขาได้หากพวกเขาไม่ปลอดภัย

RewriteCond %{QUERY_STRING} ([0-9]+)=([0-9]+)
RewriteRule ^(.*) ^/track.php

เพราะ1=1, 2=2, 1=2, 2=1, 1+1=2ฯลฯ ... เป็นคำถามที่พบบ่อยกับฐานข้อมูล SQL ของผู้โจมตี อาจเป็นเพราะแอปพลิเคชั่นแฮ็คจำนวนมาก

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


อะไรขึ้นกับ1-1=0? :)
RápliAndrás

@ ([0-9\-]+)=([0-9]+)RápliAndrásการจัดเรียงบางส่วนของ
5ervant

127

มีคำตอบมากมายสำหรับPHP และ MySQLแต่นี่คือรหัสสำหรับPHP และ Oracleเพื่อป้องกันการฉีด SQL รวมถึงการใช้ไดรเวอร์ oci8 เป็นประจำ:

$conn = oci_connect($username, $password, $connection_string);
$stmt = oci_parse($conn, 'UPDATE table SET field = :xx WHERE ID = 123');
oci_bind_by_name($stmt, ':xx', $fieldval);
oci_execute($stmt);

โปรดอธิบายพารามิเตอร์ oci_bind_by_name
Jahanzeb Awan

127

ความคิดที่ดีคือการใช้mapper เชิงวัตถุเช่นIdiorm :

$user = ORM::for_table('user')
->where_equal('username', 'j4mie')
->find_one();

$user->first_name = 'Jamie';
$user->save();

$tweets = ORM::for_table('tweet')
    ->select('tweet.*')
    ->join('user', array(
        'user.id', '=', 'tweet.user_id'
    ))
    ->where_equal('user.username', 'j4mie')
    ->find_many();

foreach ($tweets as $tweet) {
    echo $tweet->text;
}

มันไม่เพียง แต่ช่วยคุณประหยัดจากการฉีด SQL แต่ยังมาจากข้อผิดพลาดทางไวยากรณ์ด้วยเช่นกัน! นอกจากนี้ยังสนับสนุนคอลเลกชันของรุ่นที่มีวิธีการผูกมัดเพื่อกรองหรือนำการกระทำไปใช้กับผลลัพธ์หลายรายการพร้อมกันและการเชื่อมต่อที่หลากหลาย


124

คำเตือนเลิกใช้: รหัสตัวอย่างของคำตอบนี้ (เช่นรหัสตัวอย่างของคำถาม) ใช้MySQLส่วนขยายของ PHP ซึ่งเลิกใช้แล้วใน PHP 5.5.0 และลบออกทั้งหมดใน PHP 7.0.0

คำเตือนความปลอดภัย : คำตอบนี้ไม่สอดคล้องกับแนวทางปฏิบัติด้านความปลอดภัยที่ดีที่สุด การหลบหนีไม่เพียงพอเพื่อป้องกันการฉีด SQLให้ใช้คำสั่งที่เตรียมไว้แทน ใช้กลยุทธ์ที่ระบุไว้ด้านล่างด้วยความเสี่ยงของคุณเอง (นอกจากนี้mysql_real_escape_string()ถูกลบออกใน PHP 7)

การใช้PDOและMYSQLiเป็นวิธีปฏิบัติที่ดีในการป้องกันการแทรก SQL แต่ถ้าคุณต้องการทำงานกับฟังก์ชั่น MySQL และการสืบค้นจริงๆจะเป็นการดีกว่าที่จะใช้

mysql_real_escape_string

$unsafe_variable = mysql_real_escape_string($_POST['user_input']);

มีความสามารถมากขึ้นในการป้องกันสิ่งนี้เช่นระบุถ้าอินพุตเป็นสตริงตัวเลขอักขระหรืออาร์เรย์มีฟังก์ชัน inbuilt จำนวนมากที่ตรวจจับสิ่งนี้ นอกจากนี้จะเป็นการดีกว่าถ้าใช้ฟังก์ชั่นเหล่านี้เพื่อตรวจสอบข้อมูลอินพุต

is_string

$unsafe_variable = (is_string($_POST['user_input']) ? $_POST['user_input'] : '');

is_numeric

$unsafe_variable = (is_numeric($_POST['user_input']) ? $_POST['user_input'] : '');

mysql_real_escape_stringและมันก็เป็นมากดีกว่าการใช้ฟังก์ชั่นเหล่านั้นเพื่อตรวจสอบการป้อนข้อมูลด้วย


10
นอกจากนี้ยังไม่มีจุดในการตรวจสอบสมาชิกอาร์เรย์ $ _POST กับ is_string ()
Your Common Sense

21
คำเตือน! ไม่ได้เป็นความผิดmysql_real_escape_string()
eggyal

10
mysql_real_escape_stringเลิกใช้แล้วดังนั้นจึงไม่มีตัวเลือกที่ใช้การได้อีกต่อไป มันจะถูกลบออกจาก PHP ในอนาคต เป็นการดีที่สุดที่จะไปยังสิ่งที่ PHP หรือ MySQL แนะนำ
jww

2
ชุดรูปแบบ: อย่าเชื่อถือข้อมูลที่ผู้ใช้ส่ง สิ่งที่คุณคาดหวังคือข้อมูลขยะที่มีอักขระพิเศษหรือตรรกะบูลีนซึ่งตัวเองควรเป็นส่วนหนึ่งของแบบสอบถาม SQL ที่คุณอาจกำลังดำเนินการ เก็บค่า $ _POST เป็นข้อมูลเท่านั้นไม่ใช่ส่วนของ SQL
Bimal Poudel

88

ฉันได้เขียนฟังก์ชั่นเล็ก ๆ นี้เมื่อหลายปีก่อน:

function sqlvprintf($query, $args)
{
    global $DB_LINK;
    $ctr = 0;
    ensureConnection(); // Connect to database if not connected already.
    $values = array();
    foreach ($args as $value)
    {
        if (is_string($value))
        {
            $value = "'" . mysqli_real_escape_string($DB_LINK, $value) . "'";
        }
        else if (is_null($value))
        {
            $value = 'NULL';
        }
        else if (!is_int($value) && !is_float($value))
        {
            die('Only numeric, string, array and NULL arguments allowed in a query. Argument '.($ctr+1).' is not a basic type, it\'s type is '. gettype($value). '.');
        }
        $values[] = $value;
        $ctr++;
    }
    $query = preg_replace_callback(
        '/{(\\d+)}/', 
        function($match) use ($values)
        {
            if (isset($values[$match[1]]))
            {
                return $values[$match[1]];
            }
            else
            {
                return $match[0];
            }
        },
        $query
    );
    return $query;
}

function runEscapedQuery($preparedQuery /*, ...*/)
{
    $params = array_slice(func_get_args(), 1);
    $results = runQuery(sqlvprintf($preparedQuery, $params)); // Run query and fetch results.   
    return $results;
}

นี้ช่วยให้การเรียกใช้งบในหนึ่งซับ C # -ish String.Format ชอบ:

runEscapedQuery("INSERT INTO Whatever (id, foo, bar) VALUES ({0}, {1}, {2})", $numericVar, $stringVar1, $stringVar2);

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

การปรับปรุงความปลอดภัย: str_replaceรุ่นก่อนหน้าอนุญาตการฉีดโดยการเพิ่มโทเค็น {#} ลงในข้อมูลผู้ใช้ นี้preg_replace_callbackรุ่นที่ไม่ก่อให้เกิดปัญหาหากทดแทนมีราชสกุลเหล่านี้

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