PHP Secure Coding Guide 101
Table of Contents
PHP Secure Coding Guide ini dikembangkan agar mempercepat programmer PHP untuk mengamankan aplikasi yang sedang dikembangkan, tulisan ini dikembangkan saat PHP berada pada versi mayor 8.2 lalu, secara spesifik tulisan ini dibuat dengan contoh aplikasinya adalah Native, CodeIgniter4 dan Laravel versi terbaru. Namun, banyak contoh dari tulisan ini terutama pada bagian Native dapat diimplementasikan di frameworks yang lainnya, dikarenakan API Function Internal PHP Native dari PHP harusnya masih tersedia di semua frameworks yang dibangun dengan PHP itu sendiri.
PHP Versioning #
Memilih versi PHP dengan alasan security pastinya harus merujuk pada tabel yang tertera di atas, tabel di atas menunjukan informasi terkait versi mayor.minor yang masih mendapatkan dukungan dari PHP baik dari dukungan secara perubahan revisi ataupun dukungan untuk permasalahan security. Saat tulisan ini dibuat (22 Juni 2023), maka versi yang sifatnya “masih” dapat digunakan adalah versi mayor dengan identitas mulai dari 8.0 sampai dengan versi terbaru dari PHP dari tanggal dibuatnya tulisan ini adalah 8.2 (latest). Relevansi tulisan ini harusnya masih berlanjut sampai “25 November 2024” atau bahkan “8 Desember 2025” diambil dari versi sebelum versi latest yaitu 8.1, keputusan untuk memperbarui versi PHP ke mayor latest akan memiliki banyak keuntungan yang didapatkan selain dari sisi security pastinya.
Keputusan perpindahan versi misalnya dari mayor 5.6 ke 7.0 ataupun dari 7.4 ke 8.0 akan memiliki banyak sekali perubahan namun sejauh ini hal yang signifikan berubah dari 5.x ke 7.0 adalah soal syntax terutama di bagian konektivitas dari PHP ke database terutama msql_* ke mysqli_* namun hal yang umum berubah dari 5.x ke 8.x dari sisi penulisan syntax yang berpengaruh pada security adalah bagaimana cara PHP 8.0 melakukan “String to Number Comparison” singkatnya mulai di PHP 8.0 membandingkan sebuah “String” dan “Number” akan cukup “ketat” seperti contohnya pada tangkapan layar di bawah ini dan daftar rangkuman yang ada di tabel
Perbandingan | < 8.0 | ≥ 8.0 |
---|---|---|
0 == “0” | True | True |
1 == “1.0” | True | True |
0 == “any_string_alived” | True | False |
0 == “” | True | False |
1 == “ 1” | True | True |
1337 == “1337boo” | True | False |
Proses perbandingan di atas juga akan berpengaruh pada proses developtment yang berkaitan dengan security dikarenakan ada beberapa juga well-known isu yang berkaitan dengan fitur bawaan dari PHP yaitu Type Juggling
Contoh yang sangat mungkin terjadi adalah ketika ada hal-hal yang berurusan dengan validasi sebuah data contoh kasus yang mungkin saja terjadi adalah ketika ada sebuah fitur security yaitu authentikasi seperti contoh kode di bawah ini
<?php
include "secret.php";
$json = file_get_contents('php://input');
$obj = json_decode($json,true);
if($obj['username'] == $username && $obj['password'] == $password){
echo json_encode(['status' => 'success auth']);
}else{
echo json_encode(['status' => 'error auth']);
}
Meskipun bukan contoh yang riil digunakan, tapi di contoh tersebut bisa saja diimplementasikan dan bukan tidak mungkin pernah diimplementasikan, dikarenakan secara logika aturan-aturan tersebut terpenuhi dan “lazim” sebagai methode untuk authentikasi. Tapi juga tidak menutup kemungkinan kode tersebut sedang digunakan, lalu apa yang salah dan apa yang kemungkinan menjadi resiko security? benar komparasi yang tidak “ketat” pada versi PHP sebelum 8.0, jika merujuk pada tabel komparasi yang ada di atas maka cara untuk bisa terauthentikasi adalah dengan memasukan nilai int(0), dikarenakan di versi sebelum PHP 8.0 semua data yang dibandingkan (==) dengan int(0) nilainnya akan True, sebagai pembuktian berikut ini tangkapan layar penggunaan versi sebelum 8.0 dan 8.2 dengan untuk mengeksploitasi kode di atas
Bisa terlihat pada tangkapan layar bahwa versi 8.2 memberikan output yang berbeda daripada versi sebelum 8.0, penjelasannya adalah input JSON mengijinkan tipe data yang didefinisikan oleh user tetap terbawa ke dalam proses, yang mana dikarenakan nilai int(0) akan selalu dinilai sebagai True jika dibandingkan dengan apapun di versi PHP sebelum 8.0 di situlah letak security isu terjadi di versi PHP sebelum 8.0 dan sudah dapat diselesaikan oleh 8.X, lalu jika tetap ingin menggunakan versi yang sudah usang (end of life and support) apa solusinya? solusinya adalah menggunakan type data comparasion di PHP (===)
Dengan membandingkan juga tipe data (===)[strict comparison] permasalahan kode di atas akan dapat diselesaikan, namun hal ini (menuliskan syntax ===) akan juga cukup menjadi PR bagi programmer terutama jika menggunakan bahasa pemrogramman lain dalam kesehariannya (python,golang), dikarenakan umumnya di bahasa pemrogramman lain (===) tidak lazim ada penulisan syntax tersebut.
Melakukan pembaruan dari versi yang sudah (end of support / life) juga akan mengurangi resiko terkena vulnerability lainnya bisa dilihat pada table milik CVEdetails
Salah satu isu yang ada di versi yang tetap memiliki support adalah di bawah versi 8.1.7 isunya terdokumentasi dengan nomor CVE-2022-31626 meskipun tidak ada public exploit yang dibagikan namun bisa dipahami dari deskripsi dari nomor CVE tersebut bahwa salah satu cara untuk melakukan exploitasi-nya adalah dengan mengontrol salah satu ekstensi yaitu pdo_mysql, cara mengeksploitasinya memang sangat-sangat susah tapi ini tetap juga menjadi resiko security, bagaimana terhindar dari resiko ini? jawabannya tetap melakukan pembaruan ke versi yang lebih tinggi dari 8.1.7 bisa naik ke 8.1.8-15
Itulah salah satu dari banyaknya manfaat di sisi security yang ada jika kita melakukan pembaruan versi ke 8.0 / latest, panduan migrasi dari versi major dari 4 sampai ke 8.0 PHP sudah mendokumentasikannya dengan sangat rapi dan dapat diikuti panduannya di tautan berikut
PHP Security Configuration #
Salah satu faktor yang harus diperhatikan dalam membangun sebuah aplikasi dengan menggunakan bahas pemrogramman PHP adalah terkait konfigurasi di sisi PHP itu sendiri, banyak konfigurasi yang kurang benar menyebabkan kerentanan terjadi yang membuat resiko baru bukan hanya ke sisi aplikasi, juga menyebabkan resiko yang langsung ke arah mesin tempat PHP itu berjalan.
Menggunakan versi mayor yang latest di PHP sejatinya sudah memiliki konfigurasi bawaan yang direkomendasikan oleh PHP, tapi dikarenakan pasti adanya proses trial & error pada proses pengembangan aplikasi, berikut ini adalah beberapa rekomendasi konfigurasi security untuk PHP
Melakukan konfigurasi di PHP adalah dengan merubah file php.ini
file ini biasanya berlokasi di /etc/php/mayor.minor/{platform}
lokasinya mungkin bisa berbeda-beda namun umumnya akan berlokasi pada folder tersebut, isi dari {platform} biasanya ada beberapa hal seperti [cli,cgi,apache2,fpm] tergantung mode mana yang ingin disesuaikan
Error Handling #
Salah satu isu security bisa menjadi sebuah resiko adalah di saat sebuah aplikasi PHP mengeluarkan sebuah error yang sangat spesifik terkait gagalnya sebuah proses yang ada di dalam PHP, mengapa error yang dikeluarkan ke user bisa menjadi resiko, selain di sisi bisnis yang kurang baik juga di mata para penyerang sebuah error bisa membawa informasi internal terkait bagian kode mana yang bermasalah, versi berapa yang digunakan oleh server hingga path di-install-nya aplikasi PHP itu sendiri, mengapa bisa hal itu menjadi resiko? karena sekecil apapun informasi di hadapan para penyerang adalah bagian dari attack scenario yang dapat dibangun, rekomendasi dari melakukan error handling adalah dengan TIDAK mengerluarkan informasi apapun ke user dan tetap melakukan Logging di sisi Server
Error Handling Configuration on Development #
php.ini
display_errors = On
display_startup_errors = On
error_reporting = -1
log_errors = On
Menginisiasi nilai -1
pada variable error_reporting
maksudnya adalah mengeluarkan semua isi error dari PHP, ada beberapa kali perubahan isi dari error_reporting
yang dapat dirangkum pada tabel di bawah ini
Versi PHP | Error Value |
---|---|
< 5.3 | -1 or E_ALL |
5.3 | -1 or E_ALL |
> 5.3 | -1 or E_ALL |
Konfigurasi di atas hanya boleh dilakukan saat melakukan development dikarenakan PHP akan mengeluarkan semua error dari apapun yang terjadi.
Error Handling Configuration on Production #
php.ini
expose_php = Off
error_reporting = E_ALL
display_errors = Off
display_startup_errors = Off
log_errors = On
error_log = /var/logs/php/php_error.log
ignore_repeated_errors = Off
Hal yang harus diperhatikan adalah display_errors
variable tersebut yang bertanggung jawab menginformasikan error ke user dan harus off
saat berada di production, lalu di mana programmer dapat melakukan tracking error dengan mendefinisikan path storing error di variable error_log
Error Control Operators #
PHP memiliki fitur khusus untuk melakukan “silenting the errors” fitur global dengan operator sign (@) ini memungkinkan error bisa tidak muncul ke para user, namun ada beberapa resiko yang bisa terjadi oleh penggunaan sign operators tersebut, salah satu contohnya adalah seperti pada kode di bawah ini
<?php
$json = file_get_contents('php://input');
$json = '{"username": 0, "password": 0}';
$obj = json_decode($json,true);
if(@$data['username'] === 'admin' && @$data['password'] === 'admin'){
echo json_encode(['status' => 'success auth']);
}else{
echo json_encode(['status' => 'error auth']);
}
Pada kode di atas mungkin terlihat normal, tapi dengan penggunaan sign operator ada sebuah kebimbangan di mana programmer tidak tau error apa yang sedang terjadi, kode yang bermasalah ada di bagian
@$data['username'] === 'admin' && @$data['password'] === 'admin'
Penggunaan @$data['username']
dan @$data['password']
membuat programmer tidak mengetahui yang sedang error adalah $data
yang tidak ada atau index username
atau password
yang tidak ada di dalam array $data
menggunakan operator sign sebaiknya dihindari jika memang urgensinya bisa diganti dengan validasi di PHP itu sendiri.
isset($data['username'], $data['password'])
General Configuration #
Ada beberapa konfigurasi umum yang dapat dilakukan di php.ini
salah satunya berikut ini konfigurasi yang secara umum dapat diterapkan sebagai bagian dari penguatan security
General #
open_basedir = /isi/dengan/path/yang/ditentukan/
allow_url_fopen = Off
allow_url_include = Off
Variable open_basedir
membuat PHP hanya dapat melakukan operasi CRUD pada area kerja yang didefinisikan di open_basedir
, hal ini akan membantu menurunkan resiko yang ada jika ada celah yang memungkinkan membaca isi file di dalam server.
Berikut ini contoh sebuah script sederhana yang mensimulasikan terjadinya sebuah celah yang memungkinkan untuk melakukan Local File Read dengan target file berupa /etc/passwd
Di saat variable open_basedir
tidak diinisiasikan, maka ruang kerja PHP menjadi di seluruh sistem, namun jika kita menginisiasikan sebuah direktory kerja di variable tersebut, maka PHP akan mulai terbatas ruang kerjanya yang mana hal ini bisa menurunkan resiko dari celah Local File Read, seperti dicontohkan pada tangkapan layar di bawah ini
Variable allow_url_*
diinisiasikan menjadi Off adalah tindakan preventif untuk mencegah eskalasi lebih tinggi dari celah LFI (Local File Inclusion) ke RFI (Remote File Inclusion), secara default konfigurasi dari PHP untuk allow_url_include
memang sudah Off, namun perlu dipastikan kembali bahwa konfigurasinya memang sudah sesuai, menurut dokumentasi konfigurasi PHP,
Variable allow_url_include
sudah Deprecated
sejak Major 7.4.0, lalu variable allow_url_fopen
secara default dari PHP masih 1
, dengan ini akan timbul sebuah resiko jika terjadi sebuah celah yang memungkinkan para penyerang melakukan membuka file dari URL dan resiko yang lainnya.
Session Management #
Session management di PHP secara default harusnya memang sudah sesuai dengan standart keamanan yang diterapkan oleh PHP, namun ada beberapa konfigurasi tambahan yang bisa diterapkan pada php.ini
untuk mengoptimalisasi konfigurasi session di PHP, hal-hal dapat diperhatikan bisa dirangkum seperti berikut ini
session.auto_start = Off
session.use_trans_sid = 0
session.cookie_domain = seclab.id
session.save_path = /non/guessable/path/
session.use_strict_mode = 1
session.use_cookies = 1
session.use_only_cookies = 1
session.cookie_lifetime = 14400
session.cookie_secure = 1
session.cookie_httponly = 1
session.cookie_samesite = Strict
session.cache_expire = 30
session.sid_length = 100
Variable session.auto_start
harus diinisiasikan menjadi Off
agar di sisi PHP masih harus mendefinisikan session_start
, hal lainnya yang harus diperhatikan adalah life time dari Session itu sendiri, seberapa lama lifetime session tergantung kesepakatan dan bisnis proses dari aplikasi itu sendiri, tapi yang pasti lifetime session TIDAK BOLEH selama 1 hari penuh, hitungan jam seperti 1-4 jam adalah hal yang logis dan wajar untuk lifetime sebuah session. Lalu konfigurasi yang lainnya adalah cookie only accessible via http
konfigurasi ini adalah salah satu cara untuk mengurangi dampak resiko yang terjadi akibat serangan XSS, banyak objektif yang bisa dicapai dalam serangan XSS namun kebanyakan adalah melakukan pencurian cookies browser, saat cookies di-set dengan http-only maka javascript tidak akan bisa melakukan akses ke cookies.
session_start(['cookie_lifetime' => 14400,'cookie_secure' => true,'cookie_httponly' => true]);
Sejak versi Mayor PHP 7.0 kita sudah dapat melakukan inisiasi dari session_start untuk opsi-opsi security, dengan contoh kode seperti di bawah ini
<?php
session_start();
$_SESSION['author'] = "Nikko Enggaliano";
$_SESSION['email'] = "nikko@seclab.id";
print_r($_SESSION);
Dan PHP dijalankan dengan cara php -S localhost:8888 index.php
maka akan mendapatkan luaran seperti tangkapan layar di bawah, hal yang menarik adalah browser dengan javascript dapat mengakses object document.cookie
, yang mana hal ini dikarenakan session_start tidak didefinisikan dengan security options yang sudah di bahas di atas.
Dengan merubah kode dengan opsi httpOnly pada syntax session_start untuk menambahkan security, maka dengan ini browser sudah tidak dapat mengakses object document.cookie
<?php
session_start(['cookie_lifetime' => 14400,'cookie_secure' => true,'cookie_httponly' => true]);
$_SESSION['author'] = "Nikko Enggaliano";
$_SESSION['email'] = "nikko@seclab.id";
print_r($_SESSION);
Pada tab application di bagian cookies terlihat bahwa cookies dari PHP sudah di-set menjadi HttpOnly
More Security Configuration Yet!! #
Ada hal lain yang dapat dilakukan oleh php.ini
sebagai file konfigurasi, yaitu menentukan batasan jumlah memory yang bekerja hingga melakukan penonaktifan fungsi yang ada di dalam PHP itu sendiri, penentuan batasan ini fungsi untuk menjaga seberapa besar memory yang dapat diakses oleh PHP serta dapat menjaga seberapa lama (dalam detik) sebuah script melakukan eksekusi program
memory_limit = 100M
post_max_size = 10M
max_execution_time = 60
enable_dl = Off
disable_functions = show_source,phpinfo,highlight_file
Ada beberapa tambahan variable yang dapat disesuaikan pada konfigurasi php.ini
namun hal ini tidak hanya berkaitan dengan security concern juga berkaitan dengan performance concern, kedua hal ini bisa saja sangat bertentangan bagaimana mengimplementasikan program tersebut, berikut ini beberapa penjelasan mengenai isi variable di atas beserta hal-hal yang menyertainya
memory_limit
melakukan inisiasi di variable ini memungkinkan program memiliki batasan untuk mengakses seberapa besar memory yang ada, namun pembatasan ini bisa berbanding terbalik dengan kebutuhan bisnis yang memang mengharuskan sebuah program memiliki resource yang sangat besar untuk menjalankan sebuah proses, namun tetap harus melakukan batasan memory 100M bisa juga terlalu besar ataupun terlalu kecil untuk sebuah program PHP tergantung dengan kompleksitas dan banyaknya akses ke aplikasi tersebut.post_max_size
pada variable ini fokus yang dibatasi adalah seberapa besar sebuah Http Body Request dapat menampung data, umumnya besarnya Body Request terkait dengan fitur unggah sebuah file, hal ini selain dibutuhkan validasi di sisi PHP juga perlu dikonfigurasi di sisiphp.ini m
, umumnya sebuah file yang diunggah berbentuk turunan dokumen [exel,docx,pptx,pdf,csv,png] tipe-tipe file tersebut jumlahnya akan dikalkulasi dengan POST form body atau POST JSON yang lainnya untuk dilimitasi padapost_max_size
max_execution_time
di variable ini lama waktu eksekusi sebuah script PHP akan ditentukan, perlu adanya pembatasan di sini tujuannya adalah agar tidak terlalu lama sebuah script melakukan sebuah operasi, pembatasan ini akan berbanding terbalik jika memang proses yang dibutuhkan untuk eksekusi sebuah script dibutuhkan waktu yang lama.enable_dl
variable ini memungkinkan script melakukan load pada sebuah ekstensi secara direct, variable ini jika dalam kondisi On akan menjadi sebuah ancaman yang baru, namun jika memang dibutuhkan mitigasi resiko harus dipertimbangkan lebih.disable_functions
variable yang bisa menjadi penyelamat juga bisa menjadi masalah terbaru bagi sebuah aplikasi, variable ini dapat diisi berbagi fungsi yang ingin dinonaktifkan keberadaannya, fungsi-fungsi yang biasanya dinonaktifkan meliputi fungsi-fungsi yang berhubungan dengan shell command, namun penonaktifan sebuah fungsi di dalamphp.ini
harus juga berbanding lurus apakah ada alternatif selain fungsi yang dinonaktifkan, acuan untuk melakukan nonaktif sebuah fungsi tidak semata-mata dengan alasan “tidak digunakan”, dalam framework tertentu fungsi yang mungkin dinonaktifkan tidak “terlihat terpakai”.
- Daftar rekomendasi
dangerous
function
phpinfo, show_source, highlight_file, system, exec, shell_exec, passthru, popen, proc_open, fopen_with_path, putenv
Web Application Security #
Bahasa pemrogramman PHP rata-rata dan umumnya digunakan untuk mengembangkan sebuah aplikasi berbasis web, pengembangan aplikasi berbasi web harus berbanding lurus dengan keamanan yang harus diterapkan pada aplikasi berbasis web ataupun aplikasi berbasis apapun, sebuah web aplikasi memiliki sebuah framework untuk melakukan pengecekan di sisi security yaitu OWASP (Open Web Application Security Project), sampai saat ini OWASP sudah mengeluarkan Top 10 dan versi terbarunya adalah OWASP Top 10 2021, detail perubahan bisa dilihat pada gambar di bawah ini
OWASP Top 10 selalu mempunyai kategori-kategori celah seperti Injection
, Broken Authentication
, Sensitive Data Exposure
dan banyak lainnya, pada tabel di atas ada perubahan besar yang terjadi di OWASP Top 2017 ke OWASP Top 2021, di OWASP 2021 ada penurunan tingkat kerentanan, pengelompokan kategori dan ada kategori baru yang masuk pada tahun 2021.
Broken Access Control #
Kerentanan Broken Access Control adalah kerentanan yang berfokus dari kurangnnya validasi pada Access Control dari sebuah session yang sudah terauthentikasi atau bahkan tidak adanya sebuah Access Control dari sebuah user yang tidak memiliki sebuah session, kerentanan ini memiliki fokus utama yaitu “User saya dapat melakukan aksi ke user yang lainnya”, “aksi” yang dimaksud bisa berupa “CRUD” (Create, Read, Update, Delete) Data dari user yang lainnya.
Kerentanan Broken Access Control (BAC) memiliki beberapa kategori umum yang menyertainya di antaranya
- No Authentication Needed
- Vertical Broken Access Control
- Horizontal Broken Access Control
Celah BAC kategori No Authentication Needed
intinya memungkinkan sebuah penyerang TANPA memiliki session apapun dapat melakuan CRUD pada sebuah data milik dirinya atau milik orang lainnya.
Celah berikutnya pada kategori BAC ada vertical dan horizontal broken access control tidak seperti no authentication di 2 kategori ini seorang user sudah terautentikasi, sudah mendapatkan sebuah session namun user ini dapat melakukan CRUD terhadap user yang lainnya dengan role yang sama atau se-level bagian ini disebut Horizontal Broken Access Control
lalu jika seorang user dapat melakukan CRUD terhadap sebuah akun yang memiliki role yang level-nya di atas role yang sedang terautentikasi, maka celah ini disebut Vertical Broken Access Control
.
No Authentication Needed
Example
Pada sebuah aplikasi, merujuk pada spesifikasi dan merujuk pada dokumen penggunaan aplikasi disebutkan bahwa untuk dapat mengakses semua endpoint dengan prefix /user/
haruslah telah terautentikasi, di bawah ini ada sebuah contoh HTTP Request yang mana sebuah user melakukan hit ke endpoint /user/dashboard
yang mana wajib terautentikasi namun user tersebut tidak memiliki session pun dapat melakukan hit dan mendapatkan response data yang valid.
GET /user/dashboard HTTP/1.1
Host: 1.1.1.1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/114.0
Accept: application/json, text/plain, */*
Accept-Language: id-ID
Accept-Encoding: gzip, deflate
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-origin
Insecure Direct Object Reference (IDOR) #
Insecure Direct Object Reference (IDOR) adalah salah satu kategori yang mencakup 2 kategori lainnya dari BAC yaitu Vertical & Horizontal BAC. Secara singkat jika ada sebuah input data yang menjadi point select sebuah user dan data tersebut dapat diganti sehingga memunculkan data milik orang lain maka IDOR telah terjadi, baik secara vertical maupun horizontal, salah satu contoh paling gampangnnya adalah
- Base example
https://url.com/profile/1
https://url.com/profile/?username=test
https://url.com/profile/balance/1
https://url.com/public/ktp/username.png
Inti serangan broken access control terutama IDOR di sini adalah bagaimana seorang penyerang dapat mengganti sebuah input yang dapat dikontrol dari user dengan tujuan untuk dapat melihat isi dari data user yang lain, sebagai contoh merujuk pada Base example sebuah serangan IDOR dapat terlaksana jika dengan mengganti input url seperti di bawah ini
- Exploiting
https://url.com/profile/2
https://url.com/profile/?username=test2
https://url.com/profile/balance/100
https://url.com/public/ktp/username123.png
Dari serangan yang hanya “menganti” input URL tersebut, simulasi kode yang dapat terjadi untuk menggambarkan celah di atas adalah sebagai berikut ini
<?php
$conn = new PDO("mysql:host=localhost;dbname=dvci4", "root", "");
$conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$sql = "SELECT * FROM users where `id` = ? ";
$q = $conn->prepare($sql);
$id = $_GET['id'];
$q->bindParam(1, $id);
$q->execute();
$data = $q->fetch(PDO::FETCH_ASSOC);
return json_encode($data);
Mengambil sebuah data user dengan melakukan select yang mana id berasal dari input GET dari user, hal ini membuat penyerang dapat sekedar menganti dan berharap menemukan id milik orang lain saja, solusi terbaiknya adalah dengan melakukan select yang mana data id mengambil dari session yang tidak dapat dikontrol oleh user tersebut.
Broken Access Control Preventive Strategy #
Melakukan preventive terhadap serangan BAC harus merujuk pada bagaimana sebuah bisnis dapat berjalan, harus berdasar pada dokumen bisnis atau sejenisnya yang mengatur bagaimana sebuah user X dapat mengakses fitur Y, cara terbaiknya adalah tetap melakukan access control terhadap session yang sedang terautentikasi, berikut ini beberapa cara untuk terhindar dari celah BAC
- PHP Native Manualy Session Checking
<?php
if(isset($_SESSION['roles']) && $_SESSION['roles'] == 'admin'){
//process
}else{
header("location: login.php");
exit;
}
Intinya pada PHP Native kita dapat melakukan session checking dan memastikan apakah roles tersebut memang sesuai dan layak untuk memprosess ke fitur tersebut, jika tidak sesuai maka tinggal di-redirect ke halaman yang sesuai dengan bisnis prosesnya, hal yang diingat dalam PHP Native adalah, syntax header
mengirimkan sebuah header response
dari server ke browser client, jika tidak diberikan exit;
maka jika seorang user menggunakan HTTP Client yang tidak memperdulikan sebuah header
response maka celah Broken Access Control dapat terjadi, celah ini memiliki sub dari BAC yaitu EAR (Execution After Redirect), menggunakan framework akan menurunkan resiko terjadinya EAR dikarenakan tiap-tiap framework sudah mengantisipasi serangan ini, dapat dicek pada referensi kode berikut
- https://github.com/codeigniter4/CodeIgniter4/blob/1e7204bbbacc2731327d79295bc32735f28b548f/system/HTTP/ResponseTrait.php#L503
- https://github.com/laravel/framework/blob/aef89589ea70e0081c139b06550220cc75f20ea6/src/Illuminate/Routing/Router.php#L250
- https://github.com/laravel/framework/blob/aef89589ea70e0081c139b06550220cc75f20ea6/src/Illuminate/Routing/Redirector.php
- Laravel Middleware
Route::group(['middleware' => ['isAdmin']], function () {
Route::prefix('admin')->group(function () {
Route::prefix('settings')->group(function () {
Route::get('/list-setting', [settingController::class, 'list_setting'])->name('admin.list-setting');
Route::get('/add-setting', [settingController::class, 'page_add_setting'])->name('admin.add-setting');
Route::post('/save-setting', [settingController::class, 'save_setting'])->name('admin.save-setting');
Route::get('/edit-setting/{id}', [settingController::class, 'page_edit_setting'])->name('admin.edit-setting');
Route::post('/update-setting/{id}', [settingController::class, 'edit_setting'])->name('admin.update-setting');
});
});
});
- CodeIgniter4
$routes->group('admin', ['filter' => 'AdminAuth'], function($routes){
$routes->get('dashboard', 'AdminController::SpvDashboard');
$routes->get('ask-setting', 'AdminController::listsetting');
$routes->get('list-setting', 'AdminController::listsettings');
$routes->get('process-setting/(:segment)', 'AdminController::process_setting/$1');
$routes->post('process-settings/(:segment)', 'AdminController::process_settings/$1');
$routes->get('list-setting', 'AdminController::listsetting');
});
Baik CodeIgniter4 maupun Laravel sama-sama memiliki fitur yang berfungsi untuk melakukan filtering terhadap sebuah session ataupun hal-hal lainnya, untuk laravel fiturnya disebut dengan Middleware dan untuk CodeIgniter disebut dengan filter, kedua hal tersebut bisa menjadi solusi yang praktis dan cepat untuk mengurangi resiko celah kerentanan yaitu broken access control. Namun perlu diingat bahwa menangani broken access control memerlukan pemahaman yang lebih terhadap sebuah bisnis dari sebuah aplikasi. Menangani broken access control juga tidak dapat dilakukan secara otomatis melainkan harus dilakukan satu-persatu filter terhadap endpoint sesuai dengan roles yang berlaku.
Identifier Strategy #
Memberi nama pada sebuah file juga harus berhati-hati dikarenakan jika memberi nama sebuah file dengan cara berurutan atau melakukan hashing yang berurutan maka akan ada resiko penyerang dapat menebak nama file milik orang lain. Maka dari itu berikut ini adalah strategi untuk menamai sebuah file agar mengurangi sebuah resiko terjadinya IDOR terhadap file milik orang lain.
- UUID4
- hashing+cutting+random+hashing
- Menggunakan UUID4 untuk melakukan naming sebuah file ataupun memberi ID terhadap sebuah user adalah cara yang cukup efektif untuk mengurangi resiko terjadinya kerentanan IDOR, namun hal terbaiknya tetap melakukan session checking “this user belong this data?”
<?php
require 'vendor/autoload.php';
use Ramsey\Uuid\Uuid;
use Ramsey\Uuid\Guid\Guid;
$uuid4 = Uuid::uuid4();
$uuidString = $uuid4->toString();
echo $uuidString;
?>
Identifier yang ada akan berbentuk seperti berikut ini
550e8400-e29b-41d4-a716-446655440000
98b154ed-8549-41a2-9c43-daf5e4e5569e
5c33fffe-787a-43a6-a6e2-4d5699c39d79
c58305fc-3a14-4b7e-9a1b-2b8c81a54111
e1125d9e-ebb9-4e62-94d4-7cc3f1e2b392
b16b0710-8114-42b4-9fc7-2446487d92ae
a9e2aecd-3c53-4a6d-98df-63ed56f2c26e
fd9b8d7c-cf8a-4e18-a48c-9f21d6820d84
0d3d1f34-6f8b-4881-9e21-1a638cc0f741
7ebf60df-9834-4e66-9a82-8c7850e842d6
c89379f2-b29d-4402-bc5b-77c6d0a8a090
UUID4 terdari dari 32 digit hexa yang dihubungkan dengan tanda hubung (-) tiap-tiap hexa memiliki kemungkinan dari 0-9 + a-f, maka dari itu kemungkinan bruteforce UUID4 akan terdiri dari 16 ^ 32 atau 3.4028237e+38 (menurut Google kalkulator). Asumsikan saja jika sebuah komputer dapat mencoba 1000 kombinasi perdetik, maka butuh sekitar (3.4028237e+38 / 1000) adalah 3.4 x 10^35 detik atau setara dengan 1.08 x 10^29 tahun (sekitar 13 Miliar tahun).
- Melakukan generate identifier data bisa menggunakan custom algoritma (hashing+cutting+random+hashing) yang kita kembangkan sendiri namun hal-hal yang bisa dan harus diperhatikan adalah tingkat kompleksitas perulangan data dan panjangnya adalah menjadi konsen yang paling utama, semakin panjang data yang ada dan semakin acak penempatan data maka hal tersebut juga akan memperlama waktu untukl melakukan *bruteforce-*nya, contohnya seperti kode di bawah ini
<?php
function GenNameFile(){
$part1 = substr(md5(rand()), 0, rand(6,9));
$part2 = substr(md5(rand()), 0, rand(16,29));
$part3 = substr(md5(rand()), 0, rand(4,6));
$part4 = substr(sha1(rand()), 0, rand(4,6));
$part5 = substr(sha1(rand()), 0, rand(14,26));
return $part1.$part2.$part3.$part4.$part5;
}
for($i=0;$i<100;$i++){
$name = GenNameFile();
echo strlen($name)." -> ".$name.PHP_EOL;
}
Hasil yang dihasilkan akan berbentuk seperti
65 -> 6fddb0c0bd157368d21020648564eeb69300264b6d5d3ea87eb4b9859f4659eaf
55 -> b2e6739cbc77a3e425acb94cdec00b41c9442641669451c11354b77
64 -> 26b0a75da6a0f6c095e338cb39813ab0a17b0cb1629a262db5df6c5f323f6d0b
56 -> c15e828cd389eb9008fc3dd618b62cf2474ae6362407d8efba3cff6b
55 -> e20864657329b927952da8c277fe9a7af3798dfce9d387b1037d39d
57 -> 2375248a28be99757c67fcb4b4e2dc8f25e9323bc40e5d3c680db596a
58 -> 40be25b722bacbc63f0568c20317abe2a57a1080ce76b468f6aa4a9c2c
69 -> fac50d1f7b07535fbbc1fc1d643db3aa7ea0380b0efb522c100ba66b6d91bf0d956a3
56 -> 47cf007863aa54197f1976361bfd6bee541d08fac6b21a3d3aaac221
74 -> 8dac5a2e7657091ab6676fd7736d75e3a0097d8ffa704aeec1ac04f9a465330fb066f5a531
Ada banyak data yang di-generate yang memiliki panjang yang sangat beragam, hal ini mungkin kurang cocok untuk menjadi identifier data user tapi hal ini bisa saja digunakan untuk memberi nama sebuah file agar mempersulit para penyerang untuk menebak pola data milik orang lain, dikarenakan menyimpan sebuah file milik user biasanya tidak perlu autentikasi untuk mengaksesnya.
Content Signature - Key Sharing #
Konten signature berfungsi sebagai identifier dari sebuah body request yang dikirimkan dari service satu ke service lainnya atau antar service. Hal ini diperlukan agar konten / body request yang dikirimkan dari service satu ke service yang lain dipastikan otentik dan integritasnya terjamin.
Satu service dengan service yang lain harusnya akan bertukar key yang sama yang digunakan untuk membuat signature content itu sendiri, teknik signature content ini biasanya digunakan untuk bertukar data dari service 1 ke service yang lain yang masih berada pada satu environment yang sama. Pertimbangan menggunakan konten signature ini biasanya dipilih dikarenakan pengimplementasian sebuah fitur authentication dirasa terlalu berat pada satu environment yang sama, berikut di bawah ini adalah cara melakukan generate content signature untuk bertukar sebuah data
<?php
$body = json_encode([
"key1" => "value1",
"key2" => "value2"
]);
$secretKey = "SuperSecretKeyForContentSignature";
function generateContentSignature($body, $secretKey) {
$bodyStr = json_encode($body, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
$hash = hash_hmac('sha256', $bodyStr, $secretKey);
return $hash;
}
$contentSignature = generateContentSignature($body, $secretKey);
echo $contentSignature;
?>
Jika dijalankan akan menghasilkan nilai seperti berikut ini
4f39776371f587f73418e0fcf3010b5f66e703ded37feb60b49f8a62b72740d6
Hasil dari signature content ini biasanya dikirimkan melalui headers dengan nama apapun, tapi umumnya bisa menggunakan Authorization
dengan detail seperti
Authorization: HMAC-SHA256 <signature_key>
Ataupun jika header key sudah digunakan, dapat menggunakan key signature maka berikutlah bentuk HTTP Request-nya
POST /super/secret HTTP/1.1
Host: 1.1.1.1
User-Agent: PHP-CLIENT
Accept: application/json, text/plain, */*
Accept-Language: id-ID
Accept-Encoding: gzip, deflate
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-origin
Signature: 4f39776371f587f73418e0fcf3010b5f66e703ded37feb60b49f8a62b72740d6
{
"key1": "value1",
"key2": "value2"
}
Bentuk validasinya akan sama seperti bagaimana membuat signature itu sendiri, idenya adalah membandingkan body request dengan hasil signature itu sendiri dengan algoritma dan secret key yang sama, secara diagram akan berbentuk seperti berikut ini
Atau jika ditranslasikan ke kode PHP itu sendiri akan berbentuk seperti
<?php
if(isset($_SERVER['signature'])){
if($_SERVER['signature'] == generateSignatre($_POST)){
process();
}else{
return false;
}
}else{
return false;
}
Itulah salah satu cara untuk dapat bertukar data antar service yang dapat mengurangi resiko terjadinya BAC.
Injection #
Injection adalah salah satu kategori yang ada di OWASP Top 10 sejak 2017, kategori ini di tahun 2017 menempati puncak dari semua kategori kerentanan tersebut, namun di tahun 2021 Injection turun ke posisi ke-3, hal ini dikarenakan arah pergerakan teknologi dalam proses perkembangan sebuah aplikasi, salah satu faktor turunnya kategori ini pada lingkup PHP salah satunya adalah sudah banyak framework dari PHP yang sudah melakukan auto sanitize things yang membantu menekan tingkat resiko keamanan yang bakal terjadi jika menggunakan framework tersebut, beberapa framework PHP yang biasa dikenali adalah CodeIgniter (CI), Laravel, SilverStripe, YII* dan banyak lainnya.
Beberapa celah keamanan umum yang masuk ke kategori Injection di OWASP Top 10 pada tahun 2021 seperti:
- SQL Injection
- XSS (Cross Site Scripting)
- OS Command Injection
Celah umum di atas memungkinkan para penyerang melakukan tindak ancaman yang besar kepada server dan data-data yang ada di dalam server, celah pada kategori Injection terjadi karenanya Unvalidated/unfiltered/unsanitized User Input
, input dari user langsung masuk ke proses dan dari input data tersebut penyerang dapat mengontrol alur kerja dari aplikasi tersebut.
SQL Injection #
SQL Injection adalah kerentanan klasik yang memungkinkan seorang penyerang dapat melakukan dumping / memanipulasi seluruh isi database dari sebuah aplikasi berbasis web ataupun basis platform yang lainnya.
Wrong Example Code #
<?php
$conn = mysqli_connect('localhost', 'root', '', 'dvci4');
if (!$conn) {
die('Connection failed: ' . mysqli_connect_error());
}
$id = isset($_GET['id']) ? $_GET['id'] : 1;
$query = "SELECT * FROM users WHERE id = ".$id;
$result = mysqli_query($conn, $query);
if (mysqli_num_rows($result) > 0) {
while($row = mysqli_fetch_assoc($result)) {
echo $row['name'] . '<br>';
}
} else {
echo 'No results';
}
Pada kode di atas, adalah kode paling klasik dari PHP yang mencontohkan bagaimana SQL Injection terjadi, sederhananya pada bagian variable $query
yang mana adalah query untuk SQL menerima input dari parameter GET['id']
terlihat normal dan memang seperti itu kode yang dapat berjalan, namun ada isu security di mana paramter GET['id']
masuk ke dalam query tanpa adanya sebuah filter apapun, yang menyebabkan user dapat mengontrol query tersebut, bisa digambarkan sebagai berikut contoh nyatanya
$username = $_POST['username'];
$password = md5($_POST['password']);
$query = "SELECT * FROM users WHERE 'username' = '{$username}' AND 'password' = '{$password}'";
Pada contoh kode di atas, permisalan jika input dari $username
dan $password
adalah data yang benar dari sebuah user, maka query-nya dapat berbentuk seperti
SELECT * FROM users WHERE 'username' = 'admin' AND 'password' = 'password';
Namun jika seorang penyerang melakukan serangan, maka akan ada sebuah resiko di mana query dapat dikontrol oleh inputan penyerang, dengan contoh
<?php
$username = "'";
$password = md5("bla");
$query = "SELECT * FROM users WHERE 'username' = '{$username}' AND 'password' = '{$password}'";
Isi dari variable $query
adalah sebagai berikut ini
SELECT * FROM users WHERE 'username' = ''' AND 'password' = 'md5xxxx';
Query di atas akan mengalami error dan jika pada konfigurasi error handling dalam mode development maka resiko query yang error akan dimunculkan pada sisi user dan seminimal mungkin akan mempermudah bagaimana penyerang dapat memanipulasi database di aplikasi, tapi apakah jika memang error handling dalam keadaan production akan ‘menyelamatkan’ aplikasi? tentu tidak, dalam keadaan apapun penyerang akan kreatif dalam mengontrol query tersebut secara ‘blind’ atau tanpa output, misalnya input yang dikontrol seperti berikut ini
<?php
$username = "'-- -";
$password = md5("bla");
$query = "SELECT * FROM users WHERE 'username' = '{$username}' AND 'password' = '{$password}'";
Isi dari variable $query
adalah sebagai berikut ini
SELECT * FROM users WHERE 'username' = ''-- -' AND 'password' = 'md5xxxx';
Isi input dari '-- -
memungkinkan mematikan query yang lainnya seperti pada contoh di atas, bagian query setelah '-- -
akan tidak dieksekusi
' AND 'password' = 'md5xxx';
Itulah inti dari SQL Injection, penyerang dapat leluasa mematikan dan mengontrol query dengan dapat memasukan komentar pada SQL maupun fungsi-fungsi yang lainnya, namun melihat dari tren kategori Injection yang menurun ke posisi ke-3, maka hal ini bisa jadi dampak baik dari penggunaan framework di PHP, tapi apakah menggunakan framework langsung membuat secure aplikasi yang sedang dikembangkan? tentu tidak, berikut ini contoh-contoh penerapan kode yang salah yang menyebabkan SQL Injection pada beberapa frameworks
- Laravel
<?php
use Illuminate\Support\Facades\DB;
$name = $_GET['name'];
$query = "SELECT * FROM users WHERE name = '$name'";
$results = DB::select($query);
Inti implementasi penggunaan Query Builder (Facades DB) seperti contoh di atas, sama dengan PHP Native, SQL Injection dapat terjadi di framework Laravel jika input langsung masuk ke dalam fungsi dari Query Builder itu sendiri, tapi ini semua adalah contoh yang salah dari Implementasi dari penggunaan Query Builder di Laravel.
- CodeIgniter4
<?php
$name = $this->request->getGet('name');
$query = "SELECT * FROM users WHERE name = '$name'";
$results = $this->db->query($query)->getResult();
Sama dengan bentuk implementasi yang salah dari Laravel, di CI juga sangat mungkin terjadi SQL Injection jika implementasi execute query tidak mengikuti standart penggunaan dari CI itu sendiri.
Bad Preventive Strategy #
Melakukan preventive terhadap serangan SQL Injection harus benar-benar tepat agar tidak membahayakan database di kemudian hari ataupun bersifat ‘melindungi’ hanya sementara saja, dari contoh-contoh dasar serangan SQL Injection di atas, secara umum banyak developer yang akan mulai berfikir untuk melakukan beberapa teknik seperti
- Whitelisting String
- Regex Matching
- Blacklisting Malicious String
- Regex Replacing
Strategi di atas mungkin bisa saja “benar” diimplementasikan tapi bisa juga sangat “salah” dalam praktiknya, contohnya jika menggunakan teknik Whitelisting dan Blacklisting yang “berusaha dikembangkan sendiri” untuk menangani serangan SQL Injection bisa saja sangat kurang efektif, permisalan jika diketahui serangan SQL Injection berasal dari single quote dan double quote maka hal ini akan sangat kurang efektif dikarenakan mengontrol sebuah query tidak harus memasukan sebuah single quote ataupun double quote saja, mengontrol sebuah query juga bisa dilakukan dengan memasukan keywords milik SQL itu sendiri, jadi apakah efektif melakukan listing? tentu tidak, salah satunya bisa mengganggu proses bisnis dari aplikasi itu sendiri, permisalan seorang pelanggan memiliki nama Abdul Mu'ti
dalam prakteknya jika kita men-deny sebuah single quote maka nama dari pelanggan tersebut tidak akan sama seperti apa yang diharapkan oleh si pelanggan, jadi apa teknik yang paling tepat? strategi yang paling tepat untuk menghalau serangan SQL Injection di PHP adalah dengan mempercayakan kepada PHP ataupun kepada framework yang sedang digunakan untuk pengembangan.
Best Preventive Example Code #
SQL Injection adalah masalah klasik yang sampai tulisan ini dibuat masih banyak terjadi dan menjadi salah satu teknik yang menjadi ‘alasan’ banyaknya Data Breach yang terjadi baik di Indonesia ataupun di ranah Dunia, cara pasti untuk bisa menanggulangi celah ini adalah dengan teknik escaping yang sudah disiapkan oleh PHP itu sendiri ataupun dari framework yang sedang digunakan dalam proses pengembangan.
Berikut ini contoh-contoh escaping yang sudah disediakan oleh PHP ataupun framework yang langsung dapat digunakan ataupun disesuaikan
- Native PHP MYSQLi
<?php
$conn = mysqli_connect('localhost', 'root', '', 'dvci4');
if (!$conn) {
die('Connection failed: ' . mysqli_connect_error());
}
$input = "User' OR 1=1 -- -";
$name = mysqli_real_escape_string($conn, $input);
$query = "SELECT * FROM users WHERE name = '{$name}'";
echo $query.PHP_EOL;
$result = mysqli_query($conn, $query);
if (mysqli_num_rows($result) > 0) {
while($row = mysqli_fetch_assoc($result)) {
echo $row['name'] . PHP_EOL;
}
} else {
echo 'No results';
}
Jika menggunakan class mysqli_* untuk koneksi ke database maka cara terbaik untuk mencegah terjadinya SQL Injection adalah melakukan escape dengan salah satu metode yang ada di rumpun mysqli_* yaitu mysqli_real_escape_string fungsi tersebut akan melakukan sanitize terhadap input kita sebelum dimasukan ke dalam query, hal ini menjadi solusi terbaik yang bisa dilakukan, lalu sebenarnya fungsi mysqli_real_escape_string melakukan apa, bisa dilihat pada tangkapan layar di bawah ini
$input = ['"', "'", ";", ">", "<", "/", '\\', '(', ')', '!', '?', '=', '+', '-', '*'];
foreach ($input as $char) {
echo $char . ' => ' . mysqli_real_escape_string($conn, $char) . PHP_EOL;
}
Beberapa simbol yang dapat ‘mengacaukan’ query akan dilakukan escape / sanitize agar simbol tersebut tetap bisa “masuk” dan query tetap berjalan dengan baik.
- PDO Prepare Statement
<?php
$conn = new PDO("mysql:host=localhost;dbname=dvci4", "root", "");
$conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$sql = "SELECT * FROM users where `email` = ? and `password` = ?";
$q = $conn->prepare($sql);
$username = "admin@admin.com";
$password = md5("123123");
$q->bindParam(1, $username);
$q->bindParam(2, $password);
$q->execute();
$data = $q->fetch(PDO::FETCH_ASSOC);
print_r($data);
Di atas adalah contoh bagaimana menggunakan koneksi PDO dan melakukan bindParameter untuk tiap-tiap data yang menjadi input di dalam query, lalu bagaimana bentuk query yang dijalankan oleh PDO dikarenakan kode di atas tidak ‘terlihat’ melakukan ‘escape’
<?php
$conn = new PDO("mysql:host=localhost;dbname=dvci4", "root", "");
$conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$sql = "SELECT * FROM users where `email` = ? and `password` = ?";
$q = $conn->prepare($sql);
$username = "' OR 1=1 --";
$password = md5("123123");
$q->bindParam(1, $username);
$q->bindParam(2, $password);
$q->execute();
$data = $q->fetch(PDO::FETCH_ASSOC);
$q->debugDumpParams();
Dengan merubah kode di atas pada bagian input menjadi salah satu payload public yang digunakan untuk melakukan bypass logic via SQL Injection, kita dapat melihat bagaimana query ditulis menggunakan PDO koneksi ini dengan melakukan debugDumpParams()
dan hasilnya bisa dilihat seperti pada tangkapan layar di bawah
Bisa dilihat pada tangkapan layar di atas pada bagian Sent SQL [102]
query yang dikirimkan ke langsung mendapatkan escape secara otomatis dari PDO itu sendiri, inilah cara terbaik yang bisa dilakukan untuk melakukan preventif terhadap serangan SQL Injection.
- CodeIgniter4
public function debug(){
$email = "admin@admin.com'asdasd";
// $db $this->db is from ConnectionInterface &db
$user = $this->db->table("users")->where("email", $email);
return json_encode($user->get()->getResultArray());
}
Pada CodeIgniter4 (CI4) yang menggunakan interface ConnectionInterface kita dapat menajalankan query dengan query builder bawaan dari CI4 maiupun menjalankan sebuah raw query, CI4 secara otomatis melakukan escape dan sanitize terhadap input dari user, query yang dikirimkan ke database dapat dilihat pada debug bawaan CI4 dan hasilnya sudah akan mendapatkan escape
Bagaimana jika menjalankan raw query? bisa dilihat contoh di bawah ini, kuncinya adalah tetap menggunakan binding parameter ?
pada setiap inputan dari user
public function debug(){
$email = "admin@admin.com'asdasd";
$query = "SELECT * FROM users WHERE email = ?";
$result = $this->db->query($query, [$email]);
return $result->getResultArray();
}
Jika dilihat pada debug yang dimiliki oleh CI4 query yang dikirimkan sudah akan otomatis ter-sanitize
Hal lain yang dapat dilakukan untuk terhindar dari serangan SQL Injection adalah dengan bergantung pada model yang dibuat sebagai jembatan ke database. Kita juga dapat melihat kode sumber untuk tiap-tiap kelas yang digunakan untuk menjalankan query jika memang mengalami trust issue
- https://github.com/codeigniter4/CodeIgniter4/blob/0574c89444c229b612b883af21ebb0a437ff6f5b/system/Database/BasePreparedQuery.php
- https://github.com/codeigniter4/CodeIgniter4/blob/0574c89444c229b612b883af21ebb0a437ff6f5b/system/Database/BaseBuilder.php
- https://github.com/codeigniter4/CodeIgniter4/blob/0574c89444c229b612b883af21ebb0a437ff6f5b/system/Database/BaseBuilder.php#L729
- Laravel
use DB;
public function debug(){
DB::enableQueryLog();
$email = "admin@admin.com'123123123";
$data = DB::table('users')->where('email','=', $email)->get();
dd(DB::getQueryLog());
}
Dengan menggunakan facades DB kita dapat menjalankan query builder yang mana jika dilakukan debugging maka terlihat bahwa input dari user secara otomatis akan dilakukan bindding dan semuanya ditangani oleh Laravel tanpa harus memikirkan cara untuk melakukan escape
Jika ingin menjalakan raw query beserta input yang berasal dari user, Laravel juga secara otomatis akan melakukan bind dan escape agar aplikasi lebih aman dari serangan SQL Injection
public function debug(){
DB::enableQueryLog();
$email = "admin@admin.com'123123123";
$query = "SELECT * FROM users WHERE email = ?";
$users = DB::select($query, [$email]);
dd(DB::getQueryLog());
}
Ada hal lain yang dapat dilakukan untuk terhindar dari serangan SQL Injection adalah dengan bergantung pada Eloquent Model yang dibuat sebagai jembatan ke database. Kita juga dapat melihat kode sumber untuk tiap-tiap kelas yang digunakan untuk menjalankan query jika memang mengalami trust issue
- https://github.com/laravel/framework/blob/c262c1ffce93c5fa4b9e00da1a54d8c02ddddcf6/src/Illuminate/Database/Eloquent/Builder.php
- https://github.com/laravel/framework/tree/10.x/tests/Database
XSS (Cross Site Scripting) #
Cross Site Scripting (XSS) adalah sebuah celah yang masuk pada bagian kategori Injection yang titik beratnya menyerang client (browser) milik victim yang ditargetkan dengan memasukan syntax HTLM ataupun mengeksekusi script javascript untuk melakukan banyak proses yang tidak diketahui oleh victim, salah satu objective utama dari serangan XSS ini adalah cookies stealing (meskipun resiko ini bisa dikurangi dengan melakukan set HttpOnly ke Cookies), mencuri / mengirimkan cookies milik user yang sedang ditargetkan ke penyerang untuk digunakan kembali agar dapat melakukan impersonate user yang lainnya.
Serangan XSS sendiri secara sederhana memiliki 3 tipe serangan, mulai dari Refelcted XSS, Stored XSS dan Self XSS semua jenis tersebut penangannnya dari Native sampai Framework dari berbagai versi akan sama atau sedikit perbedaan jika mengikuti framework, namun ada 1 fungsi yang akan bisa membantu menanggulagi celah ini.
Reflected XSS
adalah sebuah celah di mana payload XSS ‘disimpan’ di sebuah URL yang isinya akan ditampilkan pada page yang sedang dibuka oleh victim, cara penangannya tidak ada cara khusus, tinggal melakukan escaping pada data yang di-render dari input URL.
Self XSS
adalah sebuah celah yang dapat men-trigger payload XSS yang mana targetnya adalah yang mengalami “kerugian” adalah dirinya sendiri, cara penangannanya bisa sama persis seperti bagian dari Reflected XSS atau bisa juga mengabaikan kerentanan XSS kategori ini, namun resiko akan tetap selalu ada.
Stored XSS
adalah kerentanan dengan resiko yang paling berat dari semua kategori XSS yang ada, stored xss melakukan remediasi akan cukup tricky jika melakukan strategi yang salah dalam penangannya, mari merujuk pada diagram di bawah ini
Pada skenario di atas, kasusnya terjadi sebuah celah keamanan Self XSS
dan Stored XSS
pada sebuah fitur registrasi, fitur tersebut memiliki 3 field pada sebuah form, field yang vulnerable terhadap XSS adalah field Nama, untuk menyelesaikan celah XSS tersebut cara pastinya adalah melakukan Sanitize terhadap field Nama tersebut, namun bagaimana dengan data yang ada di Database yang sudah masuk tanpa sanitize? Benar! Caranya adalah dengan melakukan sanitize di bagian-bagian view yang melakukan render data yang ada dalam database berikut ini adalah diagram strateginya
Example Code to Sanitize Data for XSS #
- Native PHP Example
<?php
//insert process
$data = array(
'nama' => htmlentities($_POST['nama')
);
//or
$data = array(
'nama' => htmlspecialchars($_POST['nama')
);
insert($data);
<?php
//render process
echo htmlentities($_GET['nama']);
//or
echo htmlspecialchars($_GET['nama']);
Cara di atas adalah cara yang pasti dan bisa digunakan di semua framework ataupun native saat mengembangkan aplikasi dengan menggunakan bahasa pemrogramman PHP, namun pada framework lainnya juga memiliki strategi untuk melakukan sanitize, seperti framework Laravel menggunakan templating (blade) yang memastikan data yang dirender dilakukan sanitize
- Laravel
<?php
echo e($data);
//auto
{{$data}}
- CodeIgniter4
<?php
echo esc($data);
Cara terbaik untuk melakukan sanitize adalah mengandalkan fungsi bawaan yang sudah dibuat oleh PHP itu sendiri. Kita juga dapat melihat kode sumber untuk tiap-tiap kelas yang digunakan untuk melakukan sanitize jika memang mengalami trust issue, lalu cara terbaik untuk terhindar dari XSS adalah tetap melakukan escape saat rendering data APAPUN dari input user.
Rekomendasi fungsi untuk melakukan escape dengan fungsi bawaan dari PHP
- htmlentities
- htmlspecialchars
Kedua fungsi tersebut dapat digunakan untuk melakukan escape data untuk menghindari serangan XSS, perbedaannya hanyalah htmlentities
meakukan enkode ke SEMUA karakter yang berkaitan dengan HTML Entity itu sendiri, jika htmlspecialchars
hanya melakukan enkode ke karakter yang problematic
yang memiliki resiko XSS, untuk kebutuhan bisnis biasanya htmlspecialchars
lebih banyak digunakan dikarenakan ramah dengan tampilan ke user, di bawah ini adalah tangkapan layar dan contoh kode untuk membandingkan penggunaan htmlentities
dan htmlspecialchars
<?php
$data = ["<h1> Hello World </h1>", "<h2> Hello World </h2>", "<img src='dpi.png' />,", "<script>alert(1)</script>", "<<<", "<><>>><<<()))"];
foreach ($data as $key => $value) {
echo $value." -> ". htmlentities($value)." -> ".htmlspecialchars($value).PHP_EOL;
}
Referensi Kode
- https://github.com/laravel/framework/blob/aef89589ea70e0081c139b06550220cc75f20ea6/src/Illuminate/View/Compilers/Concerns/CompilesEchos.php#L133
- https://github.com/laravel/framework/tree/aef89589ea70e0081c139b06550220cc75f20ea6/src/Illuminate/View/Compilers/Concerns
- https://github.com/codeigniter4/CodeIgniter4/blob/develop/system/Common.php#L424
- https://www.php.net/manual/en/function.htmlentities.php
OS Command Injection #
OS Command Injection adalah sebuah celah yang termasuk dalam bagian Injection yang sangat jarang ditemui di aplikasi umum, dikarenakan memang kebutuhan sebuah aplikasi di saat menjalankan sebuah bisnis proses sangat jarang sekali memerlukan interaksi langsung antara bahasa pemrogramman dengan OS yang sedang menjalankannya, namun tidak menutup kemungkinan bahwa memang ada kasus tertentu yang membutuhkan interaksi langsung dengan OS yang menjalankannya, misalnya melakukan backup atau melakukan interaksi dengan urusan networking seperti melakukan PING antar service di dalam lingkup area network tersebut
Wrong Example Code #
Contoh kasus yang sangat sering terjadi dan sering ditemui oleh penulis adalah sebuah aplikasi melakukan PING ke salah satu server public ataupun melakukan sebuah PING ke internal service dengan menerima alamat IP dari inputan user
<?php
$input = "8.8.8.8";
echo shell_exec("ping $input");
Terlihat normal script yang dijalankan, PHP mengirimkan perintah ke OS untuk melakukan peritah ping 8.8.8.8
yang mana 8.8.8.8
tersebut dimisalkan dari user input ataupun ada contoh kode yang lain seperti
<?php
$input = "backup.csv";
echo system("/opt/usr/binaryBackup $input");
Semua user input yang masuk ke shell os cammand function family (shell_exec,exec,system,dst) tanpa adanya sanitize akan berpotensi memiliki ancaman yang sangat-sangat besar dan puncaknya adalah bisa melakukan Server Take Over, dikarenakan user secara leluasa dapat memasukan OS Command lain yang tekniknya mirip dengan SQL Injection, pada contoh kasus PING ke service yang lain seorang penyerang dapat memasukan sebuah syntax pipeline yang mana dapat menggabungkan banyak hal dalam command ping
tersebut, berikut contoh kodenya
<?php
$input = "8.8.8.8 && dir";
echo shell_exec("ping $input");
Sederhananya setelah menjalankan proses ping
OS akan menjalankan command yang lainnya dengan bantuan syntax &&
dan command yang dijalankan adalah perintah dir
dari sinilah proses mengontrol server dapat terjadi, berikut contohnya
Setelah menjalankan proses ping
pada kotak berwarna merah, selanjutkan OS akan menjalankan command dir
yang hasilnya dapat dilihat pada kotak berwarna hijau, resiko kerentanan dari celah ini akan sangat-sangat besar dikarenakan penyerang dapat berinteraksi langsung dengan OS Server.
Preventive Example Code #
Sebelum melakukan preventive ada hal lain yang perlu diperhatikan, yaitu kebijakan memilih menggunakan fungsi yang berinteraksi dengan OS Shell resiko terjadinya sebuah kerentanan akan sangat besar, tapi jika memang harus menggunakan fungsi yang harus berinteraksi dengan OS Shell berikut ini adalah caranya untuk mengamkan input dari user
<?php
$input = "8.8.8.8 && ls -la";
if (preg_match('/^(\d{1,3}\.){3}\d{1,3}$/', $input)) {
$clean_input = escapeshellarg($input);
echo shell_exec("ping $clean_input");
} else {
echo "Input tidak valid";
}
Hasilnya akan masuk ke else
yang mana input dari ip
tidak valid dikarenakan regex tersebut tidak match dengan rule regex yang didefinisikan, lalu input akan di-sanitize dengan fungsi escapeshellarg
jika kasusnya tidak melakukan sanitize terhadap input IP, maka developer cukup untuk menggunakan fungsi escapeshellarg saja seperti
<?php
$input = "8.8.8.8 && dir";
$clean_input = escapeshellarg($input);
echo $input.PHP_EOL;
echo $clean_input.PHP_EOL;
echo shell_exec("ping $clean_input");
Hasilnya akan seperti berikut ini, command injection tidak dapat dijalankan dikarenakan semua input dibungkus dengan double quote sebagai 1 argument mutlak.
Salah satu strategi terbaik untuk menghindari serangan OS Command Injection adalah dengan melakukan sanitize dengan menggunakan escapeshellarg
sebelum dimasukan ke semua fungsi yang berhubungan dengan OS Command, strategi yang lain jika memang tidak diperlukan untuk mengurangi resiko maka bisa melakukan nonaktifkan fungsi yang berhubungan dengan OS Command Shell.
Hashing #
Hash adalah sebuah algoritma di mana kita dapat merubah sebuah data (plaintext) menjadi sebuah data (hash) yang tunggal tidak bisa dikembalikan ke data awalnya (plaintext) dan memiliki panjang data yang sama dan konsisten. Fungsi dari sebuah hash biasanya untuk melindungi atau menyimpan sebuah kata sandi atau menyimpan hal-hal yang sifatnya tidak boleh diketahui oleh siapapun, yang berhak mengetahui isi dari sebuah kata sandi adalah pemilik dari kata sandi itu sendiri, bahkan developer atau CEO dari perusahaanpun tidak berhak mengetahui kata sandi tersebut dari sinilah fungsi dari hash itu digunakan salah satunya.
Mengubah bentuk kata sandi dari plaintext ke hash bukan berarti serta merta urusan mengamankan kata sandi sudah selesai dilakukan, ada beberapa algoritma hash yang sudah “usang” sudah memiliki banyak database dari plaintext ke hash, meskipun secara teori hash algorithm tidak memiliki fungsi de-hash tapi dengan teknik bruteforce ataupun rainbowtable maka akan ada kemungkinan sebuah kata sandi dapat di-crack.
Cracking the Password #
Cracking sebuah password bisa menggunakan salah satu teknik yang umum digunakan yaitu “Rainbow Table Attack” yang mana singkatnya adalah ada sebuah database besar yang berisi bentuk plaintext,hash,algortihm jika dibuat sebuah tabel maka akan berbentuk seperti
Plaintext | Hash | Algorithm |
---|---|---|
NikkoEnggaliano | xxxxxxxxxxxx | x1 |
Lionel Messi | xxxxxyyyyyy | x2 |
Tabel inilah yang menjadi sebuah “jawaban” dari cara cracking password yang umum digunakan, adalah lagi teknik yang biasanya digunakan, ada sebuah kumpulan plaintext password yang mungkin berjumlah 10 juta, yang mana tiap-tiap password tersebut di-hash dengan algoritma yang kita inginkan, atau bisa disebut juga Reverse Lookup Tables. merujuk pada sebuah website yang menyediakan service untuk Lookup Tables CrackStation
website tersebut memiliki 15 Miliar data plaintext,hash untuk algoritma seperti md5 dan sha1 lalu memiliki sekitar 1.5 Miliar data plaintext,hash untuk hash algorithm yang lainnya seperti LM, NTLM, md2, md4, md5, md5(md5_hex), md5-half, sha1, sha224, sha256, sha384, sha512, ripeMD160, whirlpool, MySQL 4.1+ (sha1(sha1_bin)), QubesV3.1BackupDefaults.
Kembali merujuk pada website Crackstation service yang diberikan untuk melakukan cracking hanya untuk password yang tidak mempunyai salt atau unsalted hash.
Salting #
Jadi apa itu salting yang di-highlight oleh CrackStation
bahwa salted password is not working to be cracked here . Jadi, salting adalah teknik menambahkan sebuah data pada plaintext password yang mana tujuannya untuk “mengecoh” teknik Rainbow Table yang mana jika disimulasikan
plaintext | md5 |
---|---|
bissmillah | c5fe279dba24b4dff68bf86263db7981 |
Hash md5 dari kata sandi bissmillah adalah c5fe279dba24b4dff68bf86263db7981
yang mana jika dilakukan cracking pada website CrackStation maka akan ketemu hasilnya
Namun bagaimana jika password ditambahkan salt berupa random data yang panjangnnya misal 10 digit, maka tabelnya akan berbentuk seperti berikut ini
plaintext+salt | hash md5 |
---|---|
bissmillahGc5vb | 20f1a2748623f9b1206601362ae7b830 |
bissmillah6vVQO | ea6bb322b4ae9a049d1d94c85ed1f45c |
bissmillahOPKMU | 635be95c29c85eebbdf26b67e59d61cb |
bissmillahuJBzn | eeb2e60f15245abe5441652d8c4e52c8 |
bissmillahMhC85 | 71751a683921af196abc4c3a2137ec31 |
bissmillahVjR9d | 9647d55a92bc931035cd3a2a41dee7b7 |
Jika mencoba lagi melakukan cracking terhadap daftar MD5 hash yang ada dari hasil salting
20f1a2748623f9b1206601362ae7b830
ea6bb322b4ae9a049d1d94c85ed1f45c
635be95c29c85eebbdf26b67e59d61cb
eeb2e60f15245abe5441652d8c4e52c8
71751a683921af196abc4c3a2137ec31
9647d55a92bc931035cd3a2a41dee7b7
Maka crackstation pun tidak dapat menemukannya, alasannya kenapa? karena di database milik mereka data bissmillah+salt
pasti tidak ditemukan, apalagi sifat salting yang sangat acak tersebut
Lalu apakah ini adalah ‘solusi’ untuk melindungi password milik user? jawabannya tidak, masalah pada salting ini adalah apakah salt-nya dilakukan secara static atau dinamic. Jika static akan ada sebuah resiko jika salt-nya bocor ataupun ada celah yang memungkinkan seorang penyerang membaca sebuah source code, lalu masalah lainnya jika salt dibat secara dynamic bagaimana cara menyimpannya? bagaimana jika database-nya ‘bocor’ juga, itulah serba-serbi permasalahan menggunakan salt pada sebuah password plaintext.
Storing Password with PHP-way #
Cara terbaik menyimpan sebuah password adalah dengan menggunakan function yang sudah disediakan oleh PHP itu sendiri, function tersebut mempunyai banyak algoritma yang memiliki tingkat kompleksitas pembuatan password yang sangat-sangat kuat dikarenakan tiap-tiap password yang dihasilkan sudah memiliki salt tersediri yang disimpan pada password itu sendiri contohnya seperti pada di bawah ini
PHP sudah menyediakan sebuah fungsi bernama password_hash
yang mana pada versi mayor 8.x memiliki 4 dukungan pada algoritma seperti bcrypt,blowfish
lalu ARGON2
jika PHP dikompilasi menggunakan module Argon2 Support, penggunaan mode password_default
sudah dapat menghasilkan sebuah password yang memiliki salt di dalamnya, sehingga developer tidak lagi memikirkan bagaimana menggunakan system salting, berikut contoh kodenya.
<?php
$password = "NikkoEnggaliano";
for($x = 0; $x < 10; $x++){
echo $password ." -> ". password_hash($password, PASSWORD_DEFAULT). PHP_EOL;
}
password_hash
memungkinkan membuat password yang berbeda tiap-tiap dijalankan yang mana semua password tersebut akan tetap valid, berikut ini hasilnya
Setiap kali iterasi dijalankan, maka akan dihasilkannya password yang baru, bagaimana itu bisa terjadi? berikut ini stuktur dari hasil bcrypt tersebut
$2<a/b/x/y>$[cost]$[22 character salt][31 character hash]
$2y$10$gNq5E7v5rg.S0JpPLa5bseWInqM89ZV3lsAH2pgGujCfUgy4XDhqW
- 4 Karakter pertama dari password tersebut
$2y$
adalah versioning algoritma yang digunakan yaitu bcrypt, versioning ini merujuk pada tabel yang ada pada modul Crypt - Lalu diikuti dengan
10$
adalah nilai cost factor yang diulang untuk menghasilkan password tersebut, tujuannya untuk mengurangi resiko serangan bruteforce - Komponen berikutnya adalah
gNq5E7v5rg.S0JpPLa5bse
yaitu 22 karakter yang berfungsi sebagai salt - Yang terakhir adalah
WInqM89ZV3lsAH2pgGujCfUgy4XDhqW
hasil hash dengan algoritma itu sendiri
Dari password yang dihasilkan tersebut, kita tetap dapat memastikan password tersebut valid atau tidak dengan menggunakan fungsi password_verify
berikut contoh penggunaannya
if (password_verify($plaintext, $hash)){
return true;
}else{
die();;
}
Hasilnya dapat didemonstrasikan sebagai berikut ini
Jika supply plaintext memang benar, maka fungsi password_verify
akan mengembalikan True adapun sebaliknya akan mengembalikan False hal ini lah yang dijadikan acuan dasar untuk membuat sistem Login.
PHP password_verify possible password backdoor < 8.1.20 (CVE - 2023 - 0567) #
Pada versi PHP sebelum mayor 8.1.20 terdapat celah pada fungsi password_verify
yang mana memungkinkan jika ada sebuah data $
pada salt hash maka fungsi tersebut akan mengembalikan nilai True
Fungsi yang bermasalah dapat dilihat pada tautan internal di atas, fungsi password_verify
pada versi tertentu memungkinkan sebuah hash $2y$04$00000000$
dapat divalidasi dengan plaintext apapun
<?php
var_dump(password_verify("NikkoooooooEnggaliano", '$2y$04$00000000$'));
Resiko ini tergolong rendah, dikarenakan seorang penyerang harus memiliki akses ke database agar dapat menganti sebuah hash column menjadi hash magic ($2y$04$00000000$
)
File Upload Handling #
Membangun sebuah aplikasi dengan menggunakan bahasa pemrogramman PHP memiliki salah satu “celah” yang hanya dapat terjadi pada rumpun konfigurasi PHP, yaitu Remote Code Execution via File Uploads Vulnerability, celah ini memungkinkan seorang penyerang dapat mengunggah sebuah kode PHP / webshell untuk menjalankan proses yang dikehendaki oleh si penyerang hal terparahnya adalah penyerang dapat mengontrol sampai server dan database. Lalu bagaimana hal ini bisa terjadi? berikut contoh kodenya
<?php
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$targetDirectory = 'uploads/';
$targetFile = $targetDirectory . basename($_FILES['file']['name']);
if (move_uploaded_file($_FILES['file']['tmp_name'], $targetFile)) {
echo 'File berhasil diunggah.';
} else {
echo 'Error: Gagal mengunggah file.';
}
}
?>
<!DOCTYPE html>
<html>
<head>
<title>Unggah Foto Profile</title>
</head>
<body>
<form method="POST" enctype="multipart/form-data">
<input type="file" name="file" />
<input type="submit" value="Unggah" />
</form>
</body>
</html>
Pada skenario kode di atas, secara bisnis proses, kodenya akan menerima sebuah input files dari user berubah file gambar untuk mengunggah sebuah foto profil dari seorang user. Secara bisnis aplikasi file yang dapat diunggah adalah file gambar saja, namun pada praktek kodenya semua file akan dapat diunggah ke dalam server, potongan kodenya bisa dilihat
<?php
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$targetDirectory = 'uploads/';
$targetFile = $targetDirectory . basename($_FILES['file']['name']);
if (move_uploaded_file($_FILES['file']['tmp_name'], $targetFile)) {
echo 'File berhasil diunggah.';
} else {
echo 'Error: Gagal mengunggah file.';
}
}
?>
File dari user akan dipindah dari /tmp/file
ke destinasi yang ditelah ditentukan oleh file, jadi singkatnya adalah seorang penyerang dapat mengunggah file apapun ke dalam sebuah server, apalagi jika file tersebut dapat diakses secara direct oleh seorang penyerang maka resiko server ter take over akan semakin tinggi, lalu apa solusi untuk menanggulangi kerentanan ini jawabannya adalah whitelisting extension
yang boleh diunggah ke server, berikut contoh kodenya
<?php
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$fileType = strtolower(pathinfo($targetFile, PATHINFO_EXTENSION));
$allowedExtensions = array('jpg', 'jpeg', 'png', 'gif');
if (!in_array($fileType, $allowedExtensions)) {
die();
}
$targetDirectory = 'uploads/';
$targetFile = $targetDirectory . basename($_FILES['file']['name']);
if (move_uploaded_file($_FILES['file']['tmp_name'], $targetFile)) {
echo 'File berhasil diunggah.';
} else {
echo 'Error: Gagal mengunggah file.';
}
}
?>
Pada kode di atas ditambahkan sebuah validasi terkait bagaimana aplikasi memastikan ekstensi yang diunggah oleh user adalah termasuk bagian dari [”jpg”, “jpeg”, “png”, “gif”] jika file yang diunggah ekstensinya tidak sesuai dengan whitelist maka program tidak akan melanjutkan untuk memindah file ke direktori yang ditentukan, bentuk validasi yang dapat dilakukan lagi adalah melakukan pengecekan terhadap ukuran sebuah file, melakukan pengecekan terhadap lebar dan tinggi dari file gambar yang diunggah
<?php
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$fileType = strtolower(pathinfo($targetFile, PATHINFO_EXTENSION));
$allowedExtensions = array('jpg', 'jpeg', 'png', 'gif');
if($_FILES['file']['size'] > 2000000){
die('Sorry just allowed less than 2 Mb');
}
$data = getimagesize(realpath($_FILES['file']['tmp_name']));
$width = $data[0];
$height = $data[1];
if(empty($width) || empty($height)){
die('Please upload image!');
}
if (!in_array($fileType, $allowedExtensions)) {
die();
}
$targetDirectory = 'uploads/';
$targetFile = $targetDirectory . basename($_FILES['file']['name']);
if (move_uploaded_file($_FILES['file']['tmp_name'], $targetFile)) {
echo 'File berhasil diunggah.';
} else {
echo 'Error: Gagal mengunggah file.';
}
}
?>
Lalu cara ultimate dari semua ini adalah mengkonversi ekstensi-nya menjadi ".png"
lalu merubah nama filenya menjadi sesuatu yang sangat-sangat acak, berikut cara ultimate-nya
<?php
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
function GenNameFile(){
$part1 = substr(md5(rand()), 0, rand(6,9));
$part2 = substr(md5(rand()), 0, rand(16,29));
$part3 = substr(md5(rand()), 0, rand(4,6));
$part4 = substr(sha1(rand()), 0, rand(4,6));
$part5 = substr(sha1(rand()), 0, rand(14,26));
return $part1.$part2.$part3.$part4.$part5;
}
$fileType = strtolower(pathinfo($targetFile, PATHINFO_EXTENSION));
$allowedExtensions = array('jpg', 'jpeg', 'png', 'gif');
if($_FILES['file']['size'] > 2000000){
die('Sorry just allowed less than 2 Mb');
}
$data = getimagesize(realpath($_FILES['file']['tmp_name']));
$width = $data[0];
$height = $data[1];
if(empty($width) || empty($height)){
die('Please upload image!');
}
if (!in_array($fileType, $allowedExtensions)) {
die();
}
$targetDirectory = 'uploads/';
$targetFile = $targetDirectory . GenNameFile().".png";
if (move_uploaded_file($_FILES['file']['tmp_name'], $targetFile)) {
echo 'File berhasil diunggah.';
} else {
echo 'Error: Gagal mengunggah file.';
}
}
?>
Dengan cara di atas, apapun file yang diunggah oleh penyerang akan otomatis dikonversi menjadi ekstensi ".png"
dan web server
tidak akan mengeksekusi file tersebut menjadi script PHP, dengan begini aplikasi PHP sudah dapat aman dari serangan file upload vulnerability, cara ini (mempermanenkan sebuah ekstensi) juga dapat digunakan untuk fitur file uploads dokumen apapun, cara ini juga dapat digunakan untuk framework seperti CI4 ataupun Laravel bahkan framework tersebut memiliki fungsi filter validate yang membuat kode bisa sangat-sangat sederhna
- CodeIgniter4
$validationRule = [
'userfile' => [
'label' => 'Image File',
'rules' => [
'uploaded[userfile]',
'is_image[userfile]',
'mime_in[userfile,image/jpg,image/jpeg,image/gif,image/png,image/webp]',
'max_size[userfile,100]',
'max_dims[userfile,1024,768]',
],
],
];
$allowed_extension = ["png", "jpg", "jpeg"];
$file->getClientExtension();
- Laravel
$request->validate([
'file' => 'required|mimes:png,jpg,jpeg,csv,txt,xlx,xls,pdf|max:2048'
]);
$allowed_extension = ["jpg", "png", "jpeg", "gif"];
$ext = $file->getClientOriginalExtension();
Referensi kode
Closing #
Terima kasih telah membaca PHP Secure Coding! Keamanan adalah salah satu aspek paling penting dalam pengembangan perangkat lunak, dan dengan memahami praktik-praktik aman yang telah saya coba jabarkaan dalam artikel ini, Anda telah memperkuat aplikasi berbasis PHP anda terhadap potensi ancaman security yang akan datang.