Skip to content

fix: gambar logo desa pada website desa aktif#1038

Merged
affandii06 merged 7 commits into
rilis-devfrom
fix/gambar_desa_aktif
May 20, 2026
Merged

fix: gambar logo desa pada website desa aktif#1038
affandii06 merged 7 commits into
rilis-devfrom
fix/gambar_desa_aktif

Conversation

@pandigresik
Copy link
Copy Markdown
Contributor

@pandigresik pandigresik commented May 7, 2026

PR Description: Fix Gambar Desa Aktif

Deskripsi Singkat

Memperbaiki masalah Content Security Policy (CSP) yang memblokir loading gambar logo desa dari website eksternal pada halaman properti desa aktif. Solusi menggunakan image proxy server-side untuk menyajikan gambar eksternal melalui domain sendiri, menghindari pembatasan CSP.

Depedency

https://github.com/OpenSID/API-Database-Gabungan/pull/400

Perubahan yang Dilakukan

Berdasarkan analisis git diff dari branch rilis-dev ke fix/gambar_desa_aktif, perubahan meliputi:

1. File Baru: app/Http/Controllers/ImageProxyController.php

  • Controller baru untuk proxy gambar eksternal
  • Fitur: validasi URL HTTPS, caching gambar selama 1 jam, logging komprehensif, pengecekan tipe konten gambar
  • Method: proxy(Request $request): Response dengan type hints dan docblocks

2. File Baru: tests/Feature/Http/Controllers/ImageProxyControllerTest.php

  • Test suite lengkap untuk ImageProxyController
  • Test cases: invalid URL, non-image content, successful fetch, cache serving, failed requests, exceptions
  • Menggunakan Http::fake() dan Log::spy() untuk mocking

3. Update: routes/web.php

  • Menambahkan import ImageProxyController
  • Menambahkan route: Route::get('/image-proxy', [ImageProxyController::class, 'proxy'])->name('image.proxy');

4. Update: app/Policies/CustomCSPPolicy.php

  • Menambahkan Keyword::SELF ke directive Directive::IMG untuk mengizinkan gambar dari domain sendiri
  • Sebelum: ['data:', 'https://tile.openstreetmap.org/']
  • Sesudah: [Keyword::SELF, 'data:', 'https://tile.openstreetmap.org/']

5. Update: resources/views/web/partials/property.blade.php

  • Mengubah atribut src pada elemen gambar logo desa
  • Dari: src="{{ asset('web/img/keluarahan-1.jpg') }}"
  • Ke: src="/image-proxy?url={{ urlencode(item.attributes.logo) }}" (jika logo ada)
  • Menggunakan JavaScript untuk update src dengan proxy URL

Alasan Perubahan

  • Masalah CSP: Policy CSP saat ini hanya mengizinkan gambar dari data: dan tile.openstreetmap.org, sehingga gambar logo dari API eksternal (berbagai domain) diblokir browser
  • Solusi Proxy: Membuat endpoint proxy yang mengambil gambar eksternal, menyimpannya di cache, dan menyajikan dari domain sendiri, sehingga sesuai dengan CSP self
  • Keamanan: Proxy hanya mengizinkan HTTPS, validasi tipe konten gambar, dan logging untuk monitoring
  • Performa: Caching mengurangi request eksternal berulang
  • Testing: Coverage test menyeluruh untuk memastikan reliability

Dampak Perubahan

  • Positif: Gambar logo desa aktif akan tampil tanpa error CSP
  • Netral: Sedikit overhead pada request gambar pertama (fetch eksternal), tapi di-cache selanjutnya
  • Risiko: Jika proxy gagal, gambar tidak tampil (fallback ke gambar default sudah ada)
  • Kompatibilitas: Tidak mengubah API atau behavior existing, hanya menambahkan endpoint baru

Steps to Reproduce (Original Issue)

  1. Akses halaman properti desa aktif
  2. Jika ada desa dengan logo dari domain eksternal
  3. Browser console akan menampilkan error CSP: "Refused to load the image because it violates the following Content Security Policy directive"
  4. Gambar logo tidak muncul

