Keamanan Website PHP
Keamanan website adalah aspek yang sangat penting dalam pengembangan aplikasi web. Pelajari cara melindungi aplikasi PHP dari berbagai serangan dan vulnerability.
OWASP Top 10 Web Application Security Risks
OWASP (Open Web Application Security Project) merilis daftar 10 risiko keamanan teratas yang paling umum ditemukan dalam aplikasi web.
Rank | Risk | Description | Prevention |
---|---|---|---|
1 | Injection | SQL, NoSQL, OS command injection | Prepared statements, input validation |
2 | Broken Authentication | Weak authentication mechanisms | Strong password policies, MFA |
3 | Sensitive Data Exposure | Unprotected sensitive data | Encryption, HTTPS |
4 | XML External Entities (XXE) | XML processors vulnerabilities | Disable XML external entities |
5 | Broken Access Control | Improper authorization | Proper access controls, principle of least privilege |
Security Principles
- ๐ Defense in Depth: Multiple layers of security
- ๐ฏ Principle of Least Privilege: Minimal necessary access
- ๐ซ Fail Securely: Secure default behavior
- ๐ Never Trust User Input: Always validate and sanitize
- ๐ Security by Design: Build security from the start
- ๐ Keep Security Simple: Avoid complex security mechanisms
PHP Security Configuration
# php.ini security settings
# Disable dangerous functions
disable_functions = exec,passthru,shell_exec,system,proc_open,popen,curl_exec,curl_multi_exec,parse_ini_file,show_source
# Hide PHP version
expose_php = Off
# Disable remote file inclusion
allow_url_fopen = Off
allow_url_include = Off
# Session security
session.cookie_httponly = On
session.cookie_secure = On
session.use_strict_mode = On
# File upload restrictions
file_uploads = On
upload_max_filesize = 2M
max_file_uploads = 5
# Error reporting (production)
display_errors = Off
log_errors = On
error_log = /var/log/php_errors.log
SQL Injection Prevention
โ Vulnerable Code:
// JANGAN LAKUKAN INI!
$query = "SELECT * FROM users WHERE id = " . $_GET['id'];
$result = mysqli_query($connection, $query);
$email = $_POST['email'];
$query = "SELECT * FROM users WHERE email = '$email'";
// Attacker can input: ' OR '1'='1' --
โ Secure Code with Prepared Statements:
// PDO Prepared Statements
$stmt = $pdo->prepare("SELECT * FROM users WHERE id = ?");
$stmt->execute([$_GET['id']]);
// Named parameters
$stmt = $pdo->prepare("SELECT * FROM users WHERE email = :email");
$stmt->execute(['email' => $_POST['email']]);
Secure Database Class
<?php
class SecureDatabase {
private $pdo;
public function __construct($dsn, $username, $password) {
$options = [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
];
$this->pdo = new PDO($dsn, $username, $password, $options);
}
public function select($query, $params = []) {
$stmt = $this->pdo->prepare($query);
$stmt->execute($params);
return $stmt->fetchAll();
}
public function selectOne($query, $params = []) {
$stmt = $this->pdo->prepare($query);
$stmt->execute($params);
return $stmt->fetch();
}
public function insert($table, $data) {
$fields = array_keys($data);
$placeholders = ':' . implode(', :', $fields);
$query = "INSERT INTO {$table} (" . implode(', ', $fields) . ") VALUES ({$placeholders})";
$stmt = $this->pdo->prepare($query);
return $stmt->execute($data);
}
public function update($table, $data, $where, $whereParams = []) {
$fields = [];
foreach (array_keys($data) as $field) {
$fields[] = "{$field} = :{$field}";
}
$query = "UPDATE {$table} SET " . implode(', ', $fields) . " WHERE {$where}";
$stmt = $this->pdo->prepare($query);
return $stmt->execute(array_merge($data, $whereParams));
}
public function delete($table, $where, $params = []) {
$query = "DELETE FROM {$table} WHERE {$where}";
$stmt = $this->pdo->prepare($query);
return $stmt->execute($params);
}
}
// Usage
$db = new SecureDatabase($dsn, $username, $password);
// Safe operations
$users = $db->select("SELECT * FROM users WHERE age > ?", [18]);
$user = $db->selectOne("SELECT * FROM users WHERE id = ?", [$userId]);
$db->insert('users', ['name' => $name, 'email' => $email]);
$db->update('users', ['name' => $newName], 'id = :id', ['id' => $userId]);
?>
Input Validation
<?php
class InputValidator {
public static function validateId($id) {
if (!is_numeric($id) || $id <= 0) {
throw new InvalidArgumentException("Invalid ID");
}
return (int) $id;
}
public static function validateEmail($email) {
$email = filter_var($email, FILTER_SANITIZE_EMAIL);
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
throw new InvalidArgumentException("Invalid email format");
}
return $email;
}
public static function validateString($string, $minLength = 1, $maxLength = 255) {
$string = trim($string);
$length = strlen($string);
if ($length < $minLength || $length > $maxLength) {
throw new InvalidArgumentException("String length must be between {$minLength} and {$maxLength}");
}
return $string;
}
public static function validateUrl($url) {
if (!filter_var($url, FILTER_VALIDATE_URL)) {
throw new InvalidArgumentException("Invalid URL format");
}
return $url;
}
public static function sanitizeFilename($filename) {
// Remove any character that's not alphanumeric, underscore, dot, or dash
return preg_replace('/[^a-zA-Z0-9._-]/', '', $filename);
}
}
// Usage
try {
$userId = InputValidator::validateId($_GET['id']);
$email = InputValidator::validateEmail($_POST['email']);
$name = InputValidator::validateString($_POST['name'], 2, 50);
} catch (InvalidArgumentException $e) {
die("Invalid input: " . $e->getMessage());
}
?>
Cross-Site Scripting (XSS) Prevention
Types of XSS
Reflected XSS
Script dikirim melalui URL atau form dan langsung ditampilkan di halaman.
Stored XSS
Script disimpan di database dan ditampilkan setiap kali halaman dimuat.
DOM-based XSS
Script dieksekusi melalui manipulasi DOM di client-side.
โ Vulnerable Code:
// JANGAN LAKUKAN INI!
echo "Hello " . $_GET['name']; // XSS vulnerability
echo "<div>" . $_POST['comment'] . "</div>"; // Stored XSS
โ Secure Output Escaping:
// Safe output
echo "Hello " . htmlspecialchars($_GET['name'], ENT_QUOTES, 'UTF-8');
echo "<div>" . htmlspecialchars($_POST['comment'], ENT_QUOTES, 'UTF-8') . "</div>";
Output Escaping Functions
<?php
class OutputSanitizer {
/**
* Escape HTML content
*/
public static function html($string) {
return htmlspecialchars($string, ENT_QUOTES | ENT_HTML401, 'UTF-8');
}
/**
* Escape HTML attributes
*/
public static function attr($string) {
return htmlspecialchars($string, ENT_QUOTES | ENT_HTML401, 'UTF-8');
}
/**
* Escape JavaScript strings
*/
public static function js($string) {
return json_encode($string, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP);
}
/**
* Escape URL parameters
*/
public static function url($string) {
return urlencode($string);
}
/**
* Escape CSS values
*/
public static function css($string) {
return preg_replace('/[^a-zA-Z0-9\-_]/', '', $string);
}
/**
* Clean HTML content (allow safe tags only)
*/
public static function cleanHtml($string, $allowedTags = '<p><br><strong><em><u>') {
return strip_tags($string, $allowedTags);
}
}
// Usage in templates
?>
<!DOCTYPE html>
<html>
<head>
<title><?= OutputSanitizer::html($pageTitle) ?></title>
</head>
<body>
<h1><?= OutputSanitizer::html($userInput) ?></h1>
<input type="text" value="<?= OutputSanitizer::attr($userValue) ?>">
<script>
var userName = <?= OutputSanitizer::js($userName) ?>;
</script>
<a href="profile.php?id=<?= OutputSanitizer::url($userId) ?>">Profile</a>
</body>
</html>
Content Security Policy (CSP)
<?php
class CSPManager {
private $policies = [];
public function __construct() {
// Default secure policies
$this->policies = [
'default-src' => "'self'",
'script-src' => "'self'",
'style-src' => "'self' 'unsafe-inline'",
'img-src' => "'self' data: https:",
'font-src' => "'self'",
'connect-src' => "'self'",
'frame-ancestors' => "'none'",
'base-uri' => "'self'",
'form-action' => "'self'"
];
}
public function addPolicy($directive, $value) {
if (isset($this->policies[$directive])) {
$this->policies[$directive] .= ' ' . $value;
} else {
$this->policies[$directive] = $value;
}
}
public function allowInlineScripts($nonce = null) {
if ($nonce) {
$this->addPolicy('script-src', "'nonce-{$nonce}'");
} else {
$this->addPolicy('script-src', "'unsafe-inline'");
}
}
public function generateNonce() {
return base64_encode(random_bytes(16));
}
public function sendHeader() {
$policyString = '';
foreach ($this->policies as $directive => $value) {
$policyString .= "{$directive} {$value}; ";
}
header("Content-Security-Policy: " . trim($policyString));
}
}
// Usage
$csp = new CSPManager();
$csp->addPolicy('script-src', 'https://cdn.jsdelivr.net');
$csp->addPolicy('style-src', 'https://fonts.googleapis.com');
$nonce = $csp->generateNonce();
$csp->allowInlineScripts($nonce);
$csp->sendHeader();
?>
<script nonce="<?= $nonce ?>">
// This script will be allowed
console.log('Secure script execution');
</script>
HTML Purifier for Rich Content
<?php
// Install via Composer: composer require ezyang/htmlpurifier
require_once 'vendor/autoload.php';
class RichContentSanitizer {
private $purifier;
public function __construct() {
$config = HTMLPurifier_Config::createDefault();
// Allow specific HTML tags and attributes
$config->set('HTML.Allowed', 'p,br,strong,em,u,ol,ul,li,a[href],img[src|alt]');
// Set allowed protocols for links
$config->set('URI.AllowedSchemes', ['http' => true, 'https' => true, 'mailto' => true]);
// Disable external images for security
$config->set('URI.DisableExternalResources', true);
$this->purifier = new HTMLPurifier($config);
}
public function clean($html) {
return $this->purifier->purify($html);
}
}
// Usage
$sanitizer = new RichContentSanitizer();
$cleanContent = $sanitizer->clean($_POST['content']);
// Now safe to display
echo $cleanContent;
?>
Cross-Site Request Forgery (CSRF) Prevention
CSRF Token Implementation
<?php
class CSRFProtection {
private static $tokenName = 'csrf_token';
private static $sessionKey = 'csrf_tokens';
/**
* Generate a new CSRF token
*/
public static function generateToken($formName = 'default') {
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
$token = bin2hex(random_bytes(32));
// Store token in session
if (!isset($_SESSION[self::$sessionKey])) {
$_SESSION[self::$sessionKey] = [];
}
$_SESSION[self::$sessionKey][$formName] = $token;
return $token;
}
/**
* Validate CSRF token
*/
public static function validateToken($token, $formName = 'default') {
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
if (!isset($_SESSION[self::$sessionKey][$formName])) {
return false;
}
$isValid = hash_equals($_SESSION[self::$sessionKey][$formName], $token);
// Remove token after validation (one-time use)
unset($_SESSION[self::$sessionKey][$formName]);
return $isValid;
}
/**
* Get HTML input field for CSRF token
*/
public static function getTokenField($formName = 'default') {
$token = self::generateToken($formName);
return "<input type=\"hidden\" name=\"" . self::$tokenName . "\" value=\"{$token}\">";
}
/**
* Middleware to check CSRF token
*/
public static function middleware($formName = 'default') {
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$token = $_POST[self::$tokenName] ?? '';
if (!self::validateToken($token, $formName)) {
http_response_code(403);
die('CSRF token validation failed');
}
}
}
}
?>
Form Implementation
<!-- Form with CSRF protection -->
<form method="POST" action="process.php">
<?= CSRFProtection::getTokenField('user_form') ?>
<div>
<label for="name">Name:</label>
<input type="text" id="name" name="name" required>
</div>
<div>
<label for="email">Email:</label>
<input type="email" id="email" name="email" required>
</div>
<button type="submit">Submit</button>
</form>
Processing with CSRF Check
<?php
// process.php
require_once 'CSRFProtection.php';
// Check CSRF token
CSRFProtection::middleware('user_form');
// Process form data safely
$name = InputValidator::validateString($_POST['name'], 2, 50);
$email = InputValidator::validateEmail($_POST['email']);
// Continue with form processing...
echo "Form processed successfully!";
?>
AJAX CSRF Protection
<?php
// Get CSRF token for AJAX
if ($_GET['action'] === 'get_csrf_token') {
header('Content-Type: application/json');
echo json_encode([
'token' => CSRFProtection::generateToken('ajax_form')
]);
exit;
}
?>
<script>
// JavaScript CSRF handling
class CSRFManager {
static async getToken(formName = 'ajax_form') {
const response = await fetch('?action=get_csrf_token&form=' + formName);
const data = await response.json();
return data.token;
}
static async submitForm(url, data, formName = 'ajax_form') {
const token = await this.getToken(formName);
data.csrf_token = token;
return fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data)
});
}
}
// Usage
CSRFManager.submitForm('/api/users', {
name: 'John Doe',
email: 'john@example.com'
}).then(response => {
console.log('Form submitted successfully');
});
</script>
SameSite Cookies
<?php
// Configure session cookies for CSRF protection
ini_set('session.cookie_samesite', 'Strict');
ini_set('session.cookie_secure', '1');
ini_set('session.cookie_httponly', '1');
// Or set manually
session_set_cookie_params([
'lifetime' => 0,
'path' => '/',
'domain' => '',
'secure' => true,
'httponly' => true,
'samesite' => 'Strict'
]);
session_start();
?>
Secure Authentication
Password Hashing
<?php
class PasswordManager {
/**
* Hash password securely
*/
public static function hash($password) {
// Use PHP's password_hash with bcrypt (default)
return password_hash($password, PASSWORD_DEFAULT);
}
/**
* Verify password against hash
*/
public static function verify($password, $hash) {
return password_verify($password, $hash);
}
/**
* Check if password needs rehashing
*/
public static function needsRehash($hash) {
return password_needs_rehash($hash, PASSWORD_DEFAULT);
}
/**
* Validate password strength
*/
public static function validateStrength($password) {
$errors = [];
if (strlen($password) < 8) {
$errors[] = 'Password must be at least 8 characters long';
}
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 number';
}
if (!preg_match('/[^A-Za-z0-9]/', $password)) {
$errors[] = 'Password must contain at least one special character';
}
return $errors;
}
/**
* Generate secure random password
*/
public static function generate($length = 12) {
$chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*';
return substr(str_shuffle(str_repeat($chars, $length)), 0, $length);
}
}
// Usage
$password = $_POST['password'];
// Validate password strength
$errors = PasswordManager::validateStrength($password);
if (!empty($errors)) {
die('Password validation failed: ' . implode(', ', $errors));
}
// Hash password for storage
$hashedPassword = PasswordManager::hash($password);
// Store in database
$stmt = $pdo->prepare("INSERT INTO users (email, password) VALUES (?, ?)");
$stmt->execute([$email, $hashedPassword]);
?>
Secure Login System
<?php
class AuthSystem {
private $pdo;
private $maxLoginAttempts = 5;
private $lockoutTime = 900; // 15 minutes
public function __construct($pdo) {
$this->pdo = $pdo;
}
public function login($email, $password, $rememberMe = false) {
// Check if account is locked
if ($this->isAccountLocked($email)) {
throw new Exception('Account is temporarily locked due to too many failed attempts');
}
// Get user from database
$stmt = $this->pdo->prepare("SELECT id, email, password, is_active FROM users WHERE email = ?");
$stmt->execute([$email]);
$user = $stmt->fetch();
if (!$user || !PasswordManager::verify($password, $user['password'])) {
$this->recordFailedAttempt($email);
throw new Exception('Invalid email or password');
}
if (!$user['is_active']) {
throw new Exception('Account is not active');
}
// Clear failed attempts on successful login
$this->clearFailedAttempts($email);
// Rehash password if needed
if (PasswordManager::needsRehash($user['password'])) {
$newHash = PasswordManager::hash($password);
$stmt = $this->pdo->prepare("UPDATE users SET password = ? WHERE id = ?");
$stmt->execute([$newHash, $user['id']]);
}
// Create session
$this->createSession($user['id'], $rememberMe);
// Update last login
$stmt = $this->pdo->prepare("UPDATE users SET last_login = NOW() WHERE id = ?");
$stmt->execute([$user['id']]);
return $user;
}
private function isAccountLocked($email) {
$stmt = $this->pdo->prepare("
SELECT COUNT(*) as attempts
FROM login_attempts
WHERE email = ? AND attempt_time > DATE_SUB(NOW(), INTERVAL ? SECOND)
");
$stmt->execute([$email, $this->lockoutTime]);
$result = $stmt->fetch();
return $result['attempts'] >= $this->maxLoginAttempts;
}
private function recordFailedAttempt($email) {
$stmt = $this->pdo->prepare("
INSERT INTO login_attempts (email, ip_address, attempt_time)
VALUES (?, ?, NOW())
");
$stmt->execute([$email, $_SERVER['REMOTE_ADDR']]);
}
private function clearFailedAttempts($email) {
$stmt = $this->pdo->prepare("DELETE FROM login_attempts WHERE email = ?");
$stmt->execute([$email]);
}
private function createSession($userId, $rememberMe) {
session_regenerate_id(true);
$_SESSION['user_id'] = $userId;
$_SESSION['last_activity'] = time();
if ($rememberMe) {
$token = bin2hex(random_bytes(32));
$expires = time() + (30 * 24 * 60 * 60); // 30 days
// Store remember token in database
$stmt = $this->pdo->prepare("
INSERT INTO remember_tokens (user_id, token, expires_at)
VALUES (?, ?, FROM_UNIXTIME(?))
");
$stmt->execute([$userId, hash('sha256', $token), $expires]);
// Set cookie
setcookie('remember_token', $token, $expires, '/', '', true, true);
}
}
public function logout() {
// Clear remember token if exists
if (isset($_COOKIE['remember_token'])) {
$hashedToken = hash('sha256', $_COOKIE['remember_token']);
$stmt = $this->pdo->prepare("DELETE FROM remember_tokens WHERE token = ?");
$stmt->execute([$hashedToken]);
setcookie('remember_token', '', time() - 3600, '/', '', true, true);
}
// Destroy session
session_destroy();
}
public function checkSession() {
if (!isset($_SESSION['user_id'])) {
// Check remember token
if (isset($_COOKIE['remember_token'])) {
return $this->loginWithRememberToken($_COOKIE['remember_token']);
}
return false;
}
// Check session timeout (30 minutes of inactivity)
if (isset($_SESSION['last_activity']) && (time() - $_SESSION['last_activity']) > 1800) {
$this->logout();
return false;
}
$_SESSION['last_activity'] = time();
return $_SESSION['user_id'];
}
private function loginWithRememberToken($token) {
$hashedToken = hash('sha256', $token);
$stmt = $this->pdo->prepare("
SELECT rt.user_id, u.email
FROM remember_tokens rt
JOIN users u ON rt.user_id = u.id
WHERE rt.token = ? AND rt.expires_at > NOW() AND u.is_active = 1
");
$stmt->execute([$hashedToken]);
$result = $stmt->fetch();
if ($result) {
$this->createSession($result['user_id'], false);
return $result['user_id'];
}
return false;
}
}
?>
Two-Factor Authentication
<?php
// Install via Composer: composer require robthree/twofactorauth
require_once 'vendor/autoload.php';
use RobThree\Auth\TwoFactorAuth;
class TwoFactorAuth {
private $tfa;
private $pdo;
public function __construct($pdo) {
$this->pdo = $pdo;
$this->tfa = new TwoFactorAuth('MyApp');
}
public function setupTwoFactor($userId) {
// Generate secret
$secret = $this->tfa->createSecret();
// Store secret in database
$stmt = $this->pdo->prepare("UPDATE users SET two_factor_secret = ? WHERE id = ?");
$stmt->execute([$secret, $userId]);
// Generate QR code URL
$qrCodeUrl = $this->tfa->getQRCodeImageAsDataUri('MyApp', $secret);
return [
'secret' => $secret,
'qr_code' => $qrCodeUrl
];
}
public function verifyCode($userId, $code) {
// Get user's secret
$stmt = $this->pdo->prepare("SELECT two_factor_secret FROM users WHERE id = ?");
$stmt->execute([$userId]);
$user = $stmt->fetch();
if (!$user || !$user['two_factor_secret']) {
return false;
}
return $this->tfa->verifyCode($user['two_factor_secret'], $code);
}
public function enableTwoFactor($userId, $code) {
if ($this->verifyCode($userId, $code)) {
$stmt = $this->pdo->prepare("UPDATE users SET two_factor_enabled = 1 WHERE id = ?");
$stmt->execute([$userId]);
return true;
}
return false;
}
public function isEnabled($userId) {
$stmt = $this->pdo->prepare("SELECT two_factor_enabled FROM users WHERE id = ?");
$stmt->execute([$userId]);
$user = $stmt->fetch();
return $user && $user['two_factor_enabled'];
}
}
?>
๐ก Security Best Practices
- ๐ Always Use HTTPS: Encrypt all data transmission
- ๐ก๏ธ Input Validation: Never trust user input, validate everything
- โก Prepared Statements: Always use prepared statements for database queries
- ๐ Strong Passwords: Enforce strong password policies
- ๐ Regular Updates: Keep PHP and all dependencies updated
- ๐ Security Headers: Implement proper security headers
- ๐ Security Audits: Regularly audit your code for vulnerabilities