1. Pengenalan Keamanan Python
Keamanan aplikasi adalah aspek kritis dalam pengembangan perangkat lunak. Dalam era di mana serangan siber semakin canggih, developer Python harus memahami dan menerapkan praktik keamanan yang baik sejak awal pengembangan.
Python, sebagai bahasa yang populer untuk web development, API, dan otomasi, memiliki tantangan keamanan tersendiri. Sifat dinamis Python yang fleksibel juga bisa menjadi celah keamanan jika tidak digunakan dengan hati-hati.
Mengapa Keamanan Penting?
| Risiko | Dampak | Contoh Kasus |
|---|---|---|
| Data Breach | Kebocoran data pengguna, denda GDPR | 500 juta akun terbocor (LinkedIn 2021) |
| SQL Injection | Akses database tanpa izin | " OR 1=1 -- untuk bypass login |
| Remote Code Execution | Kontrol penuh atas server | eval() pada input user |
| XSS Attack | Pencurian session/cookie | <script>tag di form input |
| Dependency Vulnerability | Celah keamanan dari library pihak ketiga | Log4Shell (Log4j) |
┌─────────────────────────────────────────────────────────────────┐ │ LAYER KEAMANAN │ │ │ │ ┌────────────────────────────────────────────────────────────┐ │ │ │ Layer 1: Network Security │ │ │ │ • HTTPS/TLS • Firewall • Rate Limiting • DDoS Protection │ │ │ └────────────────────────────────────────────────────────────┘ │ │ ┌────────────────────────────────────────────────────────────┐ │ │ │ Layer 2: Application Security │ │ │ │ • Input Validation • CSRF • XSS Prevention • Auth/Session │ │ │ └────────────────────────────────────────────────────────────┘ │ │ ┌────────────────────────────────────────────────────────────┐ │ │ │ Layer 3: Data Security │ │ │ │ • SQL Injection Prevention • Encryption • Secrets Mgmt │ │ │ └────────────────────────────────────────────────────────────┘ │ │ ┌────────────────────────────────────────────────────────────┐ │ │ │ Layer 4: Infrastructure Security │ │ │ │ • Dependency Audit • Container Security • OS Hardening │ │ │ └────────────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────────┘
2. OWASP Top 10 & Python
OWASP (Open Web Application Security Project) adalah organisasi nirlaba yang mengidentifikasi risiko keamanan web yang paling kritis. OWASP Top 10 adalah standar industri untuk awareness keamanan aplikasi web.
OWASP Top 10 (2021) dan Relevansinya dengan Python
| # | Risiko | Relevansi Python | Prevention |
|---|---|---|---|
| A01 | Broken Access Control | Endpoint tanpa otorisasi | RBAC, decorator @login_required |
| A02 | Cryptographic Failures | MD5/SHA1 untuk password | bcrypt, argon2 |
| A03 | Injection | SQL injection, command injection | ORM, parameterized queries |
| A04 | Insecure Design | Tidak ada threat modeling | Security by design |
| A05 | Security Misconfiguration | DEBUG=True di production | Environment-based config |
| A06 | Vulnerable Components | Library lama dengan CVE | safety, pip-audit |
| A07 | Auth Failures | Brute force login | Rate limiting, MFA |
| A08 | Data Integrity Failures | eval() pada data | Hindari eval/exec |
| A09 | Logging Failures | Tidak ada audit log | Structured logging |
| A10 | SSRF | Fetch URL tanpa validasi | URL whitelist |
Jangan pernah menjalankan perintah atau kode dari input user tanpa validasi ketat. Hindari penggunaan eval(), exec(), os.system(), dan subprocess dengan shell=True pada input yang tidak terpercaya.
3. Input Validation
Input validation adalah lini pertahanan pertama dalam keamanan aplikasi. Setiap input dari luar (user, API, file) harus divalidasi sebelum diproses.
Prinsip Input Validation
"""Input Validation — prinsip dasar dan contoh implementasi."""
import re
from typing import Any, Optional
from dataclasses import dataclass
from enum import Enum
class ValidationError(Exception):
"""Custom exception untuk validation error."""
def __init__(self, field: str, message: str):
self.field = field
self.message = message
super().__init__(f"{field}: {message}")
class Validator:
"""Class untuk validasi berbagai tipe input."""
@staticmethod
def validate_email(email: str) -> bool:
"""Validasi format email."""
pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
if not re.match(pattern, email):
raise ValidationError("email", f"Format email tidak valid: {email}")
if len(email) > 254:
raise ValidationError("email", "Email terlalu panjang (maks 254 karakter)")
return True
@staticmethod
def validate_password(password: str) -> dict[str, Any]:
"""Validasi kekuatan password."""
errors = []
if len(password) < 8:
errors.append("Password minimal 8 karakter")
if not re.search(r'[A-Z]', password):
errors.append("Harus mengandung huruf besar")
if not re.search(r'[a-z]', password):
errors.append("Harus mengandung huruf kecil")
if not re.search(r'\d', password):
errors.append("Harus mengandung angka")
if not re.search(r'[!@#$%^&*(),.?":{}|<>]', password):
errors.append("Harus mengandung karakter spesial")
if errors:
raise ValidationError("password", "; ".join(errors))
return {
"valid": True,
"strength": "strong" if len(password) >= 12 else "medium",
}
@staticmethod
def validate_username(username: str) -> bool:
"""Validasi username — alphanumeric + underscore."""
if not username:
raise ValidationError("username", "Username tidak boleh kosong")
if len(username) < 3:
raise ValidationError("username", "Username minimal 3 karakter")
if len(username) > 30:
raise ValidationError("username", "Username maksimal 30 karakter")
if not re.match(r'^[a-zA-Z0-9_]+$', username):
raise ValidationError(
"username",
"Username hanya boleh mengandung huruf, angka, dan underscore"
)
if username[0].isdigit():
raise ValidationError("username", "Username tidak boleh diawali angka")
return True
@staticmethod
def validate_integer(value: Any, min_val: int = None, max_val: int = None) -> int:
"""Validasi dan konversi ke integer."""
try:
num = int(value)
except (ValueError, TypeError):
raise ValidationError("value", f"'{value}' bukan angka yang valid")
if min_val is not None and num < min_val:
raise ValidationError("value", f"Nilai minimal {min_val}")
if max_val is not None and num > max_val:
raise ValidationError("value", f"Nilai maksimal {max_val}")
return num
@staticmethod
def sanitize_string(text: str, max_length: int = 1000) -> str:
"""Sanitasi string dari karakter berbahaya."""
if not isinstance(text, str):
raise ValidationError("input", "Input harus berupa string")
# Strip whitespace
text = text.strip()
# Batasi panjang
if len(text) > max_length:
text = text[:max_length]
# Hapus karakter kontrol (kecuali newline dan tab)
text = ''.join(
ch for ch in text
if ch in ('\n', '\t') or (ord(ch) >= 32)
)
return text
@staticmethod
def validate_phone_id(phone: str) -> bool:
"""Validasi nomor telepon Indonesia."""
# Format: +62xxx atau 08xxx
pattern = r'^(\+62|0)[0-9]{8,13}$'
cleaned = re.sub(r'[\s\-]', '', phone) # Hapus spasi dan dash
if not re.match(pattern, cleaned):
raise ValidationError(
"phone",
"Nomor telepon tidak valid. Gunakan format +62xxx atau 08xxx"
)
return True
# Contoh penggunaan
try:
Validator.validate_email("user@example.com")
print("✅ Email valid")
Validator.validate_password("MyStr0ng!Pass")
print("✅ Password kuat")
Validator.validate_username("budi_dev")
print("✅ Username valid")
Validator.validate_phone_id("+62812****5678")
print("✅ Nomor telepon valid")
Validator.validate_integer("42", min_val=1, max_val=100)
print("✅ Integer valid")
except ValidationError as e:
print(f"❌ Validation error: {e}")
Validasi dengan Pydantic
"""Input validation menggunakan Pydantic — library populer."""
from pydantic import BaseModel, Field, field_validator, EmailStr
from datetime import date
from typing import Optional
import re
class UserRegistration(BaseModel):
"""Schema validasi untuk registrasi user."""
username: str = Field(
...,
min_length=3,
max_length=30,
pattern=r'^[a-zA-Z0-9_]+$',
description="Username alphanumeric dengan underscore",
)
email: EmailStr
password: str = Field(..., min_length=8, max_length=128)
full_name: str = Field(..., min_length=1, max_length=100)
phone: Optional[str] = None
birth_date: Optional[date] = None
age: Optional[int] = Field(None, ge=13, le=120)
@field_validator("password")
@classmethod
def validate_password_strength(cls, v: str) -> str:
if not re.search(r'[A-Z]', v):
raise ValueError("Password harus mengandung huruf besar")
if not re.search(r'[a-z]', v):
raise ValueError("Password harus mengandung huruf kecil")
if not re.search(r'\d', v):
raise ValueError("Password harus mengandung angka")
return v
@field_validator("phone")
@classmethod
def validate_phone(cls, v: Optional[str]) -> Optional[str]:
if v is not None:
cleaned = re.sub(r'[\s\-]', '', v)
if not re.match(r'^(\+62|0)[0-9]{8,13}$', cleaned):
raise ValueError("Format nomor telepon Indonesia tidak valid")
return cleaned
return v
@field_validator("full_name")
@classmethod
def sanitize_name(cls, v: str) -> str:
# Hapus karakter yang bukan huruf, spasi, atau tanda hubung
cleaned = re.sub(r'[^\w\s\-]', '', v, flags=re.UNICODE)
return cleaned.strip()
# Contoh penggunaan
try:
user = UserRegistration(
username="budi_dev",
email="budi@example.com",
password="MyStr0ng!Pass1",
full_name="Budi Santoso",
phone="+62812****5678",
age=25,
)
print(f"✅ Valid: {user.username}")
print(f" Email: {user.email}")
print(f" Data: {user.model_dump()}")
except Exception as e:
print(f"❌ Error: {e}")
# Contoh invalid
try:
bad_user = UserRegistration(
username="ab", # Terlalu pendek
email="not-an-email",
password="weak", # Terlalu lemah
full_name="Test",
)
except Exception as e:
print(f"❌ Expected error: {e}")
4. Mencegah SQL Injection
SQL Injection adalah salah satu serangan paling berbahaya dan umum. Attacker bisa memanipulasi query SQL untuk mencuri data, menghapus tabel, atau bahkan mengambil alih database.
Contoh SQL Injection
"""SQL Injection — contoh VULNERABLE dan SECURE."""
# ❌❌❌ VULNERABLE — JANGAN PERNAH LAKUKAN INI ❌❌❌
def login_vulnerable(username: str, password: str):
"""SQL INJECTION VULNERABLE! Ini hanya contoh BURUK."""
import sqlite3
conn = sqlite3.connect("app.db")
cursor = conn.cursor()
# ⚠️ BAHAYA: String formatting langsung!
query = f"SELECT * FROM users WHERE username='{username}' AND password='{password}'"
# Jika user memasukkan: username = "admin' OR '1'='1' --"
# Query menjadi: SELECT * FROM users WHERE username='admin' OR '1'='1' --' AND password=''
# Attacker bisa bypass login!
cursor.execute(query)
result = cursor.fetchone()
conn.close()
return result
# ✅✅✅ SECURE — Gunakan parameterized queries ✅✅✅
def login_secure(username: str, password: str):
"""Login yang AMAN menggunakan parameterized query."""
import sqlite3
conn = sqlite3.connect("app.db")
cursor = conn.cursor()
# ✅ AMAN: Parameter placeholder (?)
query = "SELECT * FROM users WHERE username = ? AND password_hash = ?"
cursor.execute(query, (username, password))
result = cursor.fetchone()
conn.close()
return result
Menggunakan ORM (SQLAlchemy)
"""SQL Injection prevention menggunakan SQLAlchemy ORM — metode teraman."""
from sqlalchemy import create_engine, Column, Integer, String, select
from sqlalchemy.orm import declarative_base, Session
Base = declarative_base()
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True)
username = Column(String(50), unique=True, nullable=False)
email = Column(String(100), unique=True, nullable=False)
password_hash = Column(String(255), nullable=False)
role = Column(String(20), default="user")
class UserRepository:
"""Repository yang AMAN dari SQL injection."""
def __init__(self, session: Session):
self.session = session
def find_by_username(self, username: str):
"""✅ Aman — ORM menggunakan parameterized query secara otomatis."""
return self.session.execute(
select(User).where(User.username == username)
).scalar_one_or_none()
def search_users(self, search_term: str):
"""✅ Aman — menggunakan bind parameter."""
return self.session.execute(
select(User).where(User.username.ilike(f"%{search_term}%"))
).scalars().all()
def find_by_role(self, role: str):
"""✅ Aman — parameter di-bind otomatis."""
return self.session.execute(
select(User).where(User.role == role)
).scalars().all()
def update_user(self, user_id: int, **kwargs):
"""✅ Aman — menggunakan ORM update method."""
user = self.session.get(User, user_id)
if user:
for key, value in kwargs.items():
if hasattr(user, key) and key not in ("id", "password_hash"):
setattr(user, key, value)
self.session.commit()
return user
# Contoh penggunaan yang AMAN
# engine = create_engine("sqlite:///app.db")
# with Session(engine) as session:
# repo = UserRepository(session)
# user = repo.find_by_username("budi") # ✅ Aman dari injection
# users = repo.search_users("admin") # ✅ Aman dari injection
- ❌
f"SELECT * FROM users WHERE id={user_id}" - ❌
"SELECT * FROM users WHERE name='%s'" % name - ❌
"SELECT * FROM users WHERE name='" + name + "'" - ✅
"SELECT * FROM users WHERE id=?", (user_id,) - ✅
select(User).where(User.id == user_id)
5. Mencegah XSS & CSRF
XSS (Cross-Site Scripting) terjadi ketika attacker menyisipkan script berbahaya ke halaman web yang dilihat oleh user lain. CSRF (Cross-Site Request Forgery) memaksa user melakukan aksi tanpa sepengetahuan mereka.
"""Mencegah XSS dan CSRF di Python web application."""
import html
import re
from markupsafe import escape, Markup
# ===== XSS Prevention =====
# ❌ VULNERABLE — menampilkan user input tanpa escaping
def render_comment_vulnerable(user_comment: str) -> str:
"""JANGAN LAKUKAN INI! XSS Vulnerable."""
return f"<div class='comment'>{user_comment}</div>"
# Jika user_comment = "<script>alert('hacked')</script>"
# Script akan dieksekusi di browser user lain!
# ✅ SECURE — escape HTML entities
def render_comment_secure(user_comment: str) -> str:
"""Escape HTML entities untuk mencegah XSS."""
escaped = html.escape(user_comment)
return f"<div class='comment'>{escaped}</div>"
# <script> akan ditampilkan sebagai teks biasa, bukan dieksekusi
# ✅ SECURE — menggunakan markupsafe (library yang lebih robust)
def render_comment_markup(user_comment: str) -> str:
"""Menggunakan markupsafe.escape — lebih robust."""
safe_comment = escape(user_comment)
return Markup(f"<div class='comment'>{safe_comment}</div>")
# ✅ SECURE — Whitelist allowed HTML tags (untuk rich text)
def sanitize_html(user_html: str, allowed_tags: set = None) -> str:
"""Sanitasi HTML — hapus tag berbahaya, pertahankan yang aman."""
if allowed_tags is None:
allowed_tags = {"b", "i", "u", "em", "strong", "p", "br", "ul", "ol", "li"}
# Hapus script dan style tags beserta isinya
user_html = re.sub(r'<script[^>]*>.*?</script>', '', user_html, flags=re.DOTALL)
user_html = re.sub(r'<style[^>]*>.*?</style>', '', user_html, flags=re.DOTALL)
# Hapus event handlers (onclick, onerror, dll)
user_html = re.sub(r'\s*on\w+\s*=\s*["\'][^"\']*["\']', '', user_html)
# Hapus javascript: URLs
user_html = re.sub(r'javascript:', '', user_html, flags=re.IGNORECASE)
# Hapus tag yang tidak diizinkan
def strip_tag(match):
tag = match.group(1).lower().split()[0]
if tag in allowed_tags:
return match.group(0)
return ''
user_html = re.sub(r'</?(\w+)[^>]*>', strip_tag, user_html)
return user_html
# Contoh XSS payloads yang dicegah
test_inputs = [
"<script>alert('XSS')</script>",
"<img src=x onerror=alert('XSS')>",
"<a href='javascript:alert(1)'>Click</a>",
"<div onclick='steal_cookies()'>Click me</div>",
"Normal text without HTML",
]
print("=== XSS Prevention Demo ===")
for test in test_inputs:
safe = render_comment_secure(test)
print(f" Input : {test[:60]}...")
print(f" Output: {safe[:60]}...")
print()
# ===== CSRF Prevention =====
"""CSRF prevention menggunakan token."""
import secrets
import hmac
import hashlib
import time
class CSRFProtection:
"""CSRF token generation dan validation."""
def __init__(self, secret_key: str):
self.secret_key = secret_key.encode()
self._tokens: dict[str, float] = {}
def generate_token(self, session_id: str) -> str:
"""Generate CSRF token untuk session."""
# Token = random + HMAC(session_id + timestamp)
timestamp = str(int(time.time()))
random_part = secrets.token_hex(16)
message = f"{session_id}:{timestamp}:{random_part}"
signature = hmac.new(
self.secret_key, message.encode(), hashlib.sha256
).hexdigest()
token = f"{random_part}.{timestamp}.{signature}"
self._tokens[token] = time.time()
return token
def validate_token(self, token: str, session_id: str, max_age: int = 3600) -> bool:
"""Validasi CSRF token."""
try:
parts = token.split(".")
if len(parts) != 3:
return False
random_part, timestamp, signature = parts
# Cek expiry
if time.time() - int(timestamp) > max_age:
return False
# Cek HMAC
message = f"{session_id}:{timestamp}:{random_part}"
expected = hmac.new(
self.secret_key, message.encode(), hashlib.sha256
).hexdigest()
return hmac.compare_digest(signature, expected)
except Exception:
return False
# Contoh penggunaan
csrf = CSRFProtection("my-secret-key-very-long")
# Generate token
token = csrf.generate_token("session_abc123")
print(f"✅ CSRF Token: {token}")
# Validate token
is_valid = csrf.validate_token(token, "session_abc123")
print(f"✅ Token valid: {is_valid}")
# Coba manipulasi
is_valid = csrf.validate_token("fake.token.here", "session_abc123")
print(f"❌ Fake token valid: {is_valid}")
6. Secrets Management
Menyimpan API keys, database credentials, dan secrets lainnya dengan aman adalah kunci keamanan aplikasi. Jangan pernah hardcode secrets di source code!
❌ Cara yang Salah
# ❌❌❌ JANGAN PERNAH LAKUKAN INI ❌❌❌
# Hardcoded secrets di source code
DATABASE_URL = "postgresql://admin:P@ssw0rd@db.example.com:5432/prod"
API_KEY = "sk-1234567890abcdef"
SECRET_KEY = "my-super-secret-key-12345"
AWS_SECRET = "AKIAIOSFODNN7EXAMPLE"
# Secrets di Git repository
# Sekali di-commit, selamanya ada di history Git!
✅ Cara yang Benar: Environment Variables
"""Secrets management menggunakan environment variables."""
import os
from dataclasses import dataclass
from typing import Optional
@dataclass(frozen=True)
class AppConfig:
"""Konfigurasi aplikasi dari environment variables."""
# Database
database_url: str
db_pool_size: int = 10
# API
api_key: str
api_secret: str
# Security
secret_key: str
jwt_secret: str
# External Services
redis_url: str = "redis://localhost:6379"
smtp_host: str = "smtp.gmail.com"
smtp_port: int = 587
smtp_user: Optional[str] = None
smtp_pass: Optional[str] = None
# App
debug: bool = False
environment: str = "production"
@classmethod
def from_env(cls) -> "AppConfig":
"""Load konfigurasi dari environment variables."""
missing = []
required = ["DATABASE_URL", "API_KEY", "API_SECRET", "SECRET_KEY", "JWT_SECRET"]
for var in required:
if not os.environ.get(var):
missing.append(var)
if missing:
raise EnvironmentError(
f"Environment variables yang hilang: {', '.join(missing)}\n"
f"Pastikan sudah di-set di .env atau sistem."
)
return cls(
database_url=os.environ["DATABASE_URL"],
db_pool_size=int(os.environ.get("DB_POOL_SIZE", "10")),
api_key=os.environ["API_KEY"],
api_secret=os.environ["API_SECRET"],
secret_key=os.environ["SECRET_KEY"],
jwt_secret=os.environ["JWT_SECRET"],
redis_url=os.environ.get("REDIS_URL", "redis://localhost:6379"),
smtp_host=os.environ.get("SMTP_HOST", "smtp.gmail.com"),
smtp_port=int(os.environ.get("SMTP_PORT", "587")),
smtp_user=os.environ.get("SMTP_USER"),
smtp_pass=os.environ.get("SMTP_PASS"),
debug=os.environ.get("DEBUG", "false").lower() == "true",
environment=os.environ.get("ENVIRONMENT", "production"),
)
# .env file (JANGAN commit ke Git!)
"""
DATABASE_URL=postgresql://user:pass@localhost:5432/mydb
API_KEY=sk-your-api-key
API_SECRET=your-api-secret
SECRET_KEY=generate-a-random-64-char-string
JWT_SECRET=generate-another-random-string
DEBUG=false
ENVIRONMENT=production
"""
# .gitignore (PASTIKAN ada ini!)
# .env
# .env.local
# .env.production
# *.pem
# *.key
Menggunakan python-dotenv
"""Menggunakan python-dotenv untuk development."""
# pip install python-dotenv
from dotenv import load_dotenv
import os
# Load .env file (hanya untuk development)
load_dotenv() # Default: cari .env di direktori saat ini
# Akses environment variables
database_url = os.getenv("DATABASE_URL")
api_key = os.getenv("API_KEY")
if not database_url:
raise ValueError("DATABASE_URL belum di-set")
# Best practice: buat .env.example (tanpa nilai asli) untuk template
"""
# .env.example (COMMIT ke Git sebagai template)
DATABASE_URL=postgresql://user:password@localhost:5432/dbname
API_KEY=your-api-key-here
SECRET_KEY=generate-random-key-here
DEBUG=true
ENVIRONMENT=development
"""
7. Authentication & Authorization
Mengimplementasikan authentication dan authorization yang aman sangat kritis. Gunakan library yang sudah teruji seperti bcrypt atau argon2 untuk hashing password.
"""Authentication yang aman — password hashing, JWT, rate limiting."""
import hashlib
import hmac
import secrets
import time
from dataclasses import dataclass, field
from typing import Optional
# ===== Password Hashing dengan hashlib (built-in) =====
# NOTE: Untuk production, gunakan bcrypt atau argon2-cffi
# pip install argon2-cffi
import hashlib
import os
def hash_password_salt(password: str) -> str:
"""Hash password dengan salt menggunakan hashlib (basic)."""
salt = os.urandom(32) # 32-byte random salt
key = hashlib.pbkdf2_hmac(
'sha256',
password.encode('utf-8'),
salt,
iterations=600000, # OWASP recommends 600,000 for PBKDF2-SHA256
)
# Simpan salt + key
return salt.hex() + ":" + key.hex()
def verify_password(stored_hash: str, password: str) -> bool:
"""Verifikasi password terhadap hash yang tersimpan."""
salt_hex, key_hex = stored_hash.split(":")
salt = bytes.fromhex(salt_hex)
new_key = hashlib.pbkdf2_hmac(
'sha256',
password.encode('utf-8'),
salt,
iterations=600000,
)
return hmac.compare_digest(key_hex, new_key.hex())
# Contoh
hashed = hash_password_salt("MySecureP@ss123")
print(f"Hash: {hashed[:50]}...")
print(f"Verify correct: {verify_password(hashed, 'MySecureP@ss123')}") # True
print(f"Verify wrong : {verify_password(hashed, 'wrongpassword')}") # False
# ===== Argon2 — Rekomendasi Terbaik =====
# pip install argon2-cffi
try:
from argon2 import PasswordHasher
from argon2.exceptions import VerifyMismatchError
ph = PasswordHasher(
time_cost=3, # Iterations
memory_cost=65536, # 64 MB
parallelism=4, # Threads
)
# Hash
hash_argon = ph.hash("MySecureP@ss123")
print(f"\nArgon2 hash: {hash_argon[:60]}...")
# Verify
try:
ph.verify(hash_argon, "MySecureP@ss123")
print("Argon2 verify: ✅ Benar")
except VerifyMismatchError:
print("Argon2 verify: ❌ Salah")
# Cek apakah perlu rehash (jika parameter berubah)
if ph.check_needs_rehash(hash_argon):
print("⚠️ Perlu rehash dengan parameter terbaru")
except ImportError:
print("Argon2 tidak terinstall. Gunakan: pip install argon2-cffi")
# ===== JWT (JSON Web Token) =====
# pip install pyjwt
try:
import jwt
from datetime import datetime, timedelta
SECRET = "your-jwt-secret-key-very-long-and-random"
def create_token(user_id: int, role: str, expires_hours: int = 24) -> str:
"""Buat JWT token."""
payload = {
"user_id": user_id,
"role": role,
"exp": datetime.utcnow() + timedelta(hours=expires_hours),
"iat": datetime.utcnow(),
"jti": secrets.token_hex(16), # Unique token ID
}
return jwt.encode(payload, SECRET, algorithm="HS256")
def verify_token(token: str) -> Optional[dict]:
"""Verifikasi JWT token."""
try:
payload = jwt.decode(token, SECRET, algorithms=["HS256"])
return payload
except jwt.ExpiredSignatureError:
print("❌ Token expired")
except jwt.InvalidTokenError as e:
print(f"❌ Token invalid: {e}")
return None
# Contoh
token = create_token(user_id=42, role="admin")
print(f"\nJWT Token: {token[:50]}...")
payload = verify_token(token)
if payload:
print(f"✅ User ID: {payload['user_id']}, Role: {payload['role']}")
except ImportError:
print("PyJWT tidak terinstall. Gunakan: pip install pyjwt")
# ===== Rate Limiting =====
class RateLimiter:
"""Simple rate limiter untuk mencegah brute force."""
def __init__(self, max_attempts: int = 5, window_seconds: int = 300):
self.max_attempts = max_attempts
self.window_seconds = window_seconds
self._attempts: dict[str, list[float]] = {}
def is_allowed(self, identifier: str) -> bool:
"""Cek apakah request diizinkan."""
now = time.time()
window_start = now - self.window_seconds
# Bersihkan attempt lama
if identifier in self._attempts:
self._attempts[identifier] = [
t for t in self._attempts[identifier] if t > window_start
]
else:
self._attempts[identifier] = []
if len(self._attempts[identifier]) >= self.max_attempts:
return False
self._attempts[identifier].append(now)
return True
def get_remaining_attempts(self, identifier: str) -> int:
"""Sisa attempt yang diizinkan."""
now = time.time()
window_start = now - self.window_seconds
recent = [t for t in self._attempts.get(identifier, []) if t > window_start]
return max(0, self.max_attempts - len(recent))
# Contoh rate limiting untuk login
limiter = RateLimiter(max_attempts=5, window_seconds=60)
for i in range(7):
ip = "192.168.1.1"
allowed = limiter.is_allowed(ip)
remaining = limiter.get_remaining_attempts(ip)
status = "✅ Allowed" if allowed else "❌ Blocked"
print(f" Attempt {i+1}: {status} (remaining: {remaining})")
8. Kriptografi di Python
"""Kriptografi dasar di Python — hashing, encryption, signing."""
import hashlib
import hmac
import secrets
import base64
from cryptography.fernet import Fernet
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
import os
# ===== 1. Hashing =====
print("=== Hashing ===")
data = "Hello, World! Ini data sensitif."
# SHA-256 (recommended untuk checksum)
sha256 = hashlib.sha256(data.encode()).hexdigest()
print(f"SHA-256: {sha256}")
# SHA-512 (lebih kuat)
sha512 = hashlib.sha512(data.encode()).hexdigest()
print(f"SHA-512: {sha512[:64]}...")
# ✅ HMAC — Hash dengan secret key (untuk signing)
key = b"my-secret-key"
signature = hmac.new(key, data.encode(), hashlib.sha256).hexdigest()
print(f"HMAC-SHA256: {signature}")
# Verifikasi signature
is_valid = hmac.compare_digest(
signature,
hmac.new(key, data.encode(), hashlib.sha256).hexdigest()
)
print(f"Signature valid: {is_valid}")
# ===== 2. Simmetric Encryption dengan Fernet =====
print("\n=== Fernet Encryption ===")
# Generate key
key = Fernet.generate_key()
cipher = Fernet(key)
# Encrypt
plaintext = b"Database password: P@ssw0rd123!"
encrypted = cipher.encrypt(plaintext)
print(f"Encrypted: {encrypted[:50]}...")
# Decrypt
decrypted = cipher.decrypt(encrypted)
print(f"Decrypted: {decrypted.decode()}")
# Encrypt dengan expiry
import time
token = cipher.encrypt_at_time(
b"Secret message",
current_time=int(time.time())
)
# Decrypt — masih valid
try:
result = cipher.decrypt_at_time(token, ttl=3600, current_time=int(time.time()))
print(f"Decrypted (with TTL): {result.decode()}")
except Exception as e:
print(f"Token expired: {e}")
# ===== 3. AES-GCM (Authenticated Encryption) =====
print("\n=== AES-256-GCM Encryption ===")
# Generate key dan nonce
key = AESGCM.generate_key(bit_length=256)
nonce = os.urandom(12) # 96-bit nonce
aesgcm = AESGCM(key)
# Encrypt dengan associated data (AAD)
plaintext = b"Sensitive data yang perlu di-encrypt"
associated_data = b"user_id:42" # Metadata yang tidak di-encrypt tapi di-authenticate
ciphertext = aesgcm.encrypt(nonce, plaintext, associated_data)
print(f"Ciphertext: {ciphertext[:50]}...")
# Decrypt dan verifikasi AAD
try:
decrypted = aesgcm.decrypt(nonce, ciphertext, associated_data)
print(f"Decrypted: {decrypted.decode()}")
except Exception:
print("Decryption failed — data dimanipulasi!")
# ===== 4. Secret Generation =====
print("\n=== Secure Random Generation ===")
# Token untuk API key
api_key = secrets.token_urlsafe(32)
print(f"API Key: {api_key}")
# Token hex untuk CSRF
csrf_token = secrets.token_hex(32)
print(f"CSRF Token: {csrf_token}")
# Secure random number
otp = secrets.randbelow(1000000)
print(f"OTP: {otp:06d}")
# Generate password
import string
alphabet = string.ascii_letters + string.digits + string.punctuation
password = ''.join(secrets.choice(alphabet) for _ in range(20))
print(f"Random Password: {password}")
9. Keamanan File & Path Traversal
Path traversal terjadi ketika attacker memanipulasi path file untuk mengakses file di luar direktori yang diizinkan.
"""Keamanan file — mencegah path traversal dan unsafe operations."""
import os
from pathlib import Path
# ❌ VULNERABLE — Path Traversal
def read_file_vulnerable(filename: str) -> str:
"""JANGAN LAKUKAN INI! Path Traversal Vulnerable."""
# Jika filename = "../../etc/passwd" → attacker bisa baca /etc/passwd!
with open(filename, "r") as f:
return f.read()
# ✅ SECURE — Path validation
def read_file_secure(filename: str, base_dir: str = "./uploads") -> str:
"""Baca file dengan validasi path yang aman."""
base = Path(base_dir).resolve()
target = (base / filename).resolve()
# Pastikan target berada di dalam base directory
if not str(target).startswith(str(base)):
raise PermissionError(
f"Path traversal detected! {filename} berada di luar direktori yang diizinkan"
)
if not target.exists():
raise FileNotFoundError(f"File tidak ditemukan: {filename}")
if not target.is_file():
raise ValueError(f"Bukan file: {filename}")
with open(target, "r", encoding="utf-8") as f:
return f.read()
# ✅ SECURE — Filename sanitization
def sanitize_filename(filename: str) -> str:
"""Sanitasi filename untuk mencegah path traversal dan karakter berbahaya."""
# Hapus path components
filename = os.path.basename(filename)
# Hapus karakter berbahaya
allowed = set("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789.-_")
sanitized = ''.join(ch for ch in filename if ch in allowed)
# Hindari hidden files
sanitized = sanitized.lstrip(".")
# Hindari reserved names di Windows
reserved = {"CON", "PRN", "AUX", "NUL", "COM1", "COM2", "LPT1", "LPT2"}
name_part = sanitized.split(".")[0].upper()
if name_part in reserved:
sanitized = f"file_{sanitized}"
# Default name jika kosong
if not sanitized:
sanitized = "unnamed_file"
return sanitized
# Contoh test
test_filenames = [
"../../etc/passwd",
"..\\..\\windows\\system32\\config\\sam",
"normal_file.txt",
"file.txt",
".hidden_file",
"CON.txt",
"",
]
print("=== Filename Sanitization ===")
for test in test_filenames:
sanitized = sanitize_filename(test)
print(f" '{test}' → '{sanitized}'")
# ✅ SECURE — File upload validation
def validate_upload(
file_content: bytes,
filename: str,
max_size_mb: int = 10,
allowed_extensions: set = None,
) -> dict:
"""Validasi file upload yang aman."""
if allowed_extensions is None:
allowed_extensions = {".jpg", ".jpeg", ".png", ".gif", ".pdf", ".docx", ".xlsx"}
errors = []
# 1. Cek ukuran
size_mb = len(file_content) / (1024 * 1024)
if size_mb > max_size_mb:
errors.append(f"File terlalu besar: {size_mb:.1f}MB (maks {max_size_mb}MB)")
# 2. Cek ekstensi
ext = os.path.splitext(filename)[1].lower()
if ext not in allowed_extensions:
errors.append(f"Ekstensi '{ext}' tidak diizinkan. Tersedia: {allowed_extensions}")
# 3. Cek magic bytes (file signature)
magic_signatures = {
b'\xff\xd8\xff': "jpg",
b'\x89PNG': "png",
b'GIF8': "gif",
b'%PDF': "pdf",
b'PK\x03\x04': "zip/docx/xlsx",
}
detected_type = None
for magic, file_type in magic_signatures.items():
if file_content[:len(magic)] == magic:
detected_type = file_type
break
if detected_type is None:
errors.append("Tidak bisa mendeteksi tipe file dari content")
# 4. Cek apakah ekstensi cocok dengan content
# (mencegah file .jpg yang sebenarnya .exe)
if detected_type and ext:
ext_clean = ext.lstrip(".")
if ext_clean not in detected_type and detected_type not in ext_clean:
if not (ext_clean in ("docx", "xlsx") and detected_type == "zip/docx/xlsx"):
errors.append(
f"Ekstensi '{ext}' tidak cocok dengan tipe file terdeteksi '{detected_type}'"
)
return {
"valid": len(errors) == 0,
"errors": errors,
"sanitized_name": sanitize_filename(filename),
"detected_type": detected_type,
"size_mb": round(size_mb, 2),
}
10. Dependency Security
Library pihak ketiga bisa menjadi celah keamanan. Pastikan dependency Anda selalu di-update dan di-audit secara berkala.
# ===== Security Audit Tools =====
# 1. pip-audit — audit dependency dari PyPI Advisory Database
pip install pip-audit
pip-audit
# Output contoh:
# Name ID Fixed Versions
# ------- ---------- --------------
# requests PYSEC-2023-74 >=2.31.0
# urllib3 PYSEC-2023-217 >=2.0.7
# 2. safety — cek dependency terhadap Safety DB
pip install safety
safety check
# 3. bandit — static analysis untuk kode Python
pip install bandit
bandit -r src/ -ll # Hanya level MEDIUM ke atas
# Contoh output bandit:
# Issue: [B602:subprocess_popen_with_shell_equals_true] subprocess call with shell=True
# Severity: High
# Location: src/utils.py:45
# 4. pip-audit dalam CI/CD
pip-audit --strict --desc --output audit-report.json --format json
# 5. Semgrep — advanced static analysis
pip install semgrep
semgrep --config=auto src/
Pre-commit Hooks untuk Security
# .pre-commit-config.yaml
repos:
- repo: https://github.com/PyCQA/bandit
rev: 1.7.5
hooks:
- id: bandit
args: ["-ll", "--recursive"]
additional_dependencies: ["bandit[toml]"]
- repo: https://github.com/Yelp/detect-secrets
rev: v1.4.0
hooks:
- id: detect-secrets
args: ["--baseline", ".secrets.baseline"]
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.5.0
hooks:
- id: detect-private-key
- id: check-added-large-files
11. Security Audit & Tools
Security Checklist
- ✅ Semua input user divalidasi dan di-sanitasi
- ✅ Tidak ada
eval(),exec(), atauos.system()pada input user - ✅ Database queries menggunakan parameterized queries atau ORM
- ✅ Password di-hash menggunakan bcrypt/argon2 (bukan MD5/SHA1)
- ✅ Secrets disimpan di environment variables (bukan di source code)
- ✅
.envdan secrets ada di.gitignore - ✅ HTTPS/TLS diaktifkan untuk semua endpoint
- ✅ CSRF protection diaktifkan untuk semua form
- ✅ XSS prevention (escaping) untuk semua output HTML
- ✅ Rate limiting untuk login dan API endpoints
- ✅ Dependency di-audit secara berkala (
pip-audit) - ✅
DEBUG=Falsedi production - ✅ Error messages tidak menampilkan informasi sensitif
- ✅ Logging mencatat semua aksi keamanan penting
- ✅ Security headers (CSP, HSTS, X-Frame-Options) di-set
Bandit — Static Security Analysis
# Install bandit
pip install bandit
# Scan seluruh proyek
bandit -r src/
# Hanya tampilkan HIGH severity
bandit -r src/ -lll
# Output ke file
bandit -r src/ -f json -o security-report.json
# Skip test tertentu
bandit -r src/ --skip B101 # Skip assert checks
# Scan file spesifik
bandit src/auth.py src/utils.py
Contoh Konfigurasi Bandit
# pyproject.toml
[tool.bandit]
exclude_dirs = ["tests", "venv"]
skips = ["B101"] # Skip assert warnings
assert_used = ["src/exceptions.py"]
12. Quiz Pemahaman
Uji pemahaman Anda tentang keamanan Python: