Skip to main content
  1. Researches/

Unsafe SQL-Escaping in WordPress

·1919 words·10 mins
Fariq Fadillah Gusti Insani
php wordpress cve
Table of Contents

tl;dr ini bukan membahas spesifik kerentanan, tetapi menyangkut serangkaian kerentanan karena menggunakan fungsi escape dalam situasi dimana fungsi tersebut tidak boleh digunakan dan tulisan ini membahas mengenai fungsi escape di lingkungan Wordpress. Dan tulisan ini disusun ketika Wordpress berada di versi 6.4.2.

Intro #

WordPress merupakan sistem manajemen konten (CMS) berbasis PHP yang digunakan untuk membangun dan mengelola situs web. Dalam ekosistem WordPress, kita dapat menggunakan tema dan plugin yang dapat meningkatkan desain dan fungsionalitas situs web.

Untuk pengembangan tema dan plugin WordPress, interaksi dengan database merupakan bagian penting oleh karena itu WordPress menyediakan objek global WPDB yang merupakan instance dari class WPDB untuk berinteraksi dengan database, objek tersebut memiliki beberapa method yang bisa digunakan tetapi ada beberapa yang memerlukan filter terhadap input pengguna untuk mencegah kerentanan SQL Injection.

SQL Injection terjadi ketika input dari pengguna tidak difilter dengan baik untuk karakter escape, dan kemudian input tersebut digunakan dalam query SQL. Hal ini dapat mengakibatkan manipulasi query yang dijalankan database pada aplikasi, yang salah satu dampaknya dapat mengakibatkan Data Breach karena attacker bisa melakukan view dan dumping database. Oleh karena itu, lakukan filter terhadap input pengguna untuk mencegah serangan SQL Injection.

Latar Belakang #

Ada beberapa method dari class wpdb yang membutuhkan filter terhadap input pengguna, yaitu

  • $wpdb->query() , menjalankan query SQL, menggunakan koneksi database saat ini.
  • $wpdb->get_var(), melakukan query SQL dan mengembalikan satu nilai dari database.
  • $wpdb->get_row(), melakukan query SQL dan mengembalikan satu baris dari database.
  • $wpdb->get_col(), melakukan query SQL dan mengembalikan satu kolom dari database.
  • $wpdb->get_results(), melakukan query SQL dan mengembalikan kumpulan hasil SQL dari database.
  • $wpdb->replace(), menggantikan baris dalam tabel atau menyisipkannya jika tidak ada, berdasarkan PRIMARY KEY atau indeks UNIK.

SQL-Escaping diperlukan untuk menghindari terjadinya kerentanan yang memungkinkan attacker melakukan kontrol terhadap query SQL pada sistem atau yang sering disebut SQL Injection. Pada wordpress, ada function dan method yang berfungsi untuk escaping data, tetapi penerapan yang salah tidak akan membantu dalam mengamankan sistem dari serangan SQL injection.

The Vital Role of wpdb->prepare #

$wpdb->prepare() merupakan method yang disediakan class WPDB, dimana method ini mengikuti prinsip prepared statements sehingga akan mempersiapkan query SQL untuk eksekusi yang lebih aman.

https://github.com/WordPress/wordpress-develop/blob/6.4/src/wp-includes/class-wpdb.php#L1457-L1759

Pertama, fungsi akan memeriksa apakah query yang diberikan memiliki tanda % yang menunjukkan adanya placeholder. Fungsi ini mendukung berbagai jenis format placeholder seperti %s, %d, dan %f yang digunakan untuk string, integer, dan float, serta format khusus seperti %1$s, %5s yang memungkinkan penggunaan beberapa placeholder dalam satu query.

Setelah itu akan dilakukan pemisahan argumen dan dilakukan escape menggunakan fungsi _real_escape yang mana, didalam fungsi tersebut menggunakan mysqli_real_escape_string https://github.com/WordPress/wordpress-develop/blob/6.4/src/wp-includes/class-wpdb.php#L1277-L1295

Menurut dokumentasi, fungsi mysqli_real_escape_string akan melakukan escape pada karakter NUL (ASCII 0), \n, \r, , ‘, “, and CTRL+Z. https://www.php.net/manual/en/mysqli.real-escape-string.php

Setelah nilai-nilai yang dimasukkan ke dalam placeholder sudah di-escape maka query yang telah diproses akan dieksekusi.

