bookmark_borderMySQL issue – Type conversion – Bug or Feature?

Nay gặp một bug security củ chuối liên quan đến Laravel và MySQL.

Như mọi người đều biết thì hầu hết các framework hiện nay thằng nào cũng truy vấn CSDL dựa trên Query Builder thay vì phải viết raw query như hồi xưa. Một lợi ích của nó là cung cấp 1 phương thức giúp các dev binding dữ liệu vào câu query được an toàn. Nhưng cũng chính vì thế mà Query Builder trở thành 1 cái blackbox. Dev viết truy vấn CSDL bằng Query Builder mà không cần biết câu raw query thằng Builder đó sinh ra sẽ như thế nào (trừ những lúc cần thiết lắm thì mới phải trace debug xem hình thù nó ra làm sao). Và điều đó vô tình dẫn đến mấy cái lỗi trời ơi đất hỡi mà nếu không “ngẫu nhiên” được “trải nghiệm” thử thì cũng chẳng biết nó tồn tại.

Bắt đầu với câu truy vấn đơn giản như sau, khi muốn lấy ra record trong bảng usersid bằng 1:

select * from `users` where `id` = 1

Với Laravel, việc thực hiện điều trên trở nên kín đáo hơn:

// User.php
class User extends Model {
    public $table = 'users';
}

// Query user whose id equals to 1
$id = 1; 
$user = User::find($id);

// Dumping data
var_dump($user); // User object

Rồi, tiếp theo thử dự đoán kết quả cho câu sau:

// Query user whose id equals to something else
$id = '1+23456789';
$user = User::find($id);

// Guess the data ?
var_dump($user); // User object is still here

Điều gì đã xảy ra:

// Case 1
$id = 1;
$user = User::find($id); // => select * from `users` where `id` = '1' limit 1

// Case 2
$id = '1+23456789';
$user = User::find($id); // => select * from `users` where `id` = '1+23456789' limit 1

