Initial commit - CMS Gov Bapenda Garut dengan EditorJS
This commit is contained in:
0
app/Filters/.gitkeep
Normal file
0
app/Filters/.gitkeep
Normal file
40
app/Filters/AuthFilter.php
Normal file
40
app/Filters/AuthFilter.php
Normal file
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filters;
|
||||
|
||||
use CodeIgniter\Filters\FilterInterface;
|
||||
use CodeIgniter\HTTP\RequestInterface;
|
||||
use CodeIgniter\HTTP\ResponseInterface;
|
||||
|
||||
class AuthFilter implements FilterInterface
|
||||
{
|
||||
public function before(RequestInterface $request, $arguments = null)
|
||||
{
|
||||
// Check if user is logged in
|
||||
if (!session()->get('is_logged_in')) {
|
||||
return redirect()->to('/auth/login');
|
||||
}
|
||||
|
||||
// Check if user role is admin or editor
|
||||
$userRole = session()->get('role');
|
||||
if (!in_array($userRole, ['admin', 'editor'])) {
|
||||
session()->destroy();
|
||||
return redirect()->to('/auth/login')->with('error', 'Anda tidak memiliki akses ke sistem ini.');
|
||||
}
|
||||
|
||||
// If role arguments are provided, check user role
|
||||
if ($arguments !== null && !empty($arguments)) {
|
||||
if (!in_array($userRole, $arguments)) {
|
||||
return redirect()->to('/admin')->with('error', 'Anda tidak memiliki akses ke halaman ini.');
|
||||
}
|
||||
}
|
||||
|
||||
return $request;
|
||||
}
|
||||
|
||||
public function after(RequestInterface $request, ResponseInterface $response, $arguments = null)
|
||||
{
|
||||
// Do nothing
|
||||
}
|
||||
}
|
||||
|
||||
156
app/Filters/SecurityHeaders.php
Normal file
156
app/Filters/SecurityHeaders.php
Normal file
@@ -0,0 +1,156 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filters;
|
||||
|
||||
use CodeIgniter\Filters\FilterInterface;
|
||||
use CodeIgniter\HTTP\RequestInterface;
|
||||
use CodeIgniter\HTTP\ResponseInterface;
|
||||
|
||||
class SecurityHeaders implements FilterInterface
|
||||
{
|
||||
/**
|
||||
* Do whatever processing this filter needs to do.
|
||||
* By default it should not return anything during
|
||||
* normal execution. However, when an abnormal state
|
||||
* is found, it should return an instance of
|
||||
* CodeIgniter\HTTP\Response. If it does, script
|
||||
* execution will end and that Response will be
|
||||
* sent back to the client, allowing for error pages,
|
||||
* redirects, etc.
|
||||
*
|
||||
* @param RequestInterface $request
|
||||
* @param array|null $arguments
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function before(RequestInterface $request, $arguments = null)
|
||||
{
|
||||
// No action needed before request
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows After filters to inspect and modify the response
|
||||
* object as needed. This method does not allow any way
|
||||
* to stop execution of other after filters, short of
|
||||
* throwing an Exception or Error.
|
||||
*
|
||||
* @param RequestInterface $request
|
||||
* @param ResponseInterface $response
|
||||
* @param array|null $arguments
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function after(RequestInterface $request, ResponseInterface $response, $arguments = null)
|
||||
{
|
||||
$isProduction = ENVIRONMENT === 'production';
|
||||
|
||||
// ============================================================
|
||||
// BASIC SECURITY HEADERS
|
||||
// ============================================================
|
||||
|
||||
// X-Frame-Options: Mencegah clickjacking attacks
|
||||
// SAMEORIGIN = hanya allow framing dari same origin
|
||||
$response->setHeader('X-Frame-Options', 'SAMEORIGIN');
|
||||
|
||||
// X-Content-Type-Options: Mencegah MIME type sniffing
|
||||
// nosniff = browser tidak boleh menebak content type
|
||||
$response->setHeader('X-Content-Type-Options', 'nosniff');
|
||||
|
||||
// X-XSS-Protection: Legacy header untuk browser lama (optional)
|
||||
// Mode=block = block page jika XSS terdeteksi
|
||||
$response->setHeader('X-XSS-Protection', '1; mode=block');
|
||||
|
||||
// Referrer-Policy: Kontrol informasi referrer yang dikirim
|
||||
// strict-origin-when-cross-origin = kirim full URL untuk same-origin,
|
||||
// hanya origin untuk cross-origin HTTPS, tidak ada untuk HTTP
|
||||
$response->setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
|
||||
|
||||
// Permissions-Policy: Kontrol fitur browser yang bisa digunakan
|
||||
// Membatasi akses ke fitur seperti geolocation, camera, microphone, dll
|
||||
// Hanya gunakan feature yang didukung oleh browser modern
|
||||
$permissionsPolicy = [
|
||||
'geolocation=()', // Geolocation API
|
||||
'camera=()', // Camera access
|
||||
'microphone=()', // Microphone access
|
||||
'payment=()', // Payment Request API
|
||||
'usb=()', // WebUSB API
|
||||
'magnetometer=()', // Magnetometer sensor
|
||||
'gyroscope=()', // Gyroscope sensor
|
||||
'accelerometer=()', // Accelerometer sensor
|
||||
'ambient-light-sensor=()', // Ambient light sensor
|
||||
'autoplay=()', // Autoplay media
|
||||
'fullscreen=()', // Fullscreen API
|
||||
'picture-in-picture=()', // Picture-in-picture
|
||||
];
|
||||
$response->setHeader('Permissions-Policy', implode(', ', $permissionsPolicy));
|
||||
|
||||
// ============================================================
|
||||
// HSTS (HTTP Strict Transport Security)
|
||||
// ============================================================
|
||||
// Hanya aktif di production dengan HTTPS
|
||||
// max-age=31536000 = 1 tahun
|
||||
// includeSubDomains = berlaku untuk semua subdomain
|
||||
// preload = bisa ditambahkan ke HSTS preload list (optional)
|
||||
if ($isProduction) {
|
||||
$response->setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// CONTENT SECURITY POLICY (CSP)
|
||||
// ============================================================
|
||||
// CSP directives untuk mencegah XSS attacks
|
||||
// Konfigurasi disesuaikan untuk TailAdmin dan Alpine.js
|
||||
//
|
||||
// CATATAN: Alpine.js memerlukan 'unsafe-eval' untuk mengevaluasi
|
||||
// expression JavaScript (x-data, x-show, dll). Ini trade-off security
|
||||
// yang diperlukan untuk Alpine.js bekerja dengan baik.
|
||||
$cspDirectives = [
|
||||
"default-src 'self'", // Default: hanya dari same origin
|
||||
"script-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdn.jsdelivr.net", // Script: allow inline dan eval untuk Alpine.js
|
||||
"style-src 'self' 'unsafe-inline' https://fonts.googleapis.com", // Style: allow inline untuk Tailwind
|
||||
"font-src 'self' data: https://fonts.gstatic.com", // Font: allow data URI dan Google Fonts
|
||||
"img-src 'self' data: https:", // Image: allow data URI dan HTTPS
|
||||
"connect-src 'self'", // AJAX/Fetch: hanya same origin
|
||||
"frame-ancestors 'self'", // Frame: hanya same origin
|
||||
"base-uri 'self'", // Base URI: hanya same origin
|
||||
"form-action 'self'", // Form action: hanya same origin
|
||||
"object-src 'none'", // Object/embed: tidak ada
|
||||
];
|
||||
|
||||
// Hanya tambahkan upgrade-insecure-requests di production
|
||||
if ($isProduction) {
|
||||
$cspDirectives[] = "upgrade-insecure-requests";
|
||||
}
|
||||
|
||||
$cspValue = implode('; ', $cspDirectives);
|
||||
|
||||
if ($isProduction) {
|
||||
// Enforce CSP di production
|
||||
$response->setHeader('Content-Security-Policy', $cspValue);
|
||||
} else {
|
||||
// Report-Only di development untuk testing
|
||||
$response->setHeader('Content-Security-Policy-Report-Only', $cspValue);
|
||||
// Juga set regular CSP untuk security audit tools
|
||||
$response->setHeader('Content-Security-Policy', $cspValue);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// ADDITIONAL SECURITY HEADERS
|
||||
// ============================================================
|
||||
|
||||
// Cross-Origin-Embedder-Policy: Mencegah embedding dari cross-origin
|
||||
// require-corp = require Cross-Origin Resource Policy
|
||||
// $response->setHeader('Cross-Origin-Embedder-Policy', 'require-corp'); // Optional, bisa break beberapa fitur
|
||||
|
||||
// Cross-Origin-Opener-Policy: Isolasi browsing context
|
||||
// same-origin = hanya same-origin yang bisa access window
|
||||
// $response->setHeader('Cross-Origin-Opener-Policy', 'same-origin'); // Optional
|
||||
|
||||
// Cross-Origin-Resource-Policy: Kontrol resource sharing
|
||||
// same-origin = hanya same-origin yang bisa load resource
|
||||
// $response->setHeader('Cross-Origin-Resource-Policy', 'same-origin'); // Optional
|
||||
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
|
||||
95
app/Filters/ThrottleFilter.php
Normal file
95
app/Filters/ThrottleFilter.php
Normal file
@@ -0,0 +1,95 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filters;
|
||||
|
||||
use CodeIgniter\Filters\FilterInterface;
|
||||
use CodeIgniter\HTTP\RequestInterface;
|
||||
use CodeIgniter\HTTP\ResponseInterface;
|
||||
|
||||
class ThrottleFilter implements FilterInterface
|
||||
{
|
||||
/**
|
||||
* Rate limit configuration
|
||||
* Production: 10 requests per minute
|
||||
* Development: 30 requests per minute (still active for security testing)
|
||||
*/
|
||||
protected function getRateLimit(): int
|
||||
{
|
||||
return ENVIRONMENT === 'production' ? 10 : 30;
|
||||
}
|
||||
|
||||
protected function getWindowSeconds(): int
|
||||
{
|
||||
return 60; // 1 minute window
|
||||
}
|
||||
|
||||
/**
|
||||
* Do whatever processing this filter needs to do.
|
||||
*/
|
||||
public function before(RequestInterface $request, $arguments = null)
|
||||
{
|
||||
// Only throttle POST requests to login
|
||||
if (strtolower($request->getMethod()) !== 'post') {
|
||||
return;
|
||||
}
|
||||
|
||||
$ipAddress = $request->getIPAddress();
|
||||
$cache = \Config\Services::cache();
|
||||
|
||||
// Use a more specific key for login endpoint
|
||||
$path = $request->getUri()->getPath();
|
||||
$key = 'throttle_login_' . md5($ipAddress . '_' . $path);
|
||||
|
||||
$current = $cache->get($key);
|
||||
|
||||
// Log for debugging
|
||||
log_message('debug', "Throttle check - IP: {$ipAddress}, Path: {$path}, Current: " . ($current ?? 'null') . ", Limit: {$this->getRateLimit()}");
|
||||
|
||||
if ($current === null) {
|
||||
// First request - initialize counter
|
||||
$cache->save($key, 1, $this->getWindowSeconds());
|
||||
log_message('debug', "Throttle initialized for IP: {$ipAddress}");
|
||||
return;
|
||||
}
|
||||
|
||||
// Increment counter
|
||||
$newCount = $current + 1;
|
||||
$cache->save($key, $newCount, $this->getWindowSeconds());
|
||||
|
||||
log_message('debug', "Throttle incremented - IP: {$ipAddress}, Count: {$newCount}, Limit: {$this->getRateLimit()}");
|
||||
|
||||
// Check if limit exceeded (use >= instead of > to be more strict)
|
||||
if ($newCount >= $this->getRateLimit()) {
|
||||
log_message('warning', "Rate limit exceeded for IP: {$ipAddress} on path: {$path} - Count: {$newCount}, Limit: {$this->getRateLimit()}");
|
||||
|
||||
// Create response with 429 status
|
||||
$response = service('response');
|
||||
$response->setStatusCode(429);
|
||||
$response->setBody('Too Many Requests. Please try again later.');
|
||||
$response->setHeader('Retry-After', (string) $this->getWindowSeconds());
|
||||
$response->setHeader('X-RateLimit-Limit', (string) $this->getRateLimit());
|
||||
$response->setHeader('X-RateLimit-Remaining', '0');
|
||||
$response->setHeader('X-RateLimit-Reset', (string) (time() + $this->getWindowSeconds()));
|
||||
$response->setHeader('Content-Type', 'text/plain; charset=utf-8');
|
||||
$response->setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
// Set rate limit headers for successful requests
|
||||
$remaining = max(0, $this->getRateLimit() - $newCount);
|
||||
$response = service('response');
|
||||
$response->setHeader('X-RateLimit-Limit', (string) $this->getRateLimit());
|
||||
$response->setHeader('X-RateLimit-Remaining', (string) $remaining);
|
||||
$response->setHeader('X-RateLimit-Reset', (string) (time() + $this->getWindowSeconds()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows After filters to inspect and modify the response
|
||||
*/
|
||||
public function after(RequestInterface $request, ResponseInterface $response, $arguments = null)
|
||||
{
|
||||
// No action needed after request
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user