Aplikasi PHP modern sebagian besar terdiri dari kode yang tidak Anda tulis sendiri: framework, HTTP client, library logging, dan puluhan paket transitif yang ikut terpasang lewat Composer. Setiap paket tersebut adalah kepercayaan yang Anda berikan kepada pengelola dan infrastrukturnya — dan penyerang tahu persis hal ini. Chapter ini membahas sisi risiko dari dependensi: pola serangan rantai pasokan, cara menilai paket sebelum diadopsi, serta kebiasaan operasional yang menjaga deretan paket tetap sehat. Mekanisme alat audit Composer sendiri sudah dibahas tuntas pada chapter dependency auditing , jadi fokus kita di sini adalah cara berpikir menilai risikonya.

Risiko Rantai Pasokan Dependensi #

Supply chain attack adalah serangan yang menyasar bukan aplikasi Anda, melainkan jalur pembuatannya: paket pihak ketiga yang Anda pasang percaya. Ketika composer update berjalan, kode baru dari puluhan pengelola masuk ke aplikasi dalam hitungan detik — tanpa satu pun baris direview oleh tim Anda. Empat pola risiko yang paling sering muncul:

Pola risikoBentuknyaAkibat bagi aplikasi
Typosquattingnama paket mirip paket populer, misal guzzel vs guzzlepaket jahat terinstal karena salah ketik
Paket terbengkalairilis terakhir sudah bertahun-tahun, issue menumpuk tanpa tanggapancelah keamanan tidak pernah ditambal
Akun pengelola dibajakpenyerang merilis versi baru berisi muatan jahatkode berbahaya dieksekusi saat composer update
Dependency confusionpaket internal dipublikasikan di registry publik dengan nama samaversi jahat dari registry publik menang resolusi

Kesamaan keempat pola tersebut: kerusakan masuk melalui pintu yang secara sah Anda buka sendiri. Karena itu pertahanannya bukan sekadar alat pemindai, melainkan kebiasaan mempersempit pintu masuk — menilai paket sebelum diadopsi, meminimalkan jumlahnya, dan mengunci versi yang terpasang.

Typosquatting dan Pemeriksaan Nama Mirip #

Typosquatting memanfaatkan satu salah ketik: Anda ingin memasang guzzlehttp/guzzle tetapi mengetik guzzlehttp/guzzel, dan di Packagist memang ada paket bernama persis itu. Pemeriksaan sederhana bisa dibuat dengan fungsi levenshtein() yang menghitung jumlah pengubahan karakter minimum dari satu string ke string lain:

php
<?php
function cariPaketMirip(string $kandidat, array $namaResmi, int $jarakMaks = 2): ?string
{
    $terbaik = null;
    $jarakTerbaik = $jarakMaks + 1;

    foreach ($namaResmi as $resmi) {
        $jarak = levenshtein(strtolower($kandidat), strtolower($resmi));
        if ($jarak < $jarakTerbaik) {
            $jarakTerbaik = $jarak;
            $terbaik = $resmi;
        }
    }

    return $jarakTerbaik <= $jarakMaks ? $terbaik : null;
}

$paketResmi = ["guzzlehttp/guzzle", "monolog/monolog", "symfony/http-foundation"];

foreach (["guzzlehttp/guzzel", "monolog/monolog", "swiftmailer/swiftmailer"] as $kandidat) {
    $hasil = cariPaketMirip($kandidat, $paketResmi);
    echo $kandidat . " => " . ($hasil ?? "tidak ada yang mirip") . "\n";
}

Output yang Dihasilkan:

text
guzzlehttp/guzzel => guzzlehttp/guzzle
monolog/monolog => monolog/monolog
swiftmailer/swiftmailer => tidak ada yang mirip

