คำตอบสั้น ๆ คือไม่ PDO ที่เตรียมไว้จะไม่ปกป้องคุณจากการโจมตี SQL-Injection ที่เป็นไปได้ทั้งหมด สำหรับขอบบางกรณีที่คลุมเครือ
ฉันกำลังปรับคำตอบนี้เพื่อพูดคุยเกี่ยวกับ PDO ...
คำตอบที่ยาวนั้นไม่ใช่เรื่องง่าย ก็ขึ้นอยู่การโจมตีแสดงให้เห็นที่นี่
การโจมตี
ดังนั้นเรามาเริ่มต้นด้วยการแสดงการโจมตี ...
$pdo->query('SET NAMES gbk');
$var = "\xbf\x27 OR 1=1 /*";
$query = 'SELECT * FROM test WHERE name = ? LIMIT 1';
$stmt = $pdo->prepare($query);
$stmt->execute(array($var));
ในบางสถานการณ์นั่นจะส่งคืนมากกว่า 1 แถว มาวิเคราะห์กันว่าเกิดอะไรขึ้นที่นี่:
การเลือกชุดตัวละคร
$pdo->query('SET NAMES gbk');
สำหรับการโจมตีการทำงานเราต้องเข้ารหัสที่เซิร์ฟเวอร์ของคาดหวังว่าในการเชื่อมต่อทั้งในการเข้ารหัส'
ในขณะที่เช่น ASCII 0x27
และจะมีตัวละครบางคนที่มีไบต์สุดท้ายเป็น ASCII คือ\
0x5c
มันจะเปิดออกมี 5 การเข้ารหัสดังกล่าวได้รับการสนับสนุนใน MySQL 5.6 โดยค่าเริ่มต้น: big5
, cp932
, gb2312
, และgbk
sjis
เราจะเลือกgbk
ที่นี่
ตอนนี้มันสำคัญมากที่จะต้องสังเกตการใช้SET NAMES
ที่นี่ ชุดนี้ชุดอักขระบนเซิร์ฟเวอร์ มีวิธีทำอีกวิธีหนึ่ง แต่เราจะไปถึงที่นั่นเร็วพอ
ส่วนของข้อมูล
0xbf27
น้ำหนักบรรทุกที่เรากำลังจะใช้สำหรับการฉีดนี้เริ่มต้นด้วยลำดับไบต์ ในgbk
นั้นเป็นอักขระหลายไบต์ที่ไม่ถูกต้อง ในก็สตริงlatin1
¿'
โปรดสังเกตว่าในlatin1
และ gbk
, 0x27
ในตัวเองเป็นตัวอักษร'
ตัวอักษร
เราได้เลือกเพย์โหลดนี้เพราะถ้าเราเรียกaddslashes()
มันเราจะใส่ ASCII \
เช่น0x5c
ก่อน'
ตัวละคร ดังนั้นเราจึงต้องการปิดท้ายด้วย0xbf5c27
ซึ่งgbk
เป็นลำดับสองตัวอักษร: ตามมาด้วย0xbf5c
0x27
หรือคำอื่น ๆที่ถูกต้อง'
ของตัวละครตามมาด้วยการไม่ใช้ Escape addslashes()
แต่เราไม่ได้ใช้ ดังนั้นไปยังขั้นตอนต่อไป ...
$ stmt-> รัน ()
สิ่งสำคัญที่ต้องตระหนักที่นี่คือ PDO โดยค่าเริ่มต้นไม่ได้ทำงบเตรียมไว้จริง มันเลียนแบบพวกเขา (สำหรับ MySQL) ดังนั้น PDO จึงสร้างสตริงข้อความค้นหาเรียกmysql_real_escape_string()
(ฟังก์ชัน MySQL C API) ภายในแต่ละค่าสตริงที่ถูกผูกไว้
การเรียก C API เพื่อให้mysql_real_escape_string()
แตกต่างจากaddslashes()
ที่รู้ชุดอักขระการเชื่อมต่อ ดังนั้นจึงสามารถทำการหลบหนีได้อย่างถูกต้องสำหรับชุดอักขระที่เซิร์ฟเวอร์คาดหวัง อย่างไรก็ตามจนถึงจุดนี้ลูกค้าคิดว่าเรายังคงใช้latin1
การเชื่อมต่ออยู่เพราะเราไม่เคยบอกเป็นอย่างอื่น เราไม่ได้บอกเซิร์ฟเวอร์ที่เรากำลังใช้gbk
แต่ลูกค้าlatin1
ยังคงคิดว่ามันเป็น
ดังนั้นการโทรเพื่อmysql_real_escape_string()
แทรกแบ็กสแลชและเรามี'
ตัวละครลอยฟรีในเนื้อหา "หนี" ของเรา! อันที่จริงถ้าเราต้องดู$var
ในgbk
ชุดตัวละครเราจะได้เห็น:
縗 'หรือ 1 = 1 / *
ซึ่งเป็นสิ่งที่การโจมตีต้องการ
คำค้นหา
ส่วนนี้เป็นเพียงพิธีการ แต่นี่คือแบบสอบถามที่แสดงผล:
SELECT * FROM test WHERE name = '縗' OR 1=1 /*' LIMIT 1
ขอแสดงความยินดีคุณโจมตีโปรแกรมโดยใช้ PDO Prepared Statement ...
การแก้ไขง่าย ๆ
ตอนนี้ก็เป็นที่น่าสังเกตว่าคุณสามารถป้องกันสิ่งนี้ได้โดยการปิดการใช้งานคำสั่งจำลองที่เตรียมไว้
$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
นี้จะมักจะส่งผลให้งบเตรียมความจริง (เช่นข้อมูลที่ถูกส่งผ่านในแพ็คเก็ตที่แยกต่างหากจากแบบสอบถาม) อย่างไรก็ตามโปรดทราบว่า PDO จะย้อนกลับไปเลียนแบบข้อความที่ MySQL ไม่สามารถเตรียมความพร้อมได้: ข้อความที่สามารถแสดงรายการในคู่มือได้ แต่ควรระวังในการเลือกรุ่นเซิร์ฟเวอร์ที่เหมาะสม)
การแก้ไขที่ถูกต้อง
นี่คือปัญหาที่เราไม่ได้เรียก C API เป็นแทนmysql_set_charset()
SET NAMES
ถ้าเราทำเช่นนั้นเราก็จะถูกปรับถ้าเราใช้ MySQL รุ่นตั้งแต่ปี 2549
หากคุณกำลังใช้รุ่น MySQL ก่อนหน้านี้แล้วข้อผิดพลาดในmysql_real_escape_string()
ความหมายว่าตัวอักษรสัญลักษณ์ที่ไม่ถูกต้องเช่นผู้ที่อยู่ในส่วนของข้อมูลของเราได้รับการรักษาเป็นไบต์เดียวสำหรับวัตถุประสงค์ในการหลบหนีแม้กระทั่งในกรณีที่ลูกค้าได้รับแจ้งอย่างถูกต้องของการเข้ารหัสการเชื่อมต่อและการโจมตีครั้งนี้จะ ยังคงประสบความสำเร็จ ข้อผิดพลาดได้รับการแก้ไขใน MySQL 4.1.20 , 5.0.22และ5.1.11
แต่ส่วนที่แย่ที่สุดคือPDO
ไม่ได้เปิดเผย C API เป็นเวลาmysql_set_charset()
จนถึง 5.3.6 ดังนั้นในเวอร์ชันก่อนหน้านี้จึงไม่สามารถป้องกันการโจมตีนี้สำหรับทุกคำสั่งที่เป็นไปได้! มันถูกเปิดเผยเป็นพารามิเตอร์ DSNซึ่งควรใช้แทน SET NAMES
...
พระคุณประหยัด
ดังที่เราได้กล่าวไว้ในตอนต้นสำหรับการโจมตีครั้งนี้เพื่อการทำงานการเชื่อมต่อฐานข้อมูลจะต้องเข้ารหัสโดยใช้ชุดอักขระที่มีช่องโหว่ utf8mb4
คือไม่เสี่ยงและยังสามารถรองรับทุกอักขระ Unicode: เพื่อให้คุณสามารถเลือกที่จะใช้ที่แทน แต่จะได้รับเพียงมีตั้งแต่ MySQL 5.5.3 อีกทางเลือกหนึ่งคือutf8
ซึ่งไม่เสี่ยงและสามารถรองรับ Unicode Basic Multilingual Planeทั้งหมด
หรือคุณสามารถเปิดใช้งานNO_BACKSLASH_ESCAPES
โหมด SQL ซึ่ง (ในหมู่สิ่งอื่น ๆ ) alters mysql_real_escape_string()
การดำเนินงานของ เมื่อเปิดใช้งานโหมดนี้0x27
จะถูกแทนที่ด้วย0x2727
มากกว่า0x5c27
และทำให้กระบวนการหลบหนีไม่สามารถสร้างตัวละครที่ถูกต้องในการเข้ารหัสที่มีช่องโหว่ที่ไม่เคยมีอยู่ก่อนหน้า (เช่น0xbf27
ยัง0xbf27
เป็นต้น) - ดังนั้นเซิร์ฟเวอร์จะปฏิเสธสตริงที่ไม่ถูกต้อง . อย่างไรก็ตามดูคำตอบของ @ eggyalเพื่อหาช่องโหว่อื่นที่อาจเกิดขึ้นจากการใช้โหมด SQL นี้ (แม้ว่าจะไม่ใช่ PDO)
ตัวอย่างที่ปลอดภัย
ตัวอย่างต่อไปนี้ปลอดภัย:
mysql_query('SET NAMES utf8');
$var = mysql_real_escape_string("\xbf\x27 OR 1=1 /*");
mysql_query("SELECT * FROM test WHERE name = '$var' LIMIT 1");
เนื่องจากเซิร์ฟเวอร์ต้องการutf8
...
mysql_set_charset('gbk');
$var = mysql_real_escape_string("\xbf\x27 OR 1=1 /*");
mysql_query("SELECT * FROM test WHERE name = '$var' LIMIT 1");
เนื่องจากเราได้ตั้งค่าชุดอักขระอย่างถูกต้องเพื่อให้ไคลเอ็นต์และเซิร์ฟเวอร์ตรงกัน
$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
$pdo->query('SET NAMES gbk');
$stmt = $pdo->prepare('SELECT * FROM test WHERE name = ? LIMIT 1');
$stmt->execute(array("\xbf\x27 OR 1=1 /*"));
เพราะเราได้ปิดคำสั่งที่เลียนแบบแล้ว
$pdo = new PDO('mysql:host=localhost;dbname=testdb;charset=gbk', $user, $password);
$stmt = $pdo->prepare('SELECT * FROM test WHERE name = ? LIMIT 1');
$stmt->execute(array("\xbf\x27 OR 1=1 /*"));
เพราะเราได้ตั้งค่าตัวละครไว้อย่างเหมาะสม
$mysqli->query('SET NAMES gbk');
$stmt = $mysqli->prepare('SELECT * FROM test WHERE name = ? LIMIT 1');
$param = "\xbf\x27 OR 1=1 /*";
$stmt->bind_param('s', $param);
$stmt->execute();
เพราะ MySQLi ทำงบจริงตลอดเวลา
ห่อ
ถ้าคุณ:
- ใช้ MySQL รุ่นใหม่ (ตอนปลาย 5.1, 5.5, 5.6, ฯลฯ ) และพารามิเตอร์ DSN charset ของ PDO (ใน PHP ≥ 5.3.6)
หรือ
- อย่าใช้ชุดอักขระที่มีช่องโหว่สำหรับการเข้ารหัสการเชื่อมต่อ (คุณใช้เพียง
utf8
/ latin1
/ ascii
/ ฯลฯ )
หรือ
- เปิดใช้งาน
NO_BACKSLASH_ESCAPES
โหมด SQL
คุณปลอดภัย 100%
มิฉะนั้นคุณจะมีความเสี่ยงแม้ว่าคุณจะใช้งบเตรียม PDO ...
ภาคผนวก
ฉันทำงานอย่างช้าๆเพื่อแก้ไขค่าเริ่มต้นเพื่อไม่เลียนแบบการเตรียมการสำหรับ PHP เวอร์ชันในอนาคต ปัญหาที่ฉันพบคือการทดสอบจำนวนมากแตกสลายเมื่อฉันทำเช่นนั้น ปัญหาหนึ่งคือการเตรียมการจำลองที่จะโยนข้อผิดพลาดทางไวยากรณ์ในการดำเนินการ แต่การเตรียมการจริงจะโยนข้อผิดพลาดในการเตรียมการ เพื่อที่จะทำให้เกิดปัญหา (และเป็นส่วนหนึ่งของการทดสอบเหตุผลที่บอร์ก)