Web Security Essentials — Panduan Lengkap Melindungi Aplikasi Web

Web Security Essentials — Panduan Lengkap Melindungi Aplikasi Web

10/23/2025 Security By Tech Writers
SecurityOWASPWeb DevelopmentVulnerabilitiesBest Practices

Pengenalan: Keamanan adalah Tanggung Jawab, Bukan Hal Sampingan

Di era digital yang terus berkembang, keamanan aplikasi web bukan lagi kemewahan opsional melainkan kebutuhan kritis. Setiap hari, peretas siber mengeksploitasi kerentanan di aplikasi web untuk mencuri data, mengganggu layanan, atau menyebabkan kerugian finansial. Sebagai developer, sysadmin, atau architect, kamu punya tanggung jawab untuk memahami dan menerapkan best practice keamanan di setiap lapisan aplikasi.

OWASP (Open Web Application Security Project) secara berkala menerbitkan daftar Top 10 kerentanan paling berbahaya di aplikasi production. Memahami kerentanan ini dan cara melindungi aplikasimu adalah pengetahuan penting yang harus dimiliki setiap developer modern. Dalam panduan ini, kita akan mengeksplorasi OWASP Top 10, mekanisme proteksi, enkripsi, autentikasi, dan strategi keamanan untuk production.

Daftar Isi

OWASP Top 10 Overview

1. Injection (SQL, NoSQL, OS Command)
2. Autentikasi Rusak
3. Ekspos Data Sensitif
4. XML External Entities (XXE)
5. Kontrol Akses Rusak
6. Misconfiguration Keamanan
7. Cross-Site Scripting (XSS)
8. Deserialisasi Tidak Aman
9. Menggunakan Komponen dengan Kerentanan Terkenal
10. Logging dan Monitoring Tidak Cukup

Injection Attacks: SQL, NoSQL, OS

Injection attacks terjadi ketika data yang tidak dipercaya dikirim ke interpreter sebagai perintah. Ini adalah kerentanan paling umum dan berbahaya.

SQL Injection

SQL injection adalah serangan di mana penyerang memasukkan kode SQL berbahaya melalui input pengguna. Menggunakan parameterized queries atau prepared statements bisa mencegah injeksi karena input dipisahkan dari perintah SQL.

// ✗ RENTAN: SQL Injection
app.get('/user/:id', (req, res) => {
  const query = `SELECT * FROM users WHERE id = ${req.params.id}`;
  // Penyerang: /user/1 OR 1=1 -- (memilih SEMUA user!)
  db.query(query, (err, result) => {
    res.json(result);
  });
});

// ✓ AMAN: Parameterized Queries
app.get('/user/:id', (req, res) => {
  const query = 'SELECT * FROM users WHERE id = ?';
  const values = [req.params.id];
  db.query(query, values, (err, result) => {
    res.json(result);
  });
});

// ✓ AMAN: ORM dengan Prepared Statements
const User = require('./models/User');