Penting untuk diingat bahwa menggunakan wpdb::prepare adalah cara yang baik untuk memastikan keamanan query Anda, namun penggunaan yang tepat dan pemahaman tentang bagaimana fungsinya bekerja adalah kunci. Selalu pastikan untuk mengikuti praktik terbaik keamanan saat bekerja dengan database.

Safe Escaping method #

Method yang sudah menerapkan escaping data (menurut WordPress):

  • $wpdb->insert(), menyisipkan baris dalam tabel.
  • $wpdb->update(), memperbarui baris dalam tabel.
  • $wpdb->delete(), menghapus baris dalam tabel.

Untuk membuktikannya, bisa dilakukan tracing source code dari salah satu method yaitu contohnya WPDB->insert. https://github.com/WordPress/WordPress/blob/master/wp-includes/class-wpdb.php#L2502-L2504

Fungsi insert memiliki 3 argumen yaitu table, data dan format, yang selanjutnya akan dijalankan fungsi _insert_replace_helper https://github.com/WordPress/WordPress/blob/master/wp-includes/class-wpdb.php#L2584-L2615

Fungsi helper ini akan mempersiapkan query SQL untuk eksekusi yang lebih aman menggunakan WPDB->prepare sehingga tidak perlu lagi melakukan escape data tersendiri.

Sebagai contoh user mencoba melakukan manipulasi query SQL pada display_name dengan nilai tester ’ AND 1=3 – -

<?php
global $wpdb;    


$wpdb->insert(
    "wp_users",
    array(
        "user_pass" => "dummypassword",
        "user_nicename" => "tester",
        "user_email" => "tester@tester.tester",
        "user_activation_key" => "2023-12-26 13:49:35",
        "display_name" => "tester ' AND 1=3 -- -",
        "user_login" => "tester1"
    )
);

Tetapi untuk manipulasi ini tidak bekerja pada method WPDB->insert karena otomatis dilakukan escape, sehingga query SQL-nya menjadi seperti berikut

INSERT INTO `wp_users` (`user_pass`, `user_nicename`, `user_email`, `user_activation_key`, `display_name`, `user_login`) VALUES ('dummypassword', 'tester', 'tester@tester.tester', '2023-12-26 13:49:35', 'tester \' AND 1=3 -- -', 'tester1')

Jadi untuk method wpdb::insert wpdb::update wpdb::delete sudah menggunakan wpdb::prepare didalamnya, sehingga query akan dipersiapkan untuk di-escape agar bisa dieksekusi dengan aman.

Unsafe Escaping method (in some case) #

  1. esc_sql()

Fungsi ini hanya akan melakukan escape pada value yang berada pada dalam quotes di SQL. Karakter yang akan dilakukan escape diantaranya single quotes, double quotes, backslash dan sejak versi 4.8.3 karakter percent akan diubah menjadi string placeholder.

Bisa kita tracing kodenya pada file berikut https://github.com/WordPress/wordpress-develop/blob/6.4/src/wp-includes/formatting.php#L4465-L4468

https://github.com/WordPress/wordpress-develop/blob/6.4/src/wp-includes/class-wpdb.php#L1307-L1321

Tracing source code untuk fungsi esc_sql pada WordPress dimana di dalamnya memanggil method WPDB->_escape, selanjutnya dieksekusi fungsi _real_escape untuk melakukan escape data

https://github.com/WordPress/wordpress-develop/blob/6.4/src/wp-includes/class-wpdb.php#L1277-L1295

yang mana ternyata _real_escape dalam melakukan escape data menggunakan fungsi mysqli_real_escape_string jika ada sebuah koneksi database, sedangkan jika tidak ada koneksi database akan menggunakan fungsi addslashes.

Pada dokumentasi PHP, fungsi mysqli_real_escape_string akan melakukan escape pada karakter NUL (ASCII 0), \n, \r, , ‘, “, and CTRL+Z. Sedangkan untuk fungsi addslashes akan melakukan escape pada karakter NUL (ASCII 0), , ‘, "

mysqli_real_escape_string

<?php
$mysqli = new mysqli("localhost", "root", "", "wordpress");


$input = ['"', "'", ";", '%', '_', ">", "<", "/", '\\', '(', ')', '!', '?', '=', '+', '-', '*'];


foreach ($input as $char) {
    echo $char . ' => ' . mysqli_real_escape_string($mysqli, $char) . PHP_EOL;
}

Output:

" => \"
' => \'
; => ;
% => %
_ => _
> => >
< => <
/ => /
\ => \\
( => (
) => )
! => !
? => ?
= => =
+ => +
- => -
* => *

addslashes

<?php