Như đã thấy, ở cả 2 trường hợp, dữ liệu binding vào câu query đều được Query Builder bao trọn trong dấu nháy đơn ( ' – single quote ). Và khi câu query này được send tới MySQL để thực hiện thì điều duy nhất mong muốn xảy ra ở đây là trường hợp 1 truy vấn được kết quả, trường hợp 2 phải gặp lỗi – do id là field có kiểu int.

Nhưng mà đời không như là mơ. Theo “quy tắc” của MySQL (tham khảo ở đây: https://dev.mysql.com/doc/refman/8.0/en/type-conversion.html), câu lệnh của trường hợp 2 vẫn thực hiện được:

-- This query is .. 
select * from `users` where `id` = '1+23456789' limit 1

-- .. becoming this .. 
select * from `users` where `id` = cast('1+23456789' as int) limit 1

-- .. and finally it turns into this. Bravo! 
select * from `users` where `id` = 1 limit 1

Give thanks to MySQL. You did a great job!

MySQL đã “tự ý” chuyển đổi chuỗi giá trị thành kiểu số, khiến cho câu truy vấn trở nên hợp lệ. Kết quả vẫn được trả về dẫu cho input đầu vào theo mắt thường thì rõ ràng sai rành rành. Việc kết hợp giữa cách binding dữ liệu của công cụ Query Builder nằm trong Laravel cùng với kỹ thuật Type conversion của MySQL đã làm nảy sinh 1 lỗi tiềm ẩn nên được chú ý trong thời gian tới.

bookmark_borderCài đặt PHP 7.4

PHP vừa mới ra phiên bản 7.4 hôm 28/11/2019. (Xem changelog ở đây).

Nay rảnh nên quyết định cài lên VPS.

1. PHP Package

Trước tiên, để keep-up-to-date với các phiên bản PHP, mình sử dụng PPA của bạn Ondrej (https://launchpad.net/~ondrej/+archive/ubuntu/php):

add-apt-repository ppa:ondrej/php
apt update

Very easy. Lúc này, mình có thể cài đặt bất kỳ phiên bản PHP nào, từ các bản 7.0, 7.2, vv.. cho đến bản 7.4 mới ra lò.

2. Cài đặt PHP 7.4

Sau khi đã đảm bảo việc keep-up-to-date thì tiến hành cài đặt:

apt install php7.4-fpm

Các “thứ” sẽ được cài đặt bao gồm:

php7.4-cli 
php7.4-common 
php7.4-fpm 
php7.4-json 
php7.4-opcache 
php7.4-readline

Các thứ nên/cần được cài thêm:

apt install \ 
php7.4-bcmath \ 
php7.4-bz2 \ 
php7.4-curl \ 
php7.4-gd \ 
php7.4-intl \ 
php7.4-mbstring \ 
php7.4-mysql \ 
php7.4-xml \ 
php7.4-zip

3. Cấu hình PHP 7.4

Thay đổi một số thiết lập trong file php.ini:

# Open file to edit
vi /etc/php/7.4/fpm/php.ini

Vài thiết lập cần/nên được sửa đổi để phù hợp với nhu cầu:

max_execution_time = 300 
max_input_time = 600
memory_limit = 256M 
post_max_size = 100M 
cgi.fix_pathinfo = 0 
upload_max_filesize = 100M

Sau đó, khởi động lại service PHP 7.4 FPM:

service php7.4-fpm restart

4. Cấu hình NGINX chạy PHP 7.4

Tạo file cấu hình chạy PHP cho NGINX:

# Open file to edit
vi /etc/nginx/php.conf

Dưới đây là nội dung file cấu hình tham khảo:

location / { 
    try_files $uri $uri/ /index.php?$query_string; 
} 

location ~ [^/]\.php(/|$) { 
    fastcgi_split_path_info ^(.+?\.php)(/.*)$; 
    if (!-f $document_root$fastcgi_script_name) { 
        return 404; 
    } 
    fastcgi_pass unix:/run/php/php7.4-fpm.sock;
    fastcgi_index index.php; 
    fastcgi_param HTTP_PROXY ""; 
    include fastcgi.conf; 
}

File này có thể được tái sử dụng nhiều lần khi thiết lập các website chạy trên NGINX. Ví dụ về thiết lập cho subdomain dev.linhntaim.com sử dụng mã nguồn WordPress có sử dụng thông tin cấu hình từ php.conf:

server { 
    listen 443 ssl http2; 
    listen [::]:443 ssl http2; 

    server_name dev.linhntaim.com; 

    include ssl.conf; 
    
    index index.php index.html; 
    
    ... 

    include php.conf; # PHP configuration
}

Sau khi đã tạo/chỉnh sửa các file cấu hình, thực hiện kiểm tra và khởi động lại service NGINX:

nginx -t 
service nginx restart

Hoàn tất.

5. Vài điều cần lưu ý sau khi cài đặt PHP 7.4

PHP 7.4 đã hỗ trợ và mặc định khi kết nối với CSDL MySQL 8.0 sẽ sử dụng tài khoản có password được mã hóa theo caching_sha2_password. Do vậy, khi tạo tài khoản trên CSDL MySQL 8.0 để kết nối từ mã nguồn PHP 7.4, cần tạo với câu lệnh sau:

-- A very clearly way to create an account with password hashed by caching_sha2_password
create user 'php7.4'@'localhost' identified with caching_sha2_password by 'password'; 

-- Or because MySQL 8.0 has caching_sha2_password as default method, we simply excute this 
create user 'php7.4'@'localhost' identified by 'password';

Đối với những tài khoản đã tạo với password được mã hóa theo mysql_native_password hoặc phương thức khác, để cập nhật password với phương thức mới, dùng câu lệnh sau:

alter user 'php7.4'@'localhost' identified with caching_sha2_password by 'password';

Khi cảm thấy ổn với phiên bản PHP 7.4 sau một thời gian sử dụng, tiến hành xóa bỏ các phiên bản cũ hơn nếu không cần dùng nữa:

apt remove \ 
php7.3-fpm \ 
php7.3-common \ 
php7.3-bcmath \ 
php7.3-bz2 \ 
php7.3-curl \ 
php7.3-gd \ 
php7.3-intl \ 
php7.3-mbstring \ 
php7.3-mysql \ 
php7.3-xml \ 
php7.3-zip 

apt autoremove --purge 

rm -r /etc/php/7.3