Câu trả lời ngắn gọn là KHÔNG , PDO chuẩn bị sẽ không bảo vệ bạn khỏi tất cả các cuộc tấn công SQL-Injection có thể. Đối với các trường hợp cạnh tối nghĩa nhất định.
Tôi đang điều chỉnh câu trả lời này để nói về PDO ...
Câu trả lời dài không quá dễ. Nó dựa trên một cuộc tấn công được chứng minh ở đây .
Cuộc tấn công
Vì vậy, hãy bắt đầu bằng cách hiển thị cuộc tấn công ...
$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));
Trong một số trường hợp nhất định, điều đó sẽ trả về hơn 1 hàng. Hãy phân tích những gì đang diễn ra ở đây:
Chọn một bộ ký tự
$pdo->query('SET NAMES gbk');
Để cuộc tấn công này hoạt động, chúng ta cần mã hóa mà máy chủ mong đợi trên cả kết nối để mã hóa '
như trong ASCII tức là 0x27
và để có một số ký tự có byte cuối cùng là ASCII \
tức là 0x5c
. Khi nó quay ra, có 5 mã hóa như hỗ trợ trong MySQL 5.6 theo mặc định: big5
, cp932
, gb2312
, gbk
và sjis
. Chúng tôi sẽ chọn gbk
ở đây.
Bây giờ, rất quan trọng để lưu ý việc sử dụng SET NAMES
ở đây. Cái này đặt bộ ký tự TRÊN MÁY CHỦ . Có một cách khác để làm điều đó, nhưng chúng ta sẽ đến đó sớm thôi.
Tải trọng
Tải trọng mà chúng ta sẽ sử dụng cho phép tiêm này bắt đầu bằng chuỗi byte 0xbf27
. Trong gbk
đó, đó là một nhân vật đa bào không hợp lệ; trong latin1
, đó là chuỗi ¿'
. Lưu ý rằng trong latin1
và gbk
, 0x27
trên chính nó là một '
nhân vật theo nghĩa đen .
Chúng tôi đã chọn tải trọng này bởi vì, nếu chúng tôi gọi addslashes()
nó, chúng tôi sẽ chèn ASCII \
tức là 0x5c
trước '
ký tự. Vì vậy, chúng tôi sẽ kết thúc 0xbf5c27
, trong đó gbk
là một chuỗi hai ký tự: 0xbf5c
theo sau 0x27
. Hay nói cách khác, một nhân vật hợp lệ theo sau là một người không được giải thoát '
. Nhưng chúng tôi không sử dụng addslashes()
. Vì vậy, bước tiếp theo ...
$ stmt-> thực thi ()
Điều quan trọng cần nhận ra ở đây là PDO theo mặc định KHÔNG thực hiện các tuyên bố chuẩn bị thực sự. Nó mô phỏng chúng (đối với MySQL). Do đó, PDO bên trong xây dựng chuỗi truy vấn, gọi mysql_real_escape_string()
(hàm API C của MySQL) trên mỗi giá trị chuỗi bị ràng buộc.
Lệnh gọi API C mysql_real_escape_string()
khác với addslashes()
ở chỗ nó biết bộ ký tự kết nối. Vì vậy, nó có thể thực hiện thoát đúng cho bộ ký tự mà máy chủ đang mong đợi. Tuy nhiên, cho đến thời điểm này, khách hàng nghĩ rằng chúng tôi vẫn đang sử dụng latin1
cho kết nối, bởi vì chúng tôi không bao giờ nói với nó khác. Chúng tôi đã nói với máy chủ chúng tôi đang sử dụng gbk
, nhưng khách hàng vẫn nghĩ rằng nó latin1
.
Do đó, lệnh gọi mysql_real_escape_string()
chèn dấu gạch chéo ngược và chúng tôi có một '
ký tự treo miễn phí trong nội dung "thoát" của chúng tôi! Thực tế, nếu chúng ta nhìn vào $var
bộ gbk
ký tự, chúng ta sẽ thấy:
縗 'HOẶC 1 = 1 / *
Đó chính xác là những gì cuộc tấn công yêu cầu.
Truy vấn
Phần này chỉ là một hình thức, nhưng đây là truy vấn được kết xuất:
SELECT * FROM test WHERE name = '縗' OR 1=1 /*' LIMIT 1
Xin chúc mừng, bạn vừa tấn công thành công một chương trình sử dụng Tuyên bố chuẩn bị PDO ...
Cách khắc phục đơn giản
Bây giờ, đáng chú ý là bạn có thể ngăn chặn điều này bằng cách vô hiệu hóa các câu lệnh được chuẩn bị mô phỏng:
$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
Điều này thường sẽ dẫn đến một tuyên bố được chuẩn bị thực sự (tức là dữ liệu được gửi trong một gói riêng biệt từ truy vấn). Tuy nhiên, lưu ý rằng PDO sẽ âm thầm dự phòng các giả lập mà MySQL không thể chuẩn bị một cách tự nhiên: những thứ mà nó có thể được liệt kê trong hướng dẫn, nhưng hãy cẩn thận để chọn phiên bản máy chủ phù hợp).
Sửa lỗi chính xác
Vấn đề ở đây là chúng tôi đã không gọi API C mysql_set_charset()
thay vì SET NAMES
. Nếu chúng tôi làm như vậy, chúng tôi sẽ ổn nếu chúng tôi sử dụng bản phát hành MySQL từ năm 2006.
Nếu bạn đang sử dụng một phiên bản MySQL trước, sau đó là một lỗi trong mysql_real_escape_string()
có nghĩa là ký tự nhiều byte không hợp lệ như những người trong Payload của chúng tôi bị đối xử như byte duy nhất cho mục đích thoát ngay cả khi khách hàng đã được thông báo một cách chính xác của mã hóa kết nối và do đó cuộc tấn công này sẽ vẫn thành công Lỗi này đã được cố định trong MySQL 4.1.20 , 5.0.22 và 5.1.11 .
Nhưng điều tồi tệ nhất là PDO
đã không để lộ API C cho mysql_set_charset()
đến ngày 5.3.6, vì vậy trong các phiên bản trước, nó không thể ngăn chặn cuộc tấn công này cho mọi lệnh có thể! Bây giờ nó được hiển thị dưới dạng tham số DSN , nên được sử dụng thay vì SET NAMES
...
Ân điển cứu rỗi
Như chúng ta đã nói ngay từ đầu, để cuộc tấn công này hoạt động, kết nối cơ sở dữ liệu phải được mã hóa bằng bộ ký tự dễ bị tổn thương. utf8mb4
là không dễ bị tổn thương và chưa thể hỗ trợ tất cả các ký tự Unicode: vì vậy bạn có thể chọn để sử dụng mà thay vào đó, nhưng nó đã chỉ được đưa ra từ MySQL 5.5.3. Một cách khác là utf8
, nó cũng không dễ bị tổn thương và có thể hỗ trợ toàn bộ Mặt phẳng đa ngôn ngữ Unicode Basic .
Ngoài ra, bạn có thể kích hoạt NO_BACKSLASH_ESCAPES
chế độ SQL, trong đó (trong số những thứ khác) làm thay đổi hoạt động của mysql_real_escape_string()
. Khi bật chế độ này, 0x27
sẽ được thay thế bằng 0x2727
thay vì 0x5c27
và do đó, quá trình thoát không thể tạo các ký tự hợp lệ trong bất kỳ mã hóa dễ bị tổn thương nào mà chúng không tồn tại trước đó (nghĩa 0xbf27
là vẫn 0xbf27
v.v.) - vì vậy máy chủ vẫn sẽ từ chối chuỗi là không hợp lệ . Tuy nhiên, hãy xem câu trả lời của @ eggyal để biết một lỗ hổng khác có thể phát sinh từ việc sử dụng chế độ SQL này (mặc dù không phải với PDO).
Ví dụ an toàn
Các ví dụ sau đây là an toàn:
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");
Bởi vì máy chủ đang mong đợi 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");
Bởi vì chúng tôi đã đặt đúng bộ ký tự sao cho máy khách và máy chủ khớp.
$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 /*"));
Bởi vì chúng tôi đã tắt các tuyên bố chuẩn bị mô phỏng.
$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 /*"));
Bởi vì chúng tôi đã thiết lập đúng ký tự.
$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();
Bởi vì MySQLi luôn luôn chuẩn bị các tuyên bố chuẩn bị.
Kết thúc
Nếu bạn:
- Sử dụng các phiên bản hiện đại của MySQL (cuối 5.1, tất cả 5.5, 5.6, v.v.) và tham số bộ ký tự DSN của PDO (trong PHP ≥ 5.3.6)
HOẶC LÀ
- Không sử dụng bộ ký tự dễ bị tổn thương để mã hóa kết nối (bạn chỉ sử dụng
utf8
/ latin1
/ ascii
/ etc)
HOẶC LÀ
- Kích hoạt
NO_BACKSLASH_ESCAPES
chế độ SQL
Bạn an toàn 100%.
Mặt khác, bạn dễ bị tổn thương ngay cả khi bạn đang sử dụng Tuyên bố chuẩn bị PDO ...
Phụ lục
Tôi đã dần dần làm việc trên một bản vá để thay đổi mặc định để không giả lập chuẩn bị cho phiên bản PHP trong tương lai. Vấn đề mà tôi gặp phải là rất nhiều bài kiểm tra bị hỏng khi tôi làm điều đó. Một vấn đề là các chế phẩm được mô phỏng sẽ chỉ ném lỗi cú pháp khi thực thi, nhưng các chuẩn bị thực sự sẽ ném lỗi khi chuẩn bị. Vì vậy, điều đó có thể gây ra vấn đề (và là một phần của các bài kiểm tra lý do đang làm hỏng).