$input = ['"', "'", ";", '%', '_', ">", "<", "/", '\\', '(', ')', '!', '?', '=', '+', '-', '*'];


foreach ($input as $char) {
    echo $char . ' => ' . addslashes($char) . PHP_EOL;
}

Output:

" => \"
' => \'
; => ;
% => %
_ => _
> => >
< => <
/ => /
\ => \\
( => (
) => )
! => !
? => ?
= => =
+ => +
- => -
* => *

Lalu diakhir, fungsi add_placeholder_escape yang akan dieksekusi untuk mengubah karakter percent menjadi string placeholder

https://github.com/WordPress/wordpress-develop/blob/6.4/src/wp-includes/class-wpdb.php#L2440-L2445

https://github.com/WordPress/wordpress-develop/blob/6.4/src/wp-includes/class-wpdb.php#L2409-L2430

Untuk memastikan karakter apa saja yang akan dilakukan encoding pada fungsi esc_sql, bisa menggunakan kode berikut:

<?php
global $wpdb;


$input = ['"', "'", ";", '%', ">", "<", "/", '\\', '(', ')', '!', '?', '=', '+', '-', '*'];


foreach ($input as $char) {
    echo $char . ' => ' . esc_sql($char);
}

Berikut contoh penerapan esc_sql() yang rentan terhadap celah sql injection:

<?php
global $wpdb;


$user_input = $_GET['order'];
$posts = $wpdb->get_results("SELECT post_title FROM $wpdb->posts ORDER BY " . esc_sql($user_input));

Penggunaan esc_sql pada kode diatas tidak tepat, karena fungsi esc_sql() hanya akan melakukan escape pada value yang ada didalam quotes, sehingga memungkinkan user untuk melakukan injeksi query SQL dengan cara seperti berikut.

$user_input = "post_title,(SELECT 1 FROM (SELECT SLEEP(25))A)";
$posts = $wpdb->get_results("SELECT post_title FROM $wpdb->posts ORDER BY " . esc_sql($user_input));

Dengan cara diatas, user melakukan injeksi SQL dengan tipe Time-based Blind yang ditandai dengan jeda waktu yang ditentukan user yaitu ‘25 detik’ untuk kasus diatas.

SELECT post_title FROM wp_posts ORDER BY post_title,(SELECT 1 FROM (SELECT SLEEP(25))A)

Pada wordpress ada method prepared statement $wpdb->prepare() untuk eksekusi query agar lebih aman, namun pada kasus diatas mitigasinya tidak memungkinkan menggunakan method $wpdb->prepare() saja, kita juga harus melakukan whitelisting string seperti berikut:

<?php
global $wpdb;


$allowed_columns = array('id', 'post_date');
$user_input = isset($_GET['order']) ? $_GET['order'] : '';


$query = "SELECT post_title FROM $wpdb->posts";
if (in_array($user_input, $allowed_columns)) {
    $query .= $wpdb->prepare(" ORDER BY %i", esc_sql($user_input));
}


$posts = $wpdb->get_results($query);
  1. esc_like()

Fungsi ini hanya akan melakukan escape pada special characters yang digunakan pada LIKE statements. Karakter yang akan dilakukan escape diantaranya percent, underscore dan backslash. Bisa kita tracing kodenya pada file berikut

https://github.com/WordPress/wordpress-develop/blob/6.4/src/wp-includes/class-wpdb.php#L1784-L1786

Tracing source code untuk fungsi esc_like pada WordPress dimana di dalamnya memanggil fungsi addcslashes, yang mana menurut dokumentasi PHP fungsi tersebut akan mengembalikan string dengan awalan backslashes pada karakter yang tercantum dalam parameter ke-2.

Berarti ketika ada inputan berupa karakter underscore, percent, dan backslash akan dilakukan escape. Kita bisa memastikannya dengan kode berikut

<?php


$input = ['"', "'", ";", '%', '_', ">", "<", "/", '\\', '(', ')', '!', '?', '=', '+', '-', '*'];


foreach ($input as $char) {
    echo $char . ' => ' . addcslashes($char, '_%\\') . PHP_EOL;
}

Output:

" => "
' => '
; => ;
% => \%
_ => \_
> => >
< => <
/ => /
\ => \\
( => (
) => )
! => !
? => ?
= => =
+ => +
- => -
* => *

Atau bisa juga langsung menggunakan method esc_like dengan kode berikut:

<?php
global $wpdb;


$input = ['"', "'", ";", '%', '_', ">", "<", "/", '\\', '(', ')', '!', '?', '=', '+', '-', '*'];