Testing Checklist

  • Jalankan test unit: php artisan test tests/Feature/Http/Controllers/ImageProxyControllerTest.php
  • Test manual: Akses halaman properti desa, pastikan gambar logo tampil
  • Test CSP: Periksa console browser tidak ada error CSP untuk gambar
  • Test cache: Request gambar yang sama, pastikan served from cache (cek log)
  • Test invalid URL: /image-proxy?url=invalid return 400
  • Test non-image: URL yang return HTML return 400
  • Test failed fetch: URL yang return 404 return 404
  • Test exception: Simulasi network error return 500
  • Cek log: Pastikan logging bekerja untuk debugging (storage/logs/)

Related Issue

Screenshot

simplescreenrecorder-2026-05-07_14.32.30.mp4

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 7, 2026

🔄 AI PR Review sedang antri di server...

Proses review akan segera dimulai di background — hasil akan muncul sebagai komentar setelah selesai.
Powered by CrewAI · PR #1038

@pandigresik pandigresik requested a review from affandii06 May 7, 2026 07:35
@devopsopendesa
Copy link
Copy Markdown

🔒 Security Review

Total Temuan: 3 isu (1 Critical, 1 High, 1 Medium)

Severity File Baris Isu
🚨 CRITICAL app/Http/Controllers/ImageProxyController.php 38 Server-Side Request Forgery (SSRF) / open proxy — external URLs allowed without host restrictions
⚠️ HIGH routes/web.php 4 Public route registered for proxy without rate-limiting/throttling middleware (abuse/DoS risk)
⚠️ MEDIUM app/Http/Controllers/ImageProxyController.php 52 Caching full binary response without size checks (resource exhaustion / abuse)

Detail lengkap dan cara reproduksi tersedia sebagai inline comment pada setiap baris.

@devopsopendesa
Copy link
Copy Markdown

📍 app/Http/Controllers/ImageProxyController.php LINE: 38 (baris 38)

[CRITICAL] 🔒 Security: Server-Side Request Forgery (open proxy / SSRF)

Masalah: Controller menerima arbitrary URL parameters and performs an outbound request to that URL via Http::timeout(10)->get($url) without enforcing a host whitelist or preventing requests to internal/private IP ranges. Itu membuka endpoint /image-proxy sebagai open proxy dan memungkinkan SSRF — misalnya attacker bisa meminta http://127.0.0.1:8080/ atau internal metadata endpoints, atau layanan internal lain yang hanya dapat diakses dari server.

Kode: Http::timeout(10)->get($url);

Risiko:

  • Dapat di-exploit untuk mengakses layanan internal (SSRF), mengambil data sensitif dari metadata services, mengakses internal admin interfaces, atau memindai internal network.
  • Jika upstream returns data not intended for public, attacker can exfiltrate it via the proxy.
  • Potensi remote code/data exposure or pivots inside the network.

PoC (Chrome Console):

// Jalankan di Chrome DevTools Console (F12 → Console)
// Contoh PoC: meminta layanan internal yang biasanya tidak terekspos.
// Ganti origin jika aplikasi tidak berjalan di / (tidak perlu auth for this route per PR)
(async () => {
  // Target internal URL commonly used in PoC (example: local metadata or internal admin port)
  const target = 'http://127.0.0.1:80/'; // coba nomor port sesuai lingkungan internal target
  // Note: the proxy expects a full URL; the code encodes it into the query param
  const resp = await fetch('/image-proxy?url=' + encodeURIComponent(target), {
    method: 'GET',
    // intentionally no extra headers; route is public per routes/web.php
  });
  console.log('Status:', resp.status);
  const text = await resp.text();
  console.log('Response length:', text.length);
  console.log('Response snippet:', text.slice(0,200));
})();

Fix:

<?php
// contoh perbaikan: validasi host/whitelist dan blok private IPs sebelum melakukan Http::get
// Tambahkan ini sebelum melakukan Http::get($url)
$parsed = parse_url($url);
$host = $parsed['host'] ?? null;
if (! $host) {
    abort(400, 'Invalid host');
}

