Pedoman Keamanan
Ikhtisar
Section titled “Ikhtisar”Dokumen ini menguraikan praktik terbaik keamanan untuk pengembangan XOOPS, yang mencakup validasi masukan, pengkodean keluaran, autentikasi, otorisasi, dan perlindungan terhadap kerentanan web umum.
Prinsip Keamanan
Section titled “Prinsip Keamanan”flowchart TB subgraph "Defense in Depth" A[Input Validation] --> B[Authentication] B --> C[Authorization] C --> D[Data Sanitization] D --> E[Output Encoding] E --> F[Audit Logging] endValidasi Masukan
Section titled “Validasi Masukan”Minta Sanitasi
Section titled “Minta Sanitasi”use Xoops\Core\Request;
// Always use typed getters$id = Request::getInt('id', 0, 'GET');$name = Request::getString('name', '', 'POST');$email = Request::getEmail('email', '', 'POST');$url = Request::getUrl('website', '', 'POST');
// Never use raw $_GET/$_POST/$_REQUEST// Bad: $id = $_GET['id'];// Good: $id = Request::getInt('id', 0, 'GET');Aturan Validasi
Section titled “Aturan Validasi”// Validate before useif ($id <= 0) { throw new InvalidArgumentException('Invalid ID');}
if (!preg_match('/^[a-zA-Z0-9_]{3,50}$/', $username)) { throw new InvalidArgumentException('Invalid username format');}
// Use whitelist validation for enums$allowedStatuses = ['draft', 'published', 'archived'];if (!in_array($status, $allowedStatuses, true)) { throw new InvalidArgumentException('Invalid status');}SQL Pencegahan Injeksi
Section titled “SQL Pencegahan Injeksi”Gunakan Kueri yang Diparameterisasi
Section titled “Gunakan Kueri yang Diparameterisasi”// GOOD: Parameterized query$sql = "SELECT * FROM {$xoopsDB->prefix('users')} WHERE uid = ?";$result = $xoopsDB->query($sql, [$userId]);
// BAD: String concatenation (vulnerable!)// $sql = "SELECT * FROM users WHERE uid = " . $userId;Menggunakan Objek Kriteria
Section titled “Menggunakan Objek Kriteria”use Criteria;use CriteriaCompo;
$criteria = new CriteriaCompo();$criteria->add(new Criteria('status', 'published'));$criteria->add(new Criteria('uid', $userId, '='));$criteria->add(new Criteria('created', time() - 86400, '>'));
$articles = $articleHandler->getObjects($criteria);Pencegahan XSS
Section titled “Pencegahan XSS”Pengkodean Keluaran
Section titled “Pengkodean Keluaran”use Xoops\Core\Text\Sanitizer;
// HTML context$safeName = htmlspecialchars($userName, ENT_QUOTES, 'UTF-8');
// In templates (auto-escaped){$userName|escape}
// For rich content$sanitizer = Sanitizer::getInstance();$safeContent = $sanitizer->sanitizeForDisplay($content);Kebijakan Keamanan Konten
Section titled “Kebijakan Keamanan Konten”// Set CSP headersheader("Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'");Perlindungan CSRF
Section titled “Perlindungan CSRF”Implementasi Token
Section titled “Implementasi Token”// Generate tokenuse Xoops\Core\Security;
$token = Security::createToken();
// In formecho '<input type="hidden" name="XOOPS_TOKEN_REQUEST" value="' . $token . '">';
// Verify on submissionif (!Security::checkToken()) { die('Security token mismatch');}Menggunakan XoopsForm
Section titled “Menggunakan XoopsForm”// Automatically adds CSRF token$form = new XoopsThemeForm('Edit Article', 'articleform', 'save.php');$form->addElement(new XoopsFormHiddenToken());Otentikasi
Section titled “Otentikasi”Penanganan Kata Sandi
Section titled “Penanganan Kata Sandi”// Hash passwords (PHP 5.5+)$hashedPassword = password_hash($plainPassword, PASSWORD_ARGON2ID);
// Verify passwordsif (password_verify($plainPassword, $storedHash)) { // Password correct}
// Check if rehash neededif (password_needs_rehash($storedHash, PASSWORD_ARGON2ID)) { $newHash = password_hash($plainPassword, PASSWORD_ARGON2ID); // Update stored hash}Keamanan Sesi
Section titled “Keamanan Sesi”// Regenerate session ID after loginsession_regenerate_id(true);
// Set secure session cookie optionsini_set('session.cookie_httponly', 1);ini_set('session.cookie_secure', 1);ini_set('session.cookie_samesite', 'Lax');Otorisasi
Section titled “Otorisasi”Pemeriksaan Izin
Section titled “Pemeriksaan Izin”// Check module adminif (!$xoopsUser || !$xoopsUser->isAdmin($xoopsModule->mid())) { redirect_header('index.php', 3, 'Access denied');}
// Check group permissions$grouppermHandler = xoops_getHandler('groupperm');$groups = $xoopsUser ? $xoopsUser->getGroups() : [XOOPS_GROUP_ANONYMOUS];
if (!$grouppermHandler->checkRight('view_item', $itemId, $groups, $moduleId)) { throw new AccessDeniedException('Permission denied');}Akses Berbasis Peran
Section titled “Akses Berbasis Peran”class PermissionChecker{ public function canEdit(Article $article, ?XoopsUser $user): bool { if (!$user) { return false; }
// Admin can edit anything if ($user->isAdmin()) { return true; }
// Author can edit their own if ($article->getAuthorId() === $user->uid()) { return true; }
// Check editor permission return $this->hasPermission($user, 'article_edit'); }}Keamanan Pengunggahan File
Section titled “Keamanan Pengunggahan File”class SecureUploader{ private array $allowedMimeTypes = [ 'image/jpeg', 'image/png', 'image/gif' ];
private array $allowedExtensions = ['jpg', 'jpeg', 'png', 'gif'];
public function validate(array $file): bool { // Check file size if ($file['size'] > 2 * 1024 * 1024) { throw new FileTooLargeException(); }
// Verify MIME type $finfo = new finfo(FILEINFO_MIME_TYPE); $mimeType = $finfo->file($file['tmp_name']);
if (!in_array($mimeType, $this->allowedMimeTypes, true)) { throw new InvalidFileTypeException(); }
// Check extension $extension = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION)); if (!in_array($extension, $this->allowedExtensions, true)) { throw new InvalidFileTypeException(); }
// Generate safe filename return true; }
public function generateSafeFilename(string $original): string { $extension = strtolower(pathinfo($original, PATHINFO_EXTENSION)); return bin2hex(random_bytes(16)) . '.' . $extension; }}Pencatatan Audit
Section titled “Pencatatan Audit”class SecurityLogger{ public function logAuthAttempt(string $username, bool $success, string $ip): void { $data = [ 'username' => $username, 'success' => $success, 'ip' => $ip, 'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? '', 'timestamp' => time() ];
// Log to database or file $this->log('auth', $data); }
public function logSensitiveAction(int $userId, string $action, array $context): void { $data = [ 'user_id' => $userId, 'action' => $action, 'context' => json_encode($context), 'ip' => $_SERVER['REMOTE_ADDR'], 'timestamp' => time() ];
$this->log('audit', $data); }}Header Keamanan
Section titled “Header Keamanan”// Recommended security headersheader('X-Content-Type-Options: nosniff');header('X-Frame-Options: SAMEORIGIN');header('X-XSS-Protection: 1; mode=block');header('Referrer-Policy: strict-origin-when-cross-origin');header('Permissions-Policy: geolocation=(), microphone=(), camera=()');
// HSTS (only for HTTPS sites)if (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on') { header('Strict-Transport-Security: max-age=31536000; includeSubDomains');}Pembatasan Tarif
Section titled “Pembatasan Tarif”class RateLimiter{ public function check(string $key, int $maxAttempts, int $windowSeconds): bool { $cacheKey = 'rate_limit:' . $key; $attempts = (int) $this->cache->get($cacheKey, 0);
if ($attempts >= $maxAttempts) { return false; // Rate limited }
$this->cache->increment($cacheKey, 1, $windowSeconds); return true; }}
// Usage$limiter = new RateLimiter();if (!$limiter->check('login:' . $ip, 5, 300)) { throw new TooManyRequestsException('Too many login attempts');}Daftar Periksa Keamanan
Section titled “Daftar Periksa Keamanan”- Semua masukan pengguna divalidasi dan dibersihkan
- Kueri berparameter untuk semua operasi basis data
- Pengodean keluaran untuk semua konten buatan pengguna
- Token CSRF pada semua bentuk perubahan status
- Pencirian kata sandi yang aman (Argon2id)
- Keamanan sesi dikonfigurasi
- Validasi unggahan file
- Header keamanan disetel
- Pembatasan tarif diterapkan
- Pencatatan audit diaktifkan
- Pesan kesalahan tidak membocorkan informasi sensitif
Dokumentasi Terkait
Section titled “Dokumentasi Terkait”- Sistem Otentikasi
- Sistem Izin
- Validasi Masukan