foreach ($input as $char) {
    echo $char . ' => ' . $wpdb->esc_like($char);
}

Berikut contoh penerapan esc_like() yang rentan terhadap celah sql injection:

<?php
global $wpdb;
 
$user_input = $_GET['search'];
$posts = $wpdb->get_results("SELECT post_title FROM $wpdb->posts WHERE post_content LIKE '%" . $wpdb->esc_like($user_input) . "'");

Karena fungsi esc_like() hanya akan melakukan escape pada special characters yang digunakan pada LIKE statements, maka kode diatas memungkinkan user untuk melakukan injeksi query SQL dengan menambahkan quotes, contohnya seperti berikut: Normal query:

Injeksi query:

$user_input = "abdxyz' OR 1=1;-- -";
$posts = $wpdb->get_results("SELECT post_title FROM $wpdb->posts WHERE post_content LIKE '%" . $wpdb->esc_like($user_input) . "'");

Dengan cara diatas user melakukan injeksi SQL dengan tipe Boolean-based Blind yang ditandai dengan perubahan konten yang ada.

SELECT post_title FROM wp_posts WHERE post_content LIKE '%abcdxyz' OR 1=1;-- -'

Untuk mitigasinya kita bisa menambahkan fungsi esc_sql() setelah esc_like() digunakan, dan bisa menggunakan method prepared statement $wpdb->prepare() untuk eksekusi query agar lebih aman seperti berikut:

<?php
global $wpdb;


$user_input = isset($_GET['search']) ? $_GET['search'] : '';


$escaped_keyword = esc_sql( $wpdb->esc_like( $user_input ) );
$like_pattern = '%' . $escaped_keyword;


$query = $wpdb->prepare("SELECT post_title FROM $wpdb->posts WHERE post_content LIKE %s", $like_pattern);


$posts = $wpdb->get_results($query);

Studi Kasus CVE-2023-47530 #

Referensi: https://patchstack.com/database/vulnerability/redirect-404-error-page-to-homepage-or-custom-page/wordpress-redirect-404-error-page-to-homepage-or-custom-page-with-logs-plugin-1-8-7-sql-injection-vulnerability

ParameterValue
SoftwareRedirect 404 Error Page to Homepage or Custom Page with Logs
TypeWordPress Plugin
Plugin URLhttps://wordpress.org/plugins/redirect-404-error-page-to-homepage-or-custom-page/
Active installations20,000+
Vulnerable version<= 1.8.7
Fixed in1.8.8
ClassificationSQL Injection
Required privilegeAdministrator

Celah SQL Injection pada Plugin Redirect 404 Error Page to Homepage or Custom Page with Logs, dimana kode yang rentan terhadap SQL Injection ada pada file class-list-table-404-log.php

Method $wpdb->get_results() membutuhkan filter input dari pengguna, tetapi disini developer menggunakan fungsi esc_sql() pada ORDER BY statement. Penggunakan fungsi tersebut tidak tepat karena fungsi esc_sql() tidak melakukan escape pada unquoted numeric, field name ataupun SQL keywords. Karena user dapat melakukan kontrol pada $_REQUEST['orderby'] dan $_REQUEST['order'] maka untuk melakukan injeksi query bisa menggunakan payload berikut

,(SELECT 1 FROM (SELECT SLEEP(25))A)

Dengan jeda 25 detik yang diberikan, maka user bisa melakukan verifikasi bahwa plugin tersebut rentan terhadap celah SQL Injection.

Meskipun celah SQL Injection tersebut membutuhkan hak akses Administrator, penting untuk developer menerapkan fungsi yang tepat agar memperkuat plugin terhadap potensi ancaman security. Mitigasi SQL Injection (CVE-2023-47530) pada Plugin Redirect 404 Error Page to Homepage or Custom Page with Logs

Developer melakukan mitigasi pada versi 1.8.8 dengan cara melakukan whitelisting string terhadap input $_REQUEST['orderby'] dan $_REQUEST['order']

Kesimpulan

  • $wpdb->prepare() memiliki peran penting dalam menyiapkan dan menjalankan query SQL
  • esc_sql() tidak melakukan escape pada unquoted numeric values, field name ataupun SQL keywords
  • esc_like() hanya akan melakukan escape pada special characters yang digunakan pada LIKE statements.
  • Jika Anda pengguna plugin Redirect 404 Error Page to Homepage or Custom Page with Logs versi <= 1.8.7 , segera lakukan update ke versi terbaru.