// 1) resolve host and ensure not a private/reserved IP
$ip = gethostbyname($host);
if ($ip === $host) {
    // resolution failed, treat as invalid
    abort(400, 'Unable to resolve host');
}
// block private/reserved ranges
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) === false) {
    abort(400, 'Disallowed host');
}

// 2) optional: enforce allowlist of domains (preferred)
$allowed = ['example-trusted-domain.com', 'tile.openstreetmap.org'];
$allowedMatched = false;
foreach ($allowed as $a) {
    if (str_ends_with($host, $a)) {
        $allowedMatched = true;
        break;
    }
}
if (! $allowedMatched) {
    abort(403, 'Host not allowed');
}

// only after these checks, perform Http::get($url)
$response = Http::timeout(10)->get($url);

(Implement above logic in the controller before performing the external request; add configuration-driven allowlist and explicit rejection of private IP ranges.)

@devopsopendesa
Copy link
Copy Markdown

📍 routes/web.php LINE: 4 (baris 4)

[HIGH] 🔒 Security: Public route registered without rate limiting / throttling

Masalah: Route ditambahkan publikally:
Kode: Route::get('/image-proxy', [ImageProxyController::class, 'proxy'])->name('image.proxy');

Route ini tidak dilindungi oleh middleware throttle/ratelimit atau authentication. Mengizinkan publik untuk melakukan banyak permintaan ke proxy (yang sendiri melakukan outbound requests) membuka vektor abuse: mass scanning, request flooding yang dapat menyebabkan outbound bandwidth abuse, DOS pada target atau server (resource exhaustion). Karena controller melakukan blocking Http::get dan caching full responses, attacker bisa submit banyak unique URLs dan exhaust server resources.

Risiko: Denial of Service (DoS) atau abuse to fetch arbitrary external resources; potential for being used as anonymized proxy for fetching content.

PoC (Chrome Console):

// Jalankan di Chrome DevTools Console (F12 → Console)
// Simpel script untuk mengirim banyak permintaan ke endpoint publik (shows lack of throttling)
(async () => {
  const url = '/image-proxy?url=' + encodeURIComponent('https://example.com/image.jpg');
  for (let i=0;i<50;i++) {
    fetch(url).then(r => console.log('i',i,'status', r.status)).catch(e=>console.warn('err',e));
  }
})();

Fix:

  • Apply Laravel throttle middleware or custom rate-limiter to the route. Example:
// routes/web.php
Route::middleware(['throttle:30,1']) // 30 requests per minute per IP
     ->get('/image-proxy', [ImageProxyController::class, 'proxy'])
     ->name('image.proxy');
  • Consider stricter limits or authenticated access (e.g., require API key) for heavier usage. Use Laravel's RateLimiter and per-user or per-IP quotas.

@devopsopendesa
Copy link
Copy Markdown

📍 app/Http/Controllers/ImageProxyController.php LINE: 52 (baris 52)

[MEDIUM] 🔒 Security: Caching full image binary content without size checks (resource exhaustion / storage abuse)

Masalah: Controller stores response body into cache (Cache::put) for 3600 seconds without checking content length or imposing a max size. Potensi attacker bisa request a very large resource (huge image or other large file with image content-type) and cause cache/store to bloat or fill memory/disk/quota.

Kode: Cache::put($cacheKey, ['content' => $content, 'content_type' => $contentType], 3600);

Risiko:

  • Exhaust cache memory/disk quota, causing application instability or DoS.
  • If cache store is Redis with limited memory, attacker can evict other keys or crash instance.
  • Storing unbounded binary data in typical cache backends is unsafe.

PoC (Chrome Console):

