Web Security Essentials — Panduan Lengkap Melindungi Aplikasi Web

Web Security Essentials — Panduan Lengkap Melindungi Aplikasi Web

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

Pengenalan: Security adalah Responsibility, Bukan Afterthought

Dalam era digital yang terus berkembang, keamanan web application bukan lagi optional luxury tetapi critical necessity. Setiap hari, cybercriminals mengeksploitasi vulnerabilities di web applications untuk steal data, disrupt services, atau cause financial losses. Sebagai developer, sysadmin, atau architect, responsibility Kamu untuk understand dan implement security best practices dalam setiap layer aplikasi.

OWASP (Open Web Application Security Project) secara regular menerbitkan Top 10 list dari vulnerabilities paling dangerous yang ditemukan di production applications. Understanding vulnerabilities ini dan cara meproteksi against them adalah essential knowledge yang setiap developer modern harus punya. Dalam panduan comprehensive ini, kita akan explore OWASP Top 10, protection mechanisms, encryption, authentication, dan production security strategies.

Daftar Isi

OWASP Top 10 Overview

1. Injection (SQL, NoSQL, OS Command)
2. Broken Authentication
3. Sensitive Data Exposure
4. XML External Entities (XXE)
5. Broken Access Control
6. Security Misconfiguration
7. Cross-Site Scripting (XSS)
8. Insecure Deserialization
9. Using Components with Known Vulnerabilities
10. Insufficient Logging & Monitoring

Injection Attacks: SQL, NoSQL, OS

Injection attacks terjadi ketika untrusted data dikirim ke interpreter sebagai command. Ini paling common dan dangerous vulnerability.

SQL Injection

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

// ✓ SECURE: 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);
  });
});

// ✓ SECURE: 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

// ✗ VULNERABLE: NoSQL Injection
app.post('/login', (req, res) => {
  db.users.findOne({ 
    email: req.body.email,
    password: req.body.password 
  }).then(user => {
    // Attacker: { email: { $ne: null }, password: { $ne: null } }
    // Returns first user regardless of credentials!
  });
});

// ✓ SECURE: 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
  });
});

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

// Mongoose automatically prevents NoSQL injection

OS Command Injection

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

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

// ✓ BETTER: Avoid shell commands altogether
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 not found' });
  }
});

Cross-Site Scripting (XSS)

XSS terjadi ketika attacker inject malicious scripts yang executed di browser user. Ada tiga tipe: Stored, Reflected, dan DOM-based.

Stored XSS

// ✗ VULNERABLE: Stored XSS
app.post('/comment', (req, res) => {
  // Store user input directly
  db.comments.create({
    text: req.body.text,  // Could be: <script>alert('XSS')</script>
    userId: req.user.id
  });
});

// Render without escaping
app.get('/post/:id', (req, res) => {
  const post = db.posts.findById(req.params.id);
  res.render('post', { post }); // If template doesn't escape, XSS fires!
});

// ✓ SECURE: 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
  });
});

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

Reflected XSS

Reflected XSS terjadi ketika user input langsung reflected dalam response. Attacker menipu user untuk click malicious link.

// ✗ VULNERABLE: Reflected XSS
app.get('/search', (req, res) => {
  const query = req.query.q;
  // Search: /?q=<img%20src=x%20onerror=alert('XSS')>
  res.send(`Search results for: ${query}`);
});

// ✓ SECURE: Escape special characters
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(`Search results for: ${escaped}`);
});

// ✓ BETTER: Use template engine dengan auto-escaping
// EJS, Nunjucks, etc. auto-escape by default
res.render('search', { query: req.query.q });

DOM-based XSS terjadi ketika JavaScript code construct HTML dari user input. Selalu gunakan safe DOM methods atau sanitization libraries.

DOM-based XSS

<!-- ✗ 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 attacker tricks user untuk make unwanted requests. CSRF tokens dan SameSite cookies prevent attack ini.

Cross-Site Request Forgery (CSRF)

// ✗ VULNERABLE: CSRF Attack
app.post('/transfer', (req, res) => {
  // User logged in, click malicious link on another site
  // POST /transfer { amount: 1000, to: '[email protected]' }
  // Attacker silently transfers money!
  transferMoney(req.body.amount, req.body.to);
});

// ✓ SECURE: 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 validated automatically by middleware
  if (csrfToken !== req.csrfToken()) {
    return res.status(403).json({ error: 'CSRF validation failed' });
  }
  
  transferMoney(req.body.amount, req.body.to);
});

// ✓ SECURE: SameSite Cookies (modern approach)
app.use((req, res, next) => {
  res.cookie('sessionId', req.sessionID, {
    httpOnly: true,
    secure: true,
    sameSite: 'Strict'  // Only send in same-site context
  });
  next();
});

Broken Authentication

Weak Password Storage

// ✗ VULNERABLE: Plain text passwords
app.post('/register', (req, res) => {
  db.users.create({
    email: req.body.email,
    password: req.body.password  // NEVER EVER!
  });
});

// ✗ VULNERABLE: Simple hashing (no salt)
const crypto = require('crypto');
const hash = crypto.createHash('sha256').update(password).digest('hex');

// ✓ SECURE: 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
  });
});