Mari kita bedah fungsinya:

  1. $jarakTerbaik diinisialisasi dengan $jarakMaks + 1 sehingga kandidat yang tidak mirip dengan apa pun mengembalikan null.
  2. levenshtein() + strtolower() membandingkan nama tanpa peduli kapitalisasi; jarak 1 berarti satu salah ketik — persis pola typosquatting.
  3. Loop memilih kandidat terdekat, dan hanya nama dengan jarak di bawah ambang yang dianggap “mencurigakan mirip”.

Hasilnya tampak pada output: guzzlehttp/guzzel (jarak 1) dilaporkan mirip dengan guzzlehttp/guzzle, monolog/monolog cocok persis dengan dirinya, sementara swiftmailer/swiftmailer terlalu jauh dari semua nama resmi. Di proyek nyata, daftar $namaResmi bisa diambil dari daftar dependensi resmi tim Anda, dan pemeriksaan ini dijalankan terhadap setiap paket baru yang masuk ke composer.json.

Mengevaluasi Paket Sebelum Diadopsi #

Sebelum menambah dependensi baru, luangkan beberapa menit untuk memeriksa sinyal kesehatannya. Daftar berikut adalah pertanyaan yang jawabannya bisa Anda temukan dalam lima menit di halaman repositori dan Packagist:

SinyalPertanyaan kunciAmbang waspada
Rilis terakhirkapan versi terbaru dirilis?lebih dari setahun tanpa rilis
Kontributor aktifberapa orang yang benar-benar merawat?satu orang saja, tanpa penerus
Issue dan pull requestapakah laporan ditanggapi pengelola?rasio issue terbuka terus membesar
Basis penggunaberapa unduhan per bulan?sangat kecil relatif ke klaim popularitasnya
Riwayat keamananpernah ada advisori? bagaimana penanganannya?advisori ditambal lambat atau diabaikan

Sinyal-sinyal tersebut bisa dirangkum menjadi skor sederhana agar penilaian konsisten antar anggota tim:

php
<?php
function nilaiKelayakanPaket(array $sinyal): string
{
    $skor = 0;

    if ($sinyal["hariSejakRilisTerakhir"] > 365) {
        $skor += 2;
    }
    if ($sinyal["kontributorAktif"] === 0) {
        $skor += 3;
    }
    if ($sinyal["rasioIssueTerbuka"] > 0.5) {
        $skor += 1;
    }
    if ($sinyal["unduhanBulanan"] < 500) {
        $skor += 1;
    }

    return match (true) {
        $skor >= 5 => "hindari — cari alternatif",
        $skor >= 3 => "berisiko — periksa lebih dalam",
        default    => "layak dipertimbangkan",
    };
}

$paketA = ["hariSejakRilisTerakhir" => 720, "kontributorAktif" => 0, "rasioIssueTerbuka" => 0.8, "unduhanBulanan" => 120];
$paketB = ["hariSejakRilisTerakhir" => 14, "kontributorAktif" => 6, "rasioIssueTerbuka" => 0.1, "unduhanBulanan" => 900000];

echo "paketA: " . nilaiKelayakanPaket($paketA) . "\n";
echo "paketB: " . nilaiKelayakanPaket($paketB) . "\n";

Output yang Dihasilkan:

text
paketA: hindari — cari alternatif
paketB: layak dipertimbangkan

Bobot pada fungsi tersebut mencerminkan prioritas risiko: kontributor aktif nol diberi bobot terbesar (3) karena paket tanpa perawat adalah paket yang tidak akan menambal celah, sedangkan rilis stagnan diberi bobot 2, dan dua sinyal sisanya masing-masing 1. Hasilnya, paketA yang stagnan dua tahun tanpa perawat langsung jatuh ke kategori “hindari”, sementara paketB yang dirilis dua minggu lalu oleh enam kontributor lolos sebagai kandidat sehat. Angka pastinya boleh disesuaikan kebutuhan tim — yang penting adalah keputusan adopsi paket berpijak pada kriteria yang sama untuk semua paket.

Prinsip Dependensi Minimal #