// Jalankan di Chrome DevTools Console (F12 → Console)
// PoC: trigger storing a large payload via proxy (if an attacker controls the source URL)
(async () => {
  // Suppose attacker hosts a very large "image" at https://attacker.example/huge.jpg
  const target = 'https://attacker.example/huge.jpg';
  const resp = await fetch('/image-proxy?url=' + encodeURIComponent(target), { method: 'GET' });
  console.log('Status:', resp.status, 'Length header:', resp.headers.get('Content-Length'));
})();

Fix:

  • Enforce a maximum accepted size before caching (and optionally before fetching more than X bytes).
  • Use HEAD request or Content-Length header to pre-check size; if unknown, stream and abort after threshold.
  • Do not cache items larger than a configured maximum (e.g., 1 MB).
  • Example improvement:
// Before full get: try HEAD to check Content-Length
$head = Http::timeout(5)->head($url);
$length = $head->header('Content-Length');
$maxBytes = 1024 * 1024 * 2; // 2 MB
if ($length !== null && (int)$length > $maxBytes) {
    abort(413, 'Resource too large');
}

// When performing GET, consider streaming and limiting:
$response = Http::timeout(10)->withOptions(['stream' => true])->get($url);
// stream and accumulate up to $maxBytes; if exceeds, abort and do not cache.
  • Alternatively, avoid caching binaries in general cache store; store metadata only or use a filesystem/object storage with limits.

@devopsopendesa
Copy link
Copy Markdown

⚡ Performance Review

Total Temuan: 2 isu (0 Critical, 2 High)

Severity File Baris Isu Estimasi Dampak
⚠️ HIGH app/Http/Controllers/ImageProxyController.php 49-63 Caching + loading full response body (memory / cache exhaustion risk) Potential large memory and cache store pressure; if abused can cause OOM or evictions; +multiple MB per request; high traffic → service degradation
⚠️ HIGH routes/web.php 7 Public route added with no rate limiting / throttle Open proxy can be abused to fetch many large images; increased backend traffic and external requests leading to bandwidth/cost/latency issues

Detail lengkap tersedia sebagai inline comment pada setiap baris.

abort(404, 'Image not found');
}

$content = $response->body();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[HIGH] ⚡ Performance: Loading full response body into memory and caching binary blobs
Masalah: Controller memanggil dan menyimpan seluruh body response ke memori sebelum mengirim dan menyimpan ke cache. Kode-kode berurutan terkait ini adalah titik masalah: heavy download + in-memory caching untuk setiap request. Saat banyak permintaan atau file besar, ini dapat menyebabkan penggunaan memori tinggi dan tekanan pada cache backend (terutama Redis/memcached), mengakibatkan OOM, evictions, atau degradasi performa.
Kode: $content = $response->body();
Dampak: Untuk image besar (mis. beberapa MB) setiap permintaan non-cached akan mengalokasikan memori sebesar ukuran image. Pada traffic tinggi (ribuan permintaan/menit) atau images besar, server dapat kehabisan memori atau cache penuh. Estimasi: +several MB per concurrent request; 1000 concurrent x 2MB = ~2GB tambahan memory usage. Pada Redis cache, banyak item binary besar memakan memory dan menyebabkan evictions serta latency.
Fix: Hindari memuat seluruh body ke memori: gunakan streaming / temp file / filesystem cache, dan batasi ukuran yang di-cache. Contoh perbaikan (skeleton):

// gunakan stream untuk menghindari body() penuh
$response = Http::timeout(10)->withOptions(['stream' => true])->get($url);
if (!$response->successful()) { abort(404); }
// baca stream ke temp file atau langsung stream ke client
$stream = $response->getBody(); // PSR-7 stream
// jika ingin cache, simpan ke disk (storage) jika ukurannya < MAX_BYTES

Atau minimal batasi dan skip caching untuk response > MAX_SIZE:

$maxBytes = 1024 * 1024 * 2; // 2MB
$length = (int) $response->header('Content-Length', 0);
if ($length > $maxBytes) {
    // don't cache, stream directly
    return response()->stream(function() use ($stream) {
        while (!$stream->eof()) {
            echo $stream->read(1024 * 8);
        }
    }, 200, ['Content-Type' => $contentType]);
}
// else read body and cache safely (or store to disk)

}

// Cache selama 1 jam
Cache::put($cacheKey, ['content' => $content, 'content_type' => $contentType], 3600);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[HIGH] ⚡ Performance: Caching entire image binary into default cache store
Masalah: Kode menyimpan array berisi konten biner image langsung ke Cache::put — ini berisiko besar tergantung cache driver (Redis/memcached punya limit/per-item overhead). Menyimpan banyak atau besar blobs akan cepat menguras quota memori pada cache store dan memperlambat cache operations.
Kode: Cache::put($cacheKey, ['content' => $content, 'content_type' => $contentType], 3600);
Dampak: Pada produksi, gambar besar atau volume tinggi menyebabkan cache bloat → evictions → meningkatnya latency untuk cache operations; potensi out-of-memory pada Redis/memcached, atau penyimpanan file besar jika file cache digunakan. Estimasi dampak: jika 1000 unique large images di-cache, dan tiap image 1-5MB → 1-5GB cache consumption.
Fix: Jangan gunakan cache in-memory untuk blobs. Pilihan:

  • Simpan file di filesystem (Storage::disk('local')/S3) dan cache hanya metadata/path.
  • Batasi ukuran cacheable images (skip caching jika > threshold).
  • Gunakan streaming response and a blob store optimized for files.
    Contoh:
if ($response->header('Content-Length') <= $maxBytes) {
    $path = 'image_proxy/' . md5($url);
    Storage::disk('local')->put($path, $response->body());
    Cache::put($cacheKey, ['path' => $path, 'content_type' => $contentType], 3600);
    return response()->file(storage_path('app/' . $path))->header('Content-Type', $contentType);
}
// else stream without caching

Comment thread routes/web.php
use App\Http\Controllers\ImageProxyController;
use App\Http\Controllers\Web\ArtikelController;
use App\Http\Controllers\ApiProxyController;
use App\Http\Controllers\Auth\ChangePasswordController;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[HIGH] ⚡ Performance/Security: Public image-proxy route without rate limiting
Masalah: Route baru terdaftar publik: Route::get('/image-proxy', [ImageProxyController::class, 'proxy'])->name('image.proxy'); Tidak ada throttle/middleware pembatas. Karena controller melakukan external HTTP fetch dan (saat ini) caching, endpoint ini bisa disalahgunakan untuk melakukan banyak permintaan ke eksternal dan/atau mengisi cache/storage dengan file besar. Dampak performa: peningkatan latency, outbound bandwidth, dan beban pada server (I/O, CPU), serta biaya jika banyak outbound traffic.
Kode: Route::get('/image-proxy', [ImageProxyController::class, 'proxy'])->name('image.proxy');
Dampak: Abuse (bot) dapat menghasilkan ratusan/ ribuan fetch per menit; tiap fetch memicu HTTP out, memori, dan mungkin cache writes → service degradation. Estimasi: without throttle, a single small bot could generate hundreds req/s causing CPU/IO spike and bandwidth surge.
Fix: Tambahkan throttle middleware dan/atau auth and stricter validation/whitelisting. Contoh:

Route::get('/image-proxy', [ImageProxyController::class, 'proxy'])
    ->middleware('throttle:30,1') // 30 requests per minute per IP
    ->name('image.proxy');

Selain itu, implementasikan domain allowlist inside controller and reject non-whitelisted hosts early. Combine with monitoring/alerting on outbound request rates.

@devopsopendesa
Copy link
Copy Markdown

📝 Code Quality Review

Total Temuan: 5 isu (0 Critical, 5 High)