// ✓ SECURE: Login verification
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: 'Invalid credentials' });
  }
  
  // Generate JWT atau session
  const token = jwt.sign({ userId: user.id }, process.env.JWT_SECRET);
  res.json({ token });
});

Insecure Session Management

// ✗ VULNERABLE: Predictable session IDs
const sessionId = Math.random().toString();  // Not cryptographically secure!

// ✓ SECURE: Secure session handling 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,    // No JavaScript access
    sameSite: 'Strict',
    maxAge: 24 * 60 * 60 * 1000  // 24 hours
  }
}));

Multi-Factor Authentication (MFA)

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: 'Scan with authenticator app'
  });
});

// Verify 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: 'Invalid MFA token' });
  }
  
  res.json({ success: true });
});

Sensitive Data Exposure

HTTPS dan Encryption

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

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

// ✓ SECURE: Encrypt sensitive data 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;
}

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

PII Protection

// ✗ VULNERABLE: Exposing PII di logs
console.log('User data:', user);  // Could log passwords, SSN, etc!

// ✓ SECURE: Mask atau exclude sensitive fields
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));

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

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

// NEVER commit .env files!
// Add .env ke .gitignore

Access Control Vulnerabilities

Horizontal Access Control

// ✗ VULNERABLE: Horizontal access control bypass
app.get('/user/:id', (req, res) => {
  const user = db.users.findById(req.params.id);
  res.json(user);  // Any authenticated user dapat access any user!
});

// ✓ SECURE: Check user owns the resource
app.get('/user/:id', (req, res) => {
  if (req.params.id != req.user.id) {
    return res.status(403).json({ error: 'Access denied' });
  }
  
  const user = db.users.findById(req.params.id);
  res.json(user);
});

Vertical Access Control

// ✗ VULNERABLE: Vertical access control bypass
app.get('/admin/users', (req, res) => {
  if (!req.user) {
    return res.status(401).json({ error: 'Not authenticated' });
  }
  // Missing role check!
  const users = db.users.find();
  res.json(users);
});

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

// ✓ SECURE: Middleware untuk role-based access
const requireRole = (roles) => (req, res, next) => {
  if (!req.user || !roles.includes(req.user.role)) {
    return res.status(403).json({ error: 'Access denied' });
  }
  next();
};

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

Cryptography dan Encryption

Secure Key Management

// ✓ SECURE: 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');

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

// ✓ SECURE: Use hardware security modules (HSM) para production
// Store master encryption keys in HSM, never in application code

Security Headers

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

Input Validation dan Output Encoding

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

// ✓ SECURE: Input validation
app.post('/post', [
  body('title')
    .trim()
    .notEmpty().withMessage('Title required')
    .isLength({ max: 200 }).withMessage('Title too long')
    .matches(/^[a-zA-Z0-9\s]+$/).withMessage('Invalid characters'),
  
  body('content')
    .trim()
    .notEmpty()
    .isLength({ max: 5000 })
], (req, res) => {
  const errors = validationResult(req);
  if (!errors.isEmpty()) {
    return res.status(400).json({ errors: errors.array() });
  }
  
  // ✓ SECURE: Output encoding
  const sanitized = {
    title: xss(req.body.title),
    content: xss(req.body.content, {
      whiteList: { b: [], i: [], u: [] }  // Allow only certain tags
    })
  };
  
  db.posts.create(sanitized);
  res.json({ success: true });
});

Kesimpulan

Web security adalah continuous process yang requires vigilance, knowledge, dan proactive mindset. Dengan understanding OWASP Top 10, implementing security best practices, dan staying informed tentang emerging threats, Kamu dapat significantly reduce risk terhadap attacks dan protect user data effectively.

Security Checklist untuk Production Deployment:

  • ✓ Use HTTPS everywhere (SSL/TLS certificates)
  • ✓ Implement proper input validation dan output encoding
  • ✓ Use parameterized queries untuk prevent SQL injection
  • ✓ Implement CSRF protection dengan tokens atau SameSite cookies
  • ✓ Store passwords dengan bcrypt atau argon2 (dengan salt)
  • ✓ Implement rate limiting untuk prevent brute force
  • ✓ Use security headers (CSP, X-Frame-Options, HSTS, dll)
  • ✓ Implement MFA untuk admin accounts
  • ✓ Regular security audits dan penetration testing
  • ✓ Keep dependencies updated (patch management)
  • ✓ Implement comprehensive logging dan monitoring
  • ✓ Have incident response plan
  • ✓ Security training untuk team members
  • ✓ Regular security code reviews
  • ✓ Encrypt sensitive data both in transit dan at rest

Prevention:

// Use CSRF tokens
<form method="POST">
  <input type="hidden" name="csrf_token" value="TOKEN">
</form>

Authentication & Authorization

  • Use HTTPS everywhere
  • Hash passwords with bcrypt
  • Use secure cookies
  • Implement rate limiting
  • Use JWTs carefully

OWASP Top 10

  1. Injection
  2. Broken authentication
  3. Sensitive data exposure
  4. XML External Entities
  5. Broken Access Control
  6. Security misconfiguration
  7. XSS
  8. Insecure deserialization
  9. Using components with vulnerabilities
  10. Insufficient logging

Security first approach adalah best practice!