app.get('/user/:id', async (req, res) => {
  try {
    const user = await User.findById(req.params.id);
    res.json(user);
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

NoSQL Injection

NoSQL injection mirip dengan SQL injection tapi menargetkan database NoSQL. Penyerang bisa memanipulasi query operators untuk mengakses atau memodifikasi data. Input validation dan schema typing dengan libraries seperti Mongoose bisa mencegah serangan ini.

// ✗ RENTAN: NoSQL Injection
app.post('/login', (req, res) => {
  db.users.findOne({ 
    email: req.body.email,
    password: req.body.password 
  }).then(user => {
    // Penyerang: { email: { $ne: null }, password: { $ne: null } }
    // Mengembalikan user pertama tanpa memperhatikan kredensial!
  });
});

// ✓ AMAN: Input Validation
const { body, validationResult } = require('express-validator');

app.post('/login', [
  body('email').isEmail().normalizeEmail(),
  body('password').isString().trim()
], async (req, res) => {
  const errors = validationResult(req);
  if (!errors.isEmpty()) {
    return res.status(400).json({ errors: errors.array() });
  }
  
  const user = await db.users.findOne({
    email: req.body.email,
    password: req.body.password
  });
});

// ✓ AMAN: Type Checking dengan Mongoose Schema
const userSchema = new mongoose.Schema({
  email: { type: String, required: true, index: true },
  password: { type: String, required: true }
});

// Mongoose secara otomatis mencegah NoSQL injection

OS Command Injection

OS command injection terjadi ketika aplikasi mengeksekusi perintah sistem operasi dengan input dari pengguna tanpa validasi. Penyerang bisa menyisipkan karakter khusus untuk menjalankan perintah tambahan. Hindari shell commands dengan input pengguna atau gunakan whitelist validation yang ketat.

// ✗ RENTAN: OS Command Injection
app.get('/ping/:host', (req, res) => {
  const cmd = `ping -c 1 ${req.params.host}`;
  // Penyerang: /ping/google.com && rm -rf /
  exec(cmd, (error, stdout) => {
    res.send(stdout);
  });
});

// ✓ AMAN: Gunakan array syntax (tanpa shell interpretation)
app.get('/ping/:host', (req, res) => {
  // Validasi format host
  if (!/^[a-zA-Z0-9.-]+$/.test(req.params.host)) {
    return res.status(400).json({ error: 'Host tidak valid' });
  }
  
  execFile('ping', ['-c', '1', req.params.host], (error, stdout) => {
    res.send(stdout);
  });
});

// ✓ LEBIH BAIK: Hindari shell commands sama sekali
const dns = require('dns').promises;

app.get('/ping/:host', async (req, res) => {
  try {
    const address = await dns.resolve4(req.params.host);
    res.json({ host: req.params.host, address });
  } catch (error) {
    res.status(400).json({ error: 'Host tidak ditemukan' });
  }
});

Cross-Site Scripting (XSS)

XSS terjadi ketika penyerang menginjeksi script berbahaya yang dijalankan di browser pengguna. Ada tiga jenis: Stored, Reflected, dan DOM-based.

Stored XSS

Stored XSS adalah bentuk yang paling berbahaya karena script berbahaya disimpan di database dan dijalankan setiap kali data diambil. Sanitasi input dengan DOMPurify dan selalu escape output saat merender HTML untuk mencegah serangan ini.

// ✗ RENTAN: Stored XSS
app.post('/comment', (req, res) => {
  // Simpan input pengguna langsung
  db.comments.create({
    text: req.body.text,  // Bisa: <script>alert('XSS')</script>
    userId: req.user.id
  });
});

// Render tanpa escaping
app.get('/post/:id', (req, res) => {
  const post = db.posts.findById(req.params.id);
  res.render('post', { post }); // Jika template tidak escape, XSS berjalan!
});

// ✓ AMAN: Sanitize input
const DOMPurify = require('isomorphic-dompurify');

app.post('/comment', (req, res) => {
  const sanitized = DOMPurify.sanitize(req.body.text);
  db.comments.create({
    text: sanitized,
    userId: req.user.id
  });
});

// ✓ AMAN: Escape output
app.get('/post/:id', (req, res) => {
  const post = db.posts.findById(req.params.id);
  // Di template: {{ post.text | escape }}
  res.render('post', { post });
});

Reflected XSS

Reflected XSS terjadi ketika input pengguna langsung dikembalikan dalam response. Penyerang menipu pengguna untuk mengklik link berbahaya.

// ✗ RENTAN: Reflected XSS
app.get('/search', (req, res) => {
  const query = req.query.q;
  // Pencarian: /?q=<img%20src=x%20onerror=alert('XSS')>
  res.send(`Hasil pencarian untuk: ${query}`);
});

// ✓ AMAN: Escape karakter khusus
app.get('/search', (req, res) => {
  const query = req.query.q || '';
  const escaped = query
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&#x27;');
  
  res.send(`Hasil pencarian untuk: ${escaped}`);
});

// ✓ LEBIH BAIK: Gunakan template engine dengan auto-escaping
// EJS, Nunjucks, dll secara otomatis melakukan escape
res.render('search', { query: req.query.q });

DOM-based XSS terjadi ketika kode JavaScript membangun HTML dari input pengguna. Selalu gunakan metode DOM yang aman atau library sanitization.

DOM-based XSS

DOM-based XSS terjadi di client-side ketika JavaScript menggunakan input pengguna secara tidak aman. Gunakan textContent daripada innerHTML, atau sanitasi dengan DOMPurify jika perlu manipulasi HTML.

<!-- ✗ VULNERABLE: DOM-based XSS -->
<script>
  const username = new URLSearchParams(window.location.search).get('user');
  document.getElementById('greeting').innerHTML = `Hello, ${username}!`;
  <!-- URL: ?user=<img src=x onerror=alert('XSS')> -->
</script>

<!-- SECURE: Use textContent instead of innerHTML -->
<script>
  const username = new URLSearchParams(window.location.search).get('user');
  document.getElementById('greeting').textContent = `Hello, ${username}!`;
</script>

<!-- SECURE: Use DOMPurify jika innerHTML necessary -->
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/purify.min.js"></script>
<script>
  const username = new URLSearchParams(window.location.search).get('user');
  const clean = DOMPurify.sanitize(username);
  document.getElementById('greeting').innerHTML = `Hello, ${clean}!`;
</script>

CSRF terjadi ketika penyerang menipu pengguna untuk membuat request yang tidak diinginkan. CSRF tokens dan SameSite cookies mencegah serangan ini.

Cross-Site Request Forgery (CSRF)

// ✗ RENTAN: CSRF Attack
app.post('/transfer', (req, res) => {
  // Pengguna login, mengklik link berbahaya di situs lain
  // POST /transfer { amount: 1000, to: '[email protected]' }
  // Penyerang secara diam-diam mentransfer uang!
  transferMoney(req.body.amount, req.body.to);
});

// ✓ AMAN: CSRF Tokens
const cookieParser = require('cookie-parser');
const csrf = require('csurf');

const csrfProtection = csrf({ cookie: true });

app.post('/transfer', csrfProtection, (req, res) => {
  const csrfToken = req.body._csrf;
  
  // Token divalidasi otomatis oleh middleware
  if (csrfToken !== req.csrfToken()) {
    return res.status(403).json({ error: 'CSRF validation failed' });
  }
  
  transferMoney(req.body.amount, req.body.to);
});

// ✓ AMAN: SameSite Cookies (pendekatan modern)
app.use((req, res, next) => {
  res.cookie('sessionId', req.sessionID, {
    httpOnly: true,
    secure: true,
    sameSite: 'Strict'  // Hanya kirim dalam konteks same-site
  });
  next();
});

Broken Authentication

Autentikasi yang rusak memungkinkan penyerang untuk menebak password, bypass fitur login, atau mengambil alih akun pengguna. Implementasi yang tepat meliputi penyimpanan password yang aman, session management yang baik, dan multi-factor authentication.

Weak Password Storage

Menyimpan password sebagai teks polos adalah kesalahan fatal. Gunakan fungsi hashing yang kuat seperti bcrypt atau argon2 dengan salt untuk melindungi password. Setiap password harus di-hash secara unik dengan salt yang berbeda.

// ✗ RENTAN: Password teks polos
app.post('/register', (req, res) => {
  db.users.create({
    email: req.body.email,
    password: req.body.password  // JANGAN PERNAH!
  });
});

// ✗ RENTAN: Hashing sederhana (tanpa salt)
const crypto = require('crypto');
const hash = crypto.createHash('sha256').update(password).digest('hex');

// ✓ AMAN: bcrypt dengan salt
const bcrypt = require('bcrypt');

app.post('/register', async (req, res) => {
  const saltRounds = 10;
  const hashedPassword = await bcrypt.hash(req.body.password, saltRounds);
  
  db.users.create({
    email: req.body.email,
    password: hashedPassword
  });
});

// ✓ AMAN: Verifikasi login
app.post('/login', async (req, res) => {
  const user = await db.users.findOne({ email: req.body.email });
  
  if (!user || !await bcrypt.compare(req.body.password, user.password)) {
    return res.status(401).json({ error: 'Kredensial tidak valid' });
  }
  
  // Generate JWT atau session
  const token = jwt.sign({ userId: user.id }, process.env.JWT_SECRET);
  res.json({ token });
});

Insecure Session Management

Session yang tidak aman bisa diambil alih penyerang melalui session fixation atau hijacking. Gunakan session ID yang kriptografis acak, set secure dan httpOnly flags pada cookies, dan simpan session data di server (Redis, database) bukan di client.

// ✗ RENTAN: Session ID yang dapat diprediksi
const sessionId = Math.random().toString();  // Tidak kriptografis aman!

// ✓ AMAN: Penanganan session yang aman dengan express-session
const session = require('express-session');
const RedisStore = require('connect-redis').default;
const redis = require('redis');

const redisClient = redis.createClient();

app.use(session({
  store: new RedisStore({ client: redisClient }),
  secret: process.env.SESSION_SECRET,
  resave: false,
  saveUninitialized: false,
  cookie: {
    secure: true,      // HTTPS only
    httpOnly: true,    // Tidak bisa diakses JavaScript
    sameSite: 'Strict',
    maxAge: 24 * 60 * 60 * 1000  // 24 jam
  }
}));

Multi-Factor Authentication (MFA)

MFA menambah lapisan keamanan dengan memerlukan verifikasi kedua selain password. TOTP (Time-based One-Time Password) adalah metode yang aman di mana pengguna pakai aplikasi autentikator untuk generate kode yang berubah setiap 30 detik.

const speakeasy = require('speakeasy');
const qrcode = require('qrcode');

// Generate TOTP secret
app.post('/mfa/setup', async (req, res) => {
  const secret = speakeasy.generateSecret({
    name: `MyApp (${req.user.email})`,
    issuer: 'MyApp'
  });
  
  const qr = await qrcode.toDataURL(secret.otpauth_url);
  
  res.json({
    qr,
    secret: secret.base32,
    message: 'Pindai dengan aplikasi autentikator'
  });
});

// Verifikasi TOTP token
app.post('/mfa/verify', (req, res) => {
  const verified = speakeasy.totp.verify({
    secret: req.user.mfaSecret,
    encoding: 'base32',
    token: req.body.token,
    window: 2
  });
  
  if (!verified) {
    return res.status(401).json({ error: 'Token MFA tidak valid' });
  }
  
  res.json({ success: true });
});

Sensitive Data Exposure

Data sensitif seperti password, SSN, nomor kartu kredit harus dilindungi dengan enkripsi. HTTPS melindungi data saat transit, sementara enkripsi database melindungi data saat rest. PII juga harus disembunyikan di logs dan error messages.

HTTPS dan Encryption

Enkripsi data saat transit menggunakan HTTPS/TLS bisa mencegah man-in-the-middle attacks. Enkripsi data di database melindungi jika database terkompromikan. Untuk data sangat sensitif seperti SSN atau nomor kartu kredit, enkripsi di level aplikasi diperlukan.

// ✓ AMAN: Paksakan HTTPS
app.use((req, res, next) => {
  if (req.header('x-forwarded-proto') !== 'https') {
    res.redirect(`https://${req.header('host')}${req.url}`);
  } else {
    next();
  }
});

// ✓ AMAN: HSTS header
app.use((req, res, next) => {
  res.setHeader('Strict-Transport-Security', 
    'max-age=31536000; includeSubDomains; preload');
  next();
});

// ✓ AMAN: Enkripsi data sensitif di database
const crypto = require('crypto');

function encryptField(field) {
  const cipher = crypto.createCipher('aes-256-cbc', process.env.ENCRYPTION_KEY);
  let encrypted = cipher.update(field, 'utf8', 'hex');
  encrypted += cipher.final('hex');
  return encrypted;
}

function decryptField(encrypted) {
  const decipher = crypto.createDecipher('aes-256-cbc', process.env.ENCRYPTION_KEY);
  let decrypted = decipher.update(encrypted, 'hex', 'utf8');
  decrypted += decipher.final('utf8');
  return decrypted;
}

// Simpan SSN terenkripsi
const ssn = encryptField('123-45-6789');
db.users.update({ ssn }, { where: { id: userId } });

PII Protection

Personally Identifiable Information (PII) seperti SSN, email, nomor telepon, dan tanggal lahir harus dilindungi. Jangan expose PII di logs, error messages, atau URLs. Gunakan masking untuk tampilan dan simpan secrets di environment variables.

// ✗ RENTAN: Mengexpose PII di logs
console.log('User data:', user);  // Bisa log passwords, SSN, dll!

// ✓ AMAN: Mask atau exclude field sensitif
const maskUser = (user) => {
  return {
    id: user.id,
    name: user.name,
    email: user.email.replace(/(.{2}).+(@.+)/, '$1***$2'),
    ssn: '***-**-' + user.ssn.slice(-4)
  };
};

console.log('User data:', maskUser(user));

// ✓ AMAN: Environment variables untuk secrets
require('dotenv').config();

const dbPassword = process.env.DB_PASSWORD;
const apiKey = process.env.API_KEY;

// JANGAN commit .env files!
// Tambahkan .env ke .gitignore

Access Control Vulnerabilities

Kerentanan access control terjadi ketika pengguna dapat mengakses resource atau fitur yang tidak seharusnya mereka akses. Ada dua jenis utama: horizontal (user akses data user lain) dan vertical (user mengakses fitur admin).

Horizontal Access Control

Horizontal bypass terjadi ketika user yang autentik bisa mengakses data user lain dengan memanipulasi parameter seperti user ID. Selalu verifikasi bahwa user punya hak akses ke resource spesifik yang diminta sebelum mengembalikan data.

// ✗ RENTAN: Bypass kontrol akses horizontal
app.get('/user/:id', (req, res) => {
  const user = db.users.findById(req.params.id);
  res.json(user);  // User yang autentik dapat akses user manapun!
});

// ✓ AMAN: Cek user memiliki resource
app.get('/user/:id', (req, res) => {
  if (req.params.id != req.user.id) {
    return res.status(403).json({ error: 'Akses ditolak' });
  }
  
  const user = db.users.findById(req.params.id);
  res.json(user);
});

Vertical Access Control

Vertical bypass terjadi ketika user biasa bisa akses fungsi atau resource yang hanya untuk admin. Implementasi role-based access control (RBAC) dengan middleware untuk verifikasi role pada setiap endpoint yang sensitif.

// ✗ RENTAN: Bypass kontrol akses vertical
app.get('/admin/users', (req, res) => {
  if (!req.user) {
    return res.status(401).json({ error: 'Tidak autentik' });
  }
  // Tidak cek role!
  const users = db.users.find();
  res.json(users);
});

// ✓ AMAN: Cek role user
app.get('/admin/users', (req, res) => {
  if (!req.user || req.user.role !== 'ADMIN') {
    return res.status(403).json({ error: 'Akses ditolak' });
  }
  
  const users = db.users.find();
  res.json(users);
});

// ✓ AMAN: Middleware untuk akses berbasis role
const requireRole = (roles) => (req, res, next) => {
  if (!req.user || !roles.includes(req.user.role)) {
    return res.status(403).json({ error: 'Akses ditolak' });
  }
  next();
};

app.get('/admin/users', requireRole(['ADMIN']), (req, res) => {
  res.json(db.users.find());
});

Cryptography dan Encryption

Kriptografi modern menggunakan algoritma standar industri (AES, RSA, ECDSA) untuk melindungi data. Keys harus di-generate secara kriptografis aman dan di-manage dengan hati-hati. Jangan hardcode keys di code atau repository.

Secure Key Management

Keys harus diturunkan dari password menggunakan Key Derivation Function (KDF) seperti PBKDF2 atau Argon2 dengan salt yang unik dan iterasi tinggi. Untuk production, gunakan hardware security modules (HSM) atau key management services dari cloud provider untuk menyimpan master keys.

// ✓ AMAN: Key derivation function
const crypto = require('crypto');
const pbkdf2 = require('pbkdf2');

const password = 'user_password';
const salt = crypto.randomBytes(32);

const key = pbkdf2.pbkdf2Sync(password, salt, 100000, 32, 'sha256');

// Simpan salt + key
db.users.update({
  salt: salt.toString('hex'),
  key: key.toString('hex')
}, { where: { id: userId } });

// ✓ AMAN: Gunakan hardware security modules (HSM) untuk production
// Simpan master encryption keys di HSM, jangan di application code

Security Headers

Security headers adalah instruksi ke browser tentang kebijakan keamanan aplikasi. Headers seperti Content-Security-Policy (CSP), X-Frame-Options, dan HSTS melindungi dari berbagai serangan seperti XSS, clickjacking, dan HTTPS downgrade.

app.use((req, res, next) => {
  // Content Security Policy (cegah XSS)
  res.setHeader('Content-Security-Policy', 
    "default-src 'self'; script-src 'self' trusted-site.com; style-src 'self' 'unsafe-inline'");
  
  // X-Content-Type-Options (cegah MIME sniffing)
  res.setHeader('X-Content-Type-Options', 'nosniff');
  
  // X-Frame-Options (cegah clickjacking)
  res.setHeader('X-Frame-Options', 'DENY');
  
  // X-XSS-Protection (perlindungan XSS legacy)
  res.setHeader('X-XSS-Protection', '1; mode=block');
  
  // Referrer-Policy (kontrol informasi referrer)
  res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
  
  // Permissions-Policy (kontrol fitur browser)
  res.setHeader('Permissions-Policy', 'geolocation=(), microphone=(), camera=()');
  
  next();
});

Input Validation dan Output Encoding

Input validation memastikan data masuk sesuai format dan constraint yang diharapkan. Output encoding mengkonversi karakter khusus saat merender untuk mencegah XSS. Kedua langkah ini merupakan defense-in-depth yang penting untuk melawan injection attacks.

const { body, validationResult } = require('express-validator');
const xss = require('xss');

// ✓ AMAN: Input validation
app.post('/post', [
  body('title')
    .trim()
    .notEmpty().withMessage('Judul diperlukan')
    .isLength({ max: 200 }).withMessage('Judul terlalu panjang')
    .matches(/^[a-zA-Z0-9\s]+$/).withMessage('Karakter tidak valid'),
  
  body('content')
    .trim()
    .notEmpty()
    .isLength({ max: 5000 })
], (req, res) => {
  const errors = validationResult(req);
  if (!errors.isEmpty()) {
    return res.status(400).json({ errors: errors.array() });
  }
  
  // ✓ AMAN: Output encoding
  const sanitized = {
    title: xss(req.body.title),
    content: xss(req.body.content, {
      whiteList: { b: [], i: [], u: [] }  // Izinkan hanya tag tertentu
    })
  };
  
  db.posts.create(sanitized);
  res.json({ success: true });
});

Kesimpulan

Web security adalah proses berkelanjutan yang memerlukan kewaspadaan, pengetahuan, dan mindset proaktif. Dengan memahami OWASP Top 10, menerapkan best practice keamanan, dan selalu terinformasi tentang ancaman baru, kamu bisa secara signifikan mengurangi risiko terhadap serangan dan melindungi data pengguna dengan efektif.

Security Checklist untuk Deployment Production:

  • ✓ Gunakan HTTPS di mana-mana (sertifikat SSL/TLS)
  • ✓ Implementasi input validation dan output encoding yang tepat
  • ✓ Gunakan parameterized queries untuk mencegah SQL injection
  • ✓ Implementasi proteksi CSRF dengan tokens atau SameSite cookies
  • ✓ Simpan password dengan bcrypt atau argon2 (dengan salt)
  • ✓ Implementasi rate limiting untuk mencegah brute force
  • ✓ Gunakan security headers (CSP, X-Frame-Options, HSTS, dll)
  • ✓ Implementasi MFA untuk akun admin
  • ✓ Security audits dan penetration testing secara berkala
  • ✓ Perbarui dependencies (patch management)
  • ✓ Implementasi comprehensive logging dan monitoring
  • ✓ Miliki incident response plan
  • ✓ Security training untuk team members
  • ✓ Security code reviews secara berkala
  • ✓ Enkripsi data sensitif baik in transit maupun at rest

Pendekatan security first adalah best practice!