Setiap dependensi menambah dua hal sekaligus: permukaan serangan yang harus diwaspadai dan beban maintenance yang harus dibayar terus-menerus. Sebelum memasang paket baru, biasakan bertanya tiga hal:

  • Apakah fungsinya benar-benar di luar kompetensi inti aplikasi, atau cukup 20 baris kode yang bisa kami tulis dan uji sendiri?
  • Apakah paket yang sudah terpasang sudah menyediakan fungsi tersebut sebagai fitur tambahan?
  • Apakah paket ini membawa dependensi transitif tambahan, dan berapa banyak?

Jawaban dari ketiganya sering menunjukkan bahwa dependensi baru sebenarnya tidak dibutuhkan. Sebuah library kecil yang Anda tulis sendiri mungkin lebih aman daripada paket 40 KB yang menyeret 8 dependensi transitif — terutama untuk kebutuhan yang jarang berubah seperti formatting tanggal atau validasi sederhana.

Lock File dan Audit di CI #

Dua kebiasaan operasional menjaga dependensi tetap terkendali. Pertama, komit composer.lock ke repositori: berkas tersebut mengunci versi pasti setiap paket (termasuk dependensi transitifnya), sehingga mesin CI dan seluruh anggota tim memasang versi yang identik dengan produksi. Kedua, jalankan audit secara rutin — bukan hanya saat ingat. Simpan definisi berikut sebagai berkas .github/workflows/audit.yml:

yaml
name: Audit dependensi
on: [push, schedule]
jobs:
  audit:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: composer install --prefer-dist --no-progress
      - run: composer audit

Dua pemicu pada on bukan tanpa makna: push menahan pull request yang membawa paket bermasalah, sedangkan schedule menjalankan audit berkala sehingga advisori keamanan yang baru terbit pada dependensi lama tetap terdeteksi tanpa ada perubahan kode. Detail penggunaan composer audit, pembacaan hasilnya, dan penanganan advisori dibahas pada chapter dependency auditing .

Cermin Privat untuk Paket Internal #

Tim yang membangun paket internal menghadapi dilema: paket itu perlu diinstal Composer, tetapi tidak boleh dipublikasikan ke Packagist. Solusinya adalah cermin privat (private mirror) — registry Composer yang dijalankan sendiri, seperti Private Packagist, Satis, atau proxy internal. Aplikasi kemudian dikonfigurasi untuk menarik dari cermin tersebut:

json
{
    "repositories": [
        {
            "type": "composer",
            "url": "https://packages.tokobudi.example"
        },
        {
            "packagist.org": false
        }
    ]
}

Baris "packagist.org": false adalah bagian penting dari sisi keamanan: baris tersebut mematikan Packagist sebagai sumber paket, sehingga Composer hanya pernah melihat paket internal dari cermin privat Anda. Tanpa pemadaman itu, skenario dependency confusion terbuka — ketika nama paket internal ternyata juga dipakai seseorang di Packagist, Composer bisa memilih versi publik yang tidak Anda kendalikan. Dengan cermin yang mengelola daftar paket sendiri, resolusi dependensi sepenuhnya berada dalam kendali tim.

Ringkasan #

Keamanan dependensi adalah disiplin mengecilkan kepercayaan yang Anda berikan kepada pihak ketiga: menilai kesehatan paket sebelum mengadopsinya, memilih nama paket dari sumber resmi untuk menutup celah typosquatting, menjaga jumlah dependensi seminimal mungkin, mengunci versi lewat composer.lock, dan menjalankan audit di CI termasuk secara terjadwal. Untuk paket internal, cermin privat dengan Packagist yang dimatikan menutup pintu dependency confusion. Dengan semua kebiasaan keamanan dari seluruh section ini — dari validasi input hingga pengelolaan secret dan dependensi — kita siap merangkumnya menjadi satu pemeriksaan terstruktur pada chapter penutup: Security Checklist .

Referensi #