Severity Kategori File Baris Isu
⚠️ HIGH Architecture / Security app/Http/Controllers/ImageProxyController.php L12 Open proxy / SSRF risk: no host allowlist or domain restriction
⚠️ HIGH PHP Quality app/Http/Controllers/ImageProxyController.php L8 Validation done inline instead of FormRequest (missing proper Request validation)
⚠️ HIGH Architecture / Maintainability app/Http/Controllers/ImageProxyController.php L20 Fat Controller: fetching/caching business logic lives in controller instead of a service/action
⚠️ HIGH PHP Quality / Reliability app/Http/Controllers/ImageProxyController.php L16 No exception handling around external HTTP call (Http::timeout(10)->get($url)) — uncontrolled exceptions and poor error mapping
⚠️ HIGH Architecture / Operations routes/web.php L4 Public route added without throttling/middleware — risk of abuse/DoS

Detail lengkap tersedia sebagai inline comment pada setiap baris.

@devopsopendesa
Copy link
Copy Markdown

🐛 Bug Detection Review

Total Temuan: 3 isu (1 Critical, 2 High)

Severity File Baris Bug Skenario
🚨 CRITICAL app/Http/Controllers/ImageProxyController.php 30 SSRF / Unvalidated external request Mengirim URL user langsung ke Http::get tanpa whitelist / host/IP checks (SSRF risk)
⚠️ HIGH app/Http/Controllers/ImageProxyController.php 45 Cache of full binary payload Men-cache seluruh isi biner (gambar) tanpa batas ukuran → risiko resource exhaustion / OOM
⚠️ HIGH routes/web.php 8 Open public route without rate limiting Menambahkan route publik untuk proxy tanpa throttle/auth → abuse / DoS risk

Detail skenario dan fix tersedia sebagai inline comment pada setiap baris.


// Cache key berdasarkan URL
$cacheKey = 'image_proxy_' . md5($url);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[CRITICAL] 🐛 Bug: SSRF — external URL fetched without host/IP whitelist or SSRF protections

Kode: $response = Http::timeout(10)->get($url);

Skenario: User mengirimkan URL ke /image-proxy. Kode hanya memvalidasi bahwa stringnya adalah sebuah URL (filter_var) lalu langsung melakukan Http::get($url). Tanpa pembatasan host/whitelist atau pemeriksaan alamat IP, attacker bisa mengarahkan request ke internal services (http://127.0.0.1:8080, http://169.254.169.254, internal intranet) atau ke layanan internal lain — SSRF. Ini dapat mengakibatkan kebocoran data internal, pemicu aksi internal, atau akses ke metadata endpoints (cloud metadata).

Dampak: Eksekusi request terhadap host internal, kebocoran kredensial/metadata, remote port scanning, dan potensi compromise. Production dapat menjadi target SSRF besar-besaran melalui endpoint publik ini.

Fix: Terapkan whitelist domain/host atau penolakan terhadap alamat IP privat/loopback sebelum memanggil Http::get. Contoh implementasi (masukkan ke sebelum baris yang mengeksekusi Http::get):

// parse host and enforce allowlist
$host = parse_url($url, PHP_URL_HOST) ?? '';
$allowedHosts = config('image_proxy.allowed_hosts', []); // e.g., ['example.com']
if (! in_array($host, $allowedHosts, true)) {
    Log::warning('ImageProxy: host not allowed', ['url' => $url, 'host' => $host]);
    abort(400, 'Host not allowed');
}

// additionally block private IPs (resolve and check)
$ips = gethostbynamel($host) ?: [];
foreach ($ips as $ip) {
    if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) === false) {
        Log::warning('ImageProxy: resolved to private/reserved IP', ['url' => $url, 'ip' => $ip]);
        abort(400, 'Host resolves to private or reserved IP');
    }
}

$response = Http::timeout(10)->get($url);

Catatan:

  • Tambahkan konfigurasi allowlist di config/image_proxy.php dengan key allowed_hosts.
  • Atau gunakan denylist untuk private/reserved IPs + allowlist, tergantung kebijakan.
  • Ini mengurangi SSRF attack surface secara signifikan.

Log::warning('ImageProxyController: External image request failed', [
'url' => $url,
'status' => $response->status()
]);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[HIGH] 🐛 Bug: Caching full binary response into cache store without size checks

Kode: Cache::put($cacheKey, ['content' => $content, 'content_type' => $contentType], 3600);

Skenario: Controller menyimpan isi respon (bisa berupa gambar besar) langsung ke cache store (file/redis/memcached). Jika attacker atau user mengarahkan proxy ke resource besar (multi-MB atau tens/hundreds MB), cache dapat cepat menghabiskan storage/memory, menyebabkan OOM atau degradasi performa. Selain itu, beberapa cache backends tidak cocok menyimpan binary besar.

Dampak: Exhaustion sumber daya (RAM/disk), service degradation, potensi crash atau slowdowns di production.

Fix: Batasi ukuran yang dicache; jangan cache payload yang melebihi ambang; gunakan storage yang sesuai untuk binary (filesystem / object store) atau skip caching. Contoh implementasi:

$maxCacheBytes = config('image_proxy.max_cache_bytes', 1024 * 1024); // default 1MB
$bytes = strlen($content);

if ($bytes <= $maxCacheBytes) {
    Cache::put($cacheKey, ['content' => $content, 'content_type' => $contentType], 3600);
    Log::info('ImageProxy: cached image', ['url' => $url, 'key' => $cacheKey, 'bytes' => $bytes]);
} else {
    Log::warning('ImageProxy: skipping cache, payload too large', ['url' => $url, 'bytes' => $bytes, 'max' => $maxCacheBytes]);
}

Tambahan rekomendasi:

  • Pertimbangkan menyimpan binary besar di filesystem atau object storage (S3) dan cache hanya pointer/metadata.
  • Tambahkan antrian background untuk memproses/cacheing jika perlu, bukan synchronous Cache::put pada request jalan utama.

Comment thread routes/web.php
use App\Http\Controllers\ImageProxyController;
use App\Http\Controllers\Web\ArtikelController;
use App\Http\Controllers\ApiProxyController;
use App\Http\Controllers\Auth\ChangePasswordController;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[HIGH] 🐛 Bug: Public route for image proxy added without rate-limiting/throttling or auth

Kode: Route::get('/image-proxy', [ImageProxyController::class, 'proxy'])->name('image.proxy');

Skenario: Route baru adalah endpoint publik. Bersama dengan kemampuan proxying, attacker dapat melakukan mass-abuse (flood requests), membuat server melakukan banyak outbound requests, atau gunakan endpoint untuk large downloads causing bandwidth/cost/resource exhaustion. Karena tidak ada throttle middleware atau auth, endpoint sangat mudah disalahgunakan.

Dampak: Denial of Service (lokal atau upstream), increased outbound bandwidth and costs, exploitation for SSRF scans.

Fix: Tambahkan throttle middleware (atau authentication/authorization), misal Laravel throttle, atau batasi pada route group. Contoh perbaikan minimal (tambahkan middleware throttle):

Route::get('/image-proxy', [ImageProxyController::class, 'proxy'])
    ->middleware('throttle:60,1') // max 60 requests per minute per IP
    ->name('image.proxy');

Atau jika endpoint hanya untuk internal/frontend usage, pertimbangkan:

  • Mengamankan dengan auth middleware (auth:sanctum / web).
  • Menambahkan custom middleware yang memeriksa referer/CSRF token dan domain allowlist.

@devopsopendesa
Copy link
Copy Markdown

🤖 AI Code Review — Selesai

📋 Ringkasan Semua Review

Agent Temuan Inline Comments
📊 Full-Stack Security Specialist (PHP + JavaScript) 3 ✅ 3 posted
📊 Full-Stack Performance Analyst 3 ✅ 3 posted
📊 Full-Stack Code Quality & Architecture Reviewer 0 ✅ Clean
📊 Full-Stack Logic Bug Hunter (PHP + JavaScript) 3 ✅ 3 posted

Total inline comments: 9
Setiap agent sudah mem-posting summary dan inline comment masing-masing di atas.

@affandii06 affandii06 merged commit 0300c3a into rilis-dev May 20, 2026
@affandii06 affandii06 deleted the fix/gambar_desa_aktif branch May 20, 2026 03:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants