Bảo mật WebWeb Security
Thuộc bộ kiến thức Backend Roadmap.
Tổng quan
Bảo mật web không phải là một tính năng bạn gắn thêm vào lúc cuối; nó là thuộc tính của mọi quyết định bạn đưa ra khi xây dựng backend — cách bạn truy vấn database, cách bạn render output, cách bạn lưu secrets, cách bạn cấu hình headers, và cách bạn tin (hoặc từ chối tin) dữ liệu đi qua ranh giới hệ thống. Chỉ một câu query không parameterized hoặc một header bị thiếu cũng đủ biến một service vững chắc thành một vụ rò rỉ dữ liệu.
Bài viết này mang tính phòng thủ (defensive). Mục tiêu là nhận diện được hình dạng của từng loại lỗ hổng để phòng ngừa và giảm thiểu — không phải để khai thác. Với mỗi rủi ro, chúng ta mô tả cách nó xảy ra rồi đến biện pháp kiểm soát cụ thể để vô hiệu hóa nó.
Chúng ta xây dựng mô hình tư duy theo từng lớp:
- Tư duy bảo mật (security mindset) — một tập hợp nhỏ các nguyên tắc (defense in depth, least privilege, secure defaults, never trust input) sinh ra hầu hết các quy tắc cụ thể.
- OWASP Top 10 — danh sách chuẩn ngành về các rủi ro thực sự gây ra breach, mỗi rủi ro kèm một câu phòng thủ.
- Các lớp lỗ hổng phổ biến — injection, XSS, CSRF, broken access control, SSRF, misconfiguration, sensitive data exposure — với ví dụ an toàn và không an toàn.
- Các cơ chế kiểm soát ở browser và transport — CORS, CSP, HTTPS/TLS, HSTS, và bộ security headers.
- Hardening — quản lý secrets, dependency/supply-chain security, rate limiting, và hashing đúng mục đích.
Authentication, session management, và password hashing được trình bày chi tiết trong Authentication & Authorization; các vấn đề riêng của API trong APIs; và cơ chế HTTP/TLS nền tảng trong Internet & Web Fundamentals. Bài này liên kết đến cả ba thay vì lặp lại.
Kiến thức nền tảng
Tư duy bảo mật
Hầu hết các quy tắc bảo mật cụ thể đều bắt nguồn từ bốn nguyên tắc. Nắm vững chúng, bạn có thể suy luận về những lỗ hổng mà bạn chưa từng thấy trong bất kỳ checklist nào.
- Defense in depth (phòng thủ nhiều lớp). Không bao giờ dựa vào một biện pháp kiểm soát duy nhất. Giả định rằng bất kỳ lớp nào cũng có thể thất bại và đảm bảo lớp tiếp theo vẫn bảo vệ bạn. Validate input và dùng parameterized queries và chạy DB user với quyền tối thiểu và mã hóa dữ liệu at rest. Nếu WAF bị vượt qua, query vẫn được parameterized; nếu query bị lỗi, DB user vẫn không thể drop bảng.
- Least privilege (đặc quyền tối thiểu). Mỗi thành phần — một DB account, một API token, một service, một container, một con người — chỉ nhận đúng quyền tối thiểu cần thiết để làm việc của nó, không hơn. Một reporting service chỉ đọc thì nên có DB role read-only. Một CI token chỉ deploy một app thì không nên là org admin. Điều này thu hẹp phạm vi thiệt hại (blast radius) khi có bất kỳ sự xâm phạm nào.
- Secure defaults (mặc định an toàn). Cấu hình an toàn phải là cái bạn có được khi không làm gì cả. Endpoint mới thì mặc định deny cho đến khi được cho phép rõ ràng. Cookies là
Secure/HttpOnlytrừ khi có lý do khác. TLS bật ở mọi nơi. Debug mode, error chi tiết, và default admin credentials đều tắt trong production. Nếu bảo mật đòi hỏi phải nhớ gạt một công tắc, sẽ có người quên. - Never trust input (không bao giờ tin input). Coi mọi byte đến từ bên ngoài ranh giới tin cậy (trust boundary) là thù địch cho đến khi được chứng minh ngược lại — request body, query string, header, cookie, file upload, và thậm chí cả dữ liệu từ các internal service khác. Validate ở phía server (kiểm tra ở client chỉ là UX, không phải bảo mật). Ưu tiên allow-list (“chỉ những giá trị này được phép”) hơn deny-list (“mọi thứ trừ những cái này”), vì bạn không thể liệt kê hết mọi input xấu.
Hai ý tưởng bổ trợ hoàn thiện bức tranh:
- Fail securely (thất bại an toàn). Khi có sự cố, hãy fail đóng (closed), không phải mở (open). Một authorization check ném ra exception phải dẫn đến từ chối, không phải cho phép. Đừng để lộ stack trace, internal path, hay SQL cho người dùng.
- Minimize attack surface (giảm bề mặt tấn công). Mỗi port mở, tính năng bật, dependency, và endpoint đều là thứ bạn phải phòng thủ. Tắt những gì bạn không dùng.
Bộ ba CIA và trust boundaries
Các biện pháp kiểm soát bảo mật tồn tại để giữ ba thuộc tính: Confidentiality (chỉ bên được phép mới đọc được dữ liệu), Integrity (dữ liệu không thể bị sửa đổi mà không bị phát hiện), và Availability (hệ thống vẫn dùng được). Mỗi lỗ hổng phá vỡ một hoặc nhiều thuộc tính này — một SQL injection dump bảng users phá Confidentiality; một CSRF đổi email phá Integrity; một endpoint không giới hạn bị dội bom phá Availability.
Một trust boundary là bất kỳ đường ranh nào nơi dữ liệu hoặc quyền điều khiển đi qua giữa các vùng tin cậy khác nhau — browser → server, server → database, service của bạn → third-party API. Lỗ hổng tập trung ở các ranh giới này vì đó là nơi các giả định (“client đã validate rồi”) bị vi phạm. Hãy vẽ ranh giới một cách rõ ràng và đặt một biện pháp kiểm soát ở mỗi điểm giao cắt.
Khái niệm chính
OWASP Top 10 (2021)
OWASP Top 10 là tài liệu nâng cao nhận thức được tham chiếu rộng rãi nhất về rủi ro của ứng dụng web. Hãy học nó như một checklist “mình đã nghĩ đến cái này chưa?”. Mỗi dòng dưới đây ghép rủi ro với biện pháp phòng thủ chính của nó.
| # | Rủi ro | Là gì | Câu phòng thủ |
|---|---|---|---|
| A01 | Broken Access Control | Người dùng hành động ngoài quyền được cấp (IDOR, thiếu kiểm tra, leo thang đặc quyền) | Deny by default; enforce authorization ở server trên mọi request theo user đã xác thực |
| A02 | Cryptographic Failures | Dữ liệu nhạy cảm lộ ra do crypto yếu/thiếu (plaintext, cipher yếu, không TLS) | Mã hóa in transit (TLS) và at rest; dùng thuật toán mạnh, chuẩn; đừng tự viết crypto |
| A03 | Injection | Input không tin cậy bị diễn giải thành code/lệnh (SQL, NoSQL, OS, LDAP) | Parameterized queries / prepared statements; validate và encode input |
| A04 | Insecure Design | Thiếu hoặc sai sót các biện pháp bảo mật ở mức thiết kế | Threat-model sớm; dùng secure design pattern và kiến trúc tham chiếu |
| A05 | Security Misconfiguration | Default creds, error chi tiết, cloud bucket mở, tính năng thừa | Cấu hình hardened, lặp lại được; least functionality; tắt defaults; tự động hóa |
| A06 | Vulnerable & Outdated Components | Thư viện/framework/OS package có lỗ hổng đã biết | Kiểm kê dependencies; vá kịp thời; quét (SCA) trong CI |
| A07 | Identification & Authentication Failures | Xử lý login, session, hoặc credential yếu | Auth mạnh, MFA, session management an toàn — xem ./07 |
| A08 | Software & Data Integrity Failures | Update không xác thực, deserialization không an toàn, CI/CD bị xâm phạm | Xác minh chữ ký/integrity; bảo vệ pipeline; tránh deserialization không an toàn |
| A09 | Security Logging & Monitoring Failures | Tấn công không bị phát hiện do thiếu log/alert | Log security event; tập trung hóa, alert, và lưu trữ; đừng log secrets |
| A10 | Server-Side Request Forgery (SSRF) | Server bị lừa gửi request đến đích ngoài ý muốn | Validate/allow-list URL đi ra; chặn dải nội bộ và metadata endpoint |
Injection (SQL / NoSQL / command)
Injection xảy ra khi input không tin cậy bị nối vào lệnh của một interpreter — một câu SQL, một shell command, một NoSQL filter — khiến input có thể thay đổi cấu trúc của lệnh thay vì chỉ được coi là dữ liệu thuần túy. Cách khắc phục luôn là cùng một ý tưởng: tách biệt code và data.
SQL injection — không an toàn (đừng bao giờ làm thế này):
# Nối chuỗi khiến input trở thành SQL. Input kiểu ' OR '1'='1
# biến mệnh đề WHERE thành thứ luôn khớp.
username = request.args["username"]
query = "SELECT * FROM users WHERE username = '" + username + "'"
cursor.execute(query) # DỄ BỊ TẤN CÔNG
SQL injection — an toàn (parameterized query / prepared statement):
# Driver gửi cấu trúc query và giá trị một cách riêng biệt.
# Giá trị không bao giờ bị parse thành SQL — nó luôn chỉ là data.
username = request.args["username"]
cursor.execute(
"SELECT * FROM users WHERE username = %s", # placeholder
(username,), # tham số được bind
)
Tương tự ở các hệ sinh thái khác:
// Node.js (pg) — parameterized
await client.query("SELECT * FROM users WHERE username = $1", [username]);
// Java (JDBC) — PreparedStatement
PreparedStatement ps = conn.prepareStatement(
"SELECT * FROM users WHERE username = ?");
ps.setString(1, username);
Quy tắc phòng thủ injection:
- Luôn parameterize. Dùng prepared statement / bound parameter cho mọi query có input biến đổi. Chỉ riêng điều này đã chặn phần lớn tuyệt đối các SQLi.
- Dùng ORM/query builder đúng cách. Các ORM (SQLAlchemy, Prisma, Hibernate, ActiveRecord) parameterize mặc định — nhưng các “cửa thoát” raw-query (
.raw(), nội suy chuỗi vàoWHERE) lại mở lại lỗ hổng. Đừng bao giờ ghép chuỗi SQL từ input người dùng, kể cả bên trong ORM. - Validate và allow-list. Khi input thuộc một tập đã biết (một cột sort, một hướng order), hãy ánh xạ qua allow-list thay vì truyền thẳng — bạn không thể parameterize một identifier như tên bảng hay tên cột.
- NoSQL injection. MongoDB và tương tự cũng bị injectable khi một query object được dựng từ raw input (ví dụ kẻ tấn công gửi
{"$gt": ""}ở chỗ bạn mong đợi một chuỗi). Ép kiểu/validate kiểu dữ liệu, từ chối operator object ở chỗ mong đợi scalar, và dùng typed query API của driver. - OS command injection. Tránh gọi shell với input người dùng. Nếu bắt buộc, truyền tham số dưới dạng mảng (không bao giờ là shell string), dùng API không gọi shell (
subprocess.run([...], shell=False)), và allow-list lệnh cùng tham số của nó.
Cross-Site Scripting (XSS)
XSS là injection vào browser: dữ liệu do kẻ tấn công kiểm soát lọt vào trang dưới dạng script thực thi được, chạy trong session của nạn nhân với cookie và quyền của họ. Có ba biến thể:
| Loại | Payload nằm ở đâu | Ví dụ vector |
|---|---|---|
| Stored | Lưu ở server, phục vụ cho nhiều user | Comment độc lưu vào DB, hiển thị cho mọi người xem |
| Reflected | Bật lại từ server theo request | Payload trong URL/search param được echo vào trang |
| DOM-based | Không tới server; JS ở client ghi ra | innerHTML = location.hash trong code front-end |
Các biện pháp phòng thủ, theo lớp:
- Contextual output encoding là biện pháp chính. Encode dữ liệu theo đúng ngữ cảnh nó rơi vào — HTML body, HTML attribute, JavaScript, URL, CSS đều có quy tắc escaping khác nhau. Các template engine hiện đại (React JSX, Vue, Jinja2 với autoescape, Thymeleaf, Razor) escape HTML mặc định. Hãy để chúng làm việc đó.
- Đừng tắt escaping.
dangerouslySetInnerHTML(React),v-html(Vue),|safe(Jinja),innerHTML— mỗi cái đều là lời mời XSS nếu được cho ăn dữ liệu không tin cậy. Nếu bắt buộc phải render HTML do người dùng cung cấp (một trường rich-text), hãy sanitize nó bằng một thư viện đã được kiểm chứng (DOMPurify) với allow-list các tag/attribute. - Content-Security-Policy (CSP) là lưới an toàn khi encoding bị bỏ sót — xem bên dưới.
- Đặt cookie
HttpOnlyđể ngay cả một XSS thành công cũng không đọc được session cookie quadocument.cookie. - Validate input như một biện pháp phụ (ví dụ một trường URL phải thực sự là URL
http(s), chặn payloadjavascript:).
{# Jinja2 autoescape BẬT (mặc định): an toàn theo thiết kế #}
<p>{{ user_comment }}</p> {# < > & được escape #}
{# NGUY HIỂM — chỉ dùng sau khi đã sanitize user_comment ở server #}
<div>{{ user_comment | safe }}</div>
Cross-Site Request Forgery (CSRF)
CSRF lợi dụng thói quen của browser là tự động đính kèm cookie vào mọi request đến domain của bạn. Nếu một user đang đăng nhập vào site của bạn và ghé thăm trang của kẻ tấn công, trang đó có thể âm thầm submit một form hoặc bắn một request đến app của bạn, và browser gửi kèm session cookie — nên hành động chạy dưới danh nghĩa nạn nhân. CSRF nhắm vào các request thay đổi trạng thái (POST/PUT/DELETE), không phải các thao tác đọc.
Các biện pháp phòng thủ:
- SameSite cookies. Đặt
SameSite=Lax(mặc định hợp lý mà vẫn cho phép điều hướng top-level) hoặcSameSite=Strictcho session cookie. Điều này bảo browser không gửi cookie trên các cross-site request, vô hiệu hóa phần lớn CSRF. Kết hợp vớiSecurevàHttpOnly. - Anti-CSRF token (synchronizer token pattern). Server phát ra một token ngẫu nhiên, theo mỗi session (hoặc mỗi request); form đính kèm nó dưới dạng hidden field, và server từ chối bất kỳ request thay đổi trạng thái nào có token thiếu hoặc sai. Trang của kẻ tấn công không thể đọc được token (same-origin policy), nên không thể giả mạo một request hợp lệ. Hầu hết các framework đều có sẵn (Django
{% csrf_token %}, Railsprotect_from_forgery, Spring Security CSRF). - Double-submit cookie là một biến thể stateless: token được gửi vừa dưới dạng cookie vừa dưới dạng header/field; server kiểm tra chúng khớp nhau.
- Với API dùng bearer token (token trong header
Authorizationthay vì cookie), CSRF phần lớn không áp dụng, vì browser không tự động đính kèm header đó. Chính các session dựa trên cookie mới cần bảo vệ CSRF. - Kiểm tra header
Origin/Referertrên các request nhạy cảm như một tín hiệu phòng thủ nhiều lớp.
Set-Cookie: session=...; HttpOnly; Secure; SameSite=Lax; Path=/
Broken authentication & session management
Các luồng login yếu, session ID dễ đoán, token không bao giờ hết hạn, và credential trong URL cho phép kẻ tấn công trở thành người dùng khác. Vì đây là một chủ đề lớn, phần trình bày đầy đủ — password hashing (bcrypt/argon2), MFA, vòng đời session, các cạm bẫy JWT, OAuth/OIDC — nằm trong Authentication & Authorization. Các quy tắc một dòng: dùng một auth library đã được kiểm chứng, hash password bằng hàm adaptive chậm, xoay vòng và cho hết hạn session, và enforce MFA trên các tài khoản nhạy cảm.
Broken access control & IDOR
Access control quyết định một user đã xác thực được phép làm gì. Nó vỡ khi server tin tưởng client tự enforce giới hạn. Dạng kinh điển là IDOR (Insecure Direct Object Reference): một endpoint kiểu GET /api/invoices/1043 trả về hóa đơn có ID đó mà không kiểm tra nó thuộc về người gọi, nên đổi số thành 1044 sẽ lộ dữ liệu của người khác.
Các biện pháp phòng thủ:
- Enforce authorization ở server, trên mọi request, dựa trên danh tính đã xác thực — không bao giờ dựa vào user ID hay role do client cung cấp.
- Deny by default. Route mới không truy cập được cho đến khi một quy tắc cho phép rõ ràng.
- Kiểm tra quyền sở hữu, không chỉ xác thực. “User này đã đăng nhập chưa?” không phải là “hóa đơn này có thuộc về user này không?”. Giới hạn query theo user hiện tại:
WHERE invoice.id = ? AND invoice.owner_id = ?. - Đừng dựa vào sự mù mờ (obscurity). ID khó đoán (UUID) giúp ích nhưng không phải là access control. Vẫn phải enforce kiểm tra bất kể thế nào.
- Tập trung hóa policy. Middleware/decorator/policy object chạy trên mọi request thì khó quên hơn các kiểm tra
ifrời rạc rải rác trong từng handler.
Server-Side Request Forgery (SSRF)
SSRF xảy ra khi server của bạn fetch một URL do người dùng cung cấp (trực tiếp hoặc gián tiếp) — import ảnh, webhook, PDF renderer, link preview — và kẻ tấn công trỏ nó vào trong: đến http://169.254.169.254/ (cloud metadata, thường chứa credential), đến các admin panel nội bộ, hoặc đến các service localhost vốn tin tưởng lưu lượng local.
Các biện pháp phòng thủ:
- Allow-list các đích đến. Chỉ cho phép request đến các host/domain đã biết, đã dự kiến. Đây là biện pháp mạnh nhất.
- Chặn các dải nội bộ. Từ chối private IP (
10/8,172.16/12,192.168/16,127/8,169.254/16, và tương đương IPv6) sau khi resolve DNS — validate IP đã resolve, không chỉ hostname (để chống DNS rebinding và redirect). - Tắt các URL scheme không cần (
file://,gopher://) và follow redirect một cách thận trọng (re-validate từng hop). - Khóa cloud metadata. Yêu cầu IMDSv2 trên AWS, hoặc chặn metadata IP ở mức network.
- Cho service đi fetch một network egress bị giới hạn riêng.
Security misconfiguration
Nhóm phổ biến nhất trong thực tế: default admin password, bật directory listing, stack trace chi tiết trong production, các service thừa đang chạy, cloud storage quá lỏng (public S3 bucket), thiếu security header, và dev tooling bị lộ. Biện pháp phòng thủ: một cấu hình baseline hardened, được quản lý version; cùng một config trên các môi trường (trừ secrets); tắt mọi thứ bạn không cần; error page chung chung trong production; và tự động scan để phát hiện drift.
Sensitive data exposure / cryptographic failures
Dữ liệu rò rỉ qua lưu trữ plaintext, mã hóa yếu hoặc thiếu, secrets trong log hoặc source control, và API response quá rộng. Biện pháp phòng thủ:
- Phân loại dữ liệu để biết cái gì cần bảo vệ (PII, credential, dữ liệu thanh toán, dữ liệu sức khỏe).
- Mã hóa in transit bằng TLS ở mọi nơi (xem Transport security) và at rest cho các kho dữ liệu nhạy cảm.
- Giảm thiểu những gì bạn thu thập và trả về. Đừng gửi các field client không cần; đừng lưu dữ liệu bạn không cần.
- Giữ secrets ngoài code và log (xem Quản lý secrets).
- Dùng thuật toán và thư viện chuẩn, hiện hành — đừng bao giờ tự phát minh crypto.
CORS (Cross-Origin Resource Sharing)
Same-Origin Policy (SOP) của browser chặn JavaScript ở https://a.com đọc response từ https://b.com. CORS là cơ chế có kiểm soát, do server điều khiển để nới lỏng điều đó — nó cho phép API của bạn báo cho browser biết origin nào khác được phép đọc response của nó. CORS là một lớp enforcement ở browser; nó không bảo vệ server khỏi các client không phải browser (curl, các server), và không thay thế cho authentication hay authorization.
Cách hoạt động. Với các request “simple”, browser gửi request kèm header Origin và kiểm tra Access-Control-Allow-Origin của response trước khi lộ nó cho JS. Với các request có thể thay đổi trạng thái hoặc dùng custom header, browser trước tiên gửi một request preflight OPTIONS để xin phép:
# Preflight request (browser -> API của bạn)
OPTIONS /api/orders HTTP/1.1
Origin: https://app.example.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Content-Type, Authorization
# Preflight response (API của bạn -> browser)
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Allow-Credentials: true
Access-Control-Max-Age: 600
Cấu hình an toàn:
- Allow-list các origin cụ thể. Chỉ phản hồi các origin đã biết là tốt. Không bao giờ kết hợp
Access-Control-Allow-Origin: *vớiAccess-Control-Allow-Credentials: true— browser cấm điều đó, và việc phản hồi origin tùy ý kèm credential thực chất là mở API đã xác thực của bạn cho mọi site. - Đừng echo mù quáng header
Origintrở lại vàoAllow-Originmà không kiểm tra nó với allow-list — làm vậy chẳng khác gì cho phép tất cả. - Giới hạn method và header đúng những gì API thực sự dùng.
- Đặt
Access-Control-Max-Ageđể cache preflight và giảm overhead.
# Flask-CORS — allow-list rõ ràng, không phải "*"
CORS(app,
origins=["https://app.example.com"],
methods=["GET", "POST", "PUT", "DELETE"],
allow_headers=["Content-Type", "Authorization"],
supports_credentials=True)
Content-Security-Policy (CSP)
CSP là một HTTP response header báo cho browser biết những nguồn content nào (script, style, image, frame, connection) là đáng tin cậy. Lợi ích nổi bật của nó: là một tuyến phòng thủ thứ hai mạnh mẽ chống XSS — ngay cả khi kẻ tấn công inject được một <script>, một CSP tốt sẽ ngăn nó thực thi (không cho inline script, chỉ cho origin trong allow-list). Nó cũng giảm thiểu data exfiltration và clickjacking.
Một policy khởi đầu hợp lý:
Content-Security-Policy:
default-src 'self';
script-src 'self';
style-src 'self';
img-src 'self' data:;
connect-src 'self' https://api.example.com;
frame-ancestors 'none';
base-uri 'self';
form-action 'self';
object-src 'none';
upgrade-insecure-requests;
Lưu ý:
- Tránh
'unsafe-inline'và'unsafe-eval'cho script — chúng phá vỡ mục đích chính của CSP. Nếu cần inline script, dùng nonce theo mỗi request (script-src 'nonce-<random>') hoặc hash. frame-ancestors 'none'là biện pháp chống clickjacking hiện đại (thay thếX-Frame-Options).- Triển khai với
Content-Security-Policy-Report-Onlytrước, thu thập báo cáo vi phạm, rồi mới enforce — một CSP nghiêm ngặt thường làm hỏng nhiều thứ trong lần thử đầu tiên.
Transport security: HTTPS / TLS và HSTS
Mọi lưu lượng nên được mã hóa bằng TLS (HTTPS), luôn luôn, ở mọi nơi — không chỉ trên trang login. Không có nó, bất kỳ ai trên đường đi của mạng cũng có thể đọc hoặc sửa đổi lưu lượng, đánh cắp session cookie, và inject content. Xem Internet & Web Fundamentals để hiểu cơ chế handshake.
- HTTPS mọi nơi. Redirect HTTP → HTTPS. Dùng TLS 1.2+ (ưu tiên 1.3), cipher suite mạnh, và certificate hợp lệ, tự động gia hạn (Let’s Encrypt / ACME).
- HSTS (HTTP Strict Transport Security). Một response header báo cho browser chỉ kết nối qua HTTPS đến domain của bạn trong một khoảng thời gian nhất định, đánh bại các tấn công downgrade SSL-stripping.
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
Triển khai cẩn thận: một khi browser thấy HSTS, nó sẽ từ chối HTTP thuần trong max-age giây; preload (gửi vào danh sách preload của browser) thực chất là vĩnh viễn, nên chỉ bật khi bạn chắc chắn mọi subdomain đều hỗ trợ HTTPS.
Security response headers
Một tập nhỏ các header giúp hardening mọi response. Gửi chúng toàn cục (ở reverse proxy hoặc app middleware).
| Header | Mục đích |
|---|---|
Strict-Transport-Security | Buộc HTTPS cho các lần truy cập sau (HSTS) |
Content-Security-Policy | Giới hạn nguồn content; giảm thiểu XSS/clickjacking |
X-Content-Type-Options: nosniff | Ngăn browser MIME-sniff response thành kiểu ngoài ý muốn |
X-Frame-Options: DENY | Ngăn framing (clickjacking); frame-ancestors là kế thừa của CSP |
Referrer-Policy: no-referrer (hoặc strict-origin-when-cross-origin) | Giới hạn lượng thông tin URL rò rỉ trong header Referer |
Permissions-Policy | Tắt các tính năng browser mạnh (camera, geolocation) bạn không dùng |
Cache-Control: no-store (trên response nhạy cảm) | Ngăn cache dữ liệu riêng tư |
Set-Cookie: ... HttpOnly; Secure; SameSite | Bảo vệ session cookie (xem CSRF/XSS) |
Đừng để lộ banner phiên bản Server/X-Powered-By — chúng giúp kẻ tấn công fingerprint các phiên bản có lỗ hổng đã biết.
Application & server hardening
- Quản lý secrets. Đừng bao giờ commit secrets (API key, DB password, private key) vào source control. Nạp chúng từ environment variable được inject lúc runtime, hoặc từ một secrets manager (HashiCorp Vault, AWS Secrets Manager, GCP Secret Manager, cloud KMS). Xoay vòng định kỳ, và scan repo/CI để tìm secrets vô tình bị commit (git-secrets, trufflehog, gitleaks). Nếu một secret bị lộ, hãy xoay vòng nó — đừng chỉ xóa commit.
- Dependency & supply-chain security. Phần lớn code của bạn là code của người khác. Duy trì kiểm kê dependency (SBOM), pin/lock version, và chạy Software Composition Analysis (Dependabot, Snyk,
npm audit,pip-audit, OWASP Dependency-Check) trong CI để bắt các thư viện có lỗ hổng đã biết. Xác minh integrity của những gì bạn kéo về (hash trong lockfile, artifact có chữ ký), và cảnh giác với typosquatting cùng các package bị xâm phạm. Hardening chính pipeline CI/CD — nó có quyền truy cập rất rộng. - Rate limiting & chống lạm dụng. Giới hạn số request theo IP / theo account để làm nhụt các nỗ lực brute-force login, credential stuffing, scraping, và DoS ở tầng ứng dụng. Áp giới hạn chặt hơn cho auth và các endpoint tốn kém, thêm exponential backoff / lockout khi thất bại lặp lại, và đặt WAF / CDN phía trước để chống tấn công volumetric. Rate limiting bảo vệ Availability và Authentication.
- Least-privilege ở mọi nơi. Chạy service dưới quyền non-root; cấp cho DB account chỉ những quyền chúng cần; giới hạn phạm vi API token; dùng credential riêng cho mỗi môi trường.
- Logging & monitoring. Log các sự kiện liên quan bảo mật (đăng nhập, authz thất bại, hành động admin) với đủ ngữ cảnh để điều tra — nhưng đừng bao giờ log secrets, password, token, hay full PII. Tập trung hóa log, alert khi có bất thường, và lưu giữ đủ lâu để điều tra sự cố (A09).
Hashing: integrity vs. password
Cả hai đều dùng hàm hash, nhưng yêu cầu lại ngược nhau, và nhầm lẫn chúng là một lỗ hổng thực sự.
| Trường hợp dùng | Mục tiêu | Nên dùng | KHÔNG dùng |
|---|---|---|---|
| Integrity / checksum (fingerprint file, cache key, khử trùng lặp, ETag) | Nhanh, tất định | SHA-256 (nhanh là tốt và mong muốn) | — |
| Message authentication (xác minh payload không bị sửa, dùng shared key) | Nhanh + có key | HMAC-SHA256 | SHA thuần (không key) |
| Lưu trữ password | Chậm, có salt, memory-hard | argon2id, bcrypt, scrypt (hoặc PBKDF2) | MD5, SHA-1, SHA-256 (quá nhanh → dễ brute-force) |
Điểm mấu chốt: với integrity bạn muốn một hash nhanh. Với password bạn muốn một hàm cố tình chậm, có salt, memory-hard để việc brute-force offline là bất khả thi — dùng một hash nhanh đa dụng (kể cả SHA-256) cho password là một sai lầm nghiêm trọng. MD5 và SHA-1 cũng đã bị phá cho cả integrity nhạy cảm về bảo mật (collision) và nên tránh cho chữ ký/certificate. Hướng dẫn đầy đủ về password hashing cùng tham số nằm trong Authentication & Authorization.
Best Practices
- Không bao giờ tin input. Validate ở server với allow-list; coi mọi dữ liệu ngoài là thù địch.
- Parameterize mọi query. Không nối chuỗi vào SQL/NoSQL/command, bao giờ cả — kể cả bên trong ORM.
- Encode output theo ngữ cảnh và để framework autoescape làm việc; sanitize bất kỳ HTML nào bạn buộc phải render.
- Enforce authorization ở server trên mọi request, giới hạn theo user đã xác thực; deny by default; kiểm tra quyền sở hữu.
- Bảo vệ các request thay đổi trạng thái bằng
SameSitecookie và anti-CSRF token. - TLS mọi nơi, kèm HSTS; redirect HTTP sang HTTPS; tự động gia hạn cert.
- Gửi bộ security header (CSP, HSTS,
nosniff, chống framing,Referrer-Policy) toàn cục. - Cấu hình CORS với allow-list origin rõ ràng; không bao giờ
*kèm credential. - Quản lý secrets ngoài source control; xoay vòng; scan tìm rò rỉ.
- Vá dependency và scan chúng (SCA) trong CI; bảo vệ build pipeline.
- Rate-limit các endpoint auth và tốn kém; thêm lockout/backoff.
- Hash password bằng argon2/bcrypt (chậm); chỉ dùng hash nhanh cho integrity.
- Fail securely, log security event (không bao giờ log secrets), và monitor/alert.
- Threat-model các tính năng mới và giữ attack surface tối thiểu.
Security checklist
| Lĩnh vực | Kiểm tra |
|---|---|
| Input | Mọi input ngoài được validate ở server (allow-list) |
| Injection | Parameterized query / prepared statement ở mọi nơi |
| XSS | Output được encode; autoescape bật; HTML người dùng được sanitize; CSP enforce |
| CSRF | SameSite cookie + anti-CSRF token trên request thay đổi trạng thái |
| Auth | Library đã kiểm chứng, MFA, session an toàn (xem ./07) |
| Access control | Authz ở server trên mọi request; deny by default; kiểm tra sở hữu |
| SSRF | URL đi ra được allow-list; chặn dải nội bộ & metadata |
| Transport | TLS 1.2+ mọi nơi; redirect HTTP→HTTPS; HSTS |
| Headers | CSP, HSTS, nosniff, X-Frame-Options/frame-ancestors, Referrer-Policy |
| CORS | Allow-list origin rõ ràng; không * + credential |
| Secrets | Ngoài source control; trong secrets manager; xoay vòng; scan rò rỉ |
| Dependencies | Được kiểm kê, pin, SCA-scan, vá kịp thời |
| Cookies | Đặt HttpOnly, Secure, SameSite |
| Passwords | argon2id/bcrypt/scrypt có salt (không bao giờ MD5/SHA) |
| Rate limiting | Áp cho các endpoint auth và tốn kém |
| Config | Baseline hardened; tắt defaults; error production chung chung |
| Logging | Log security event; không secrets trong log; monitor/alert |
Tài liệu tham khảo
Part of the Backend Roadmap knowledge base.
Overview
Web security is not a feature you bolt on at the end; it is a property of every decision you make while building a backend — how you query the database, how you render output, how you store secrets, how you configure headers, and how you trust (or refuse to trust) the data crossing your boundary. A single unparameterized query or one missing header can turn an otherwise solid service into a data breach.
This note is defensive. The goal is to recognize the shape of each class of vulnerability so you can prevent and mitigate it — not to exploit it. For every risk we describe the failure mode and then the concrete control that neutralizes it.
We build up the mental model in layers:
- The security mindset — a small set of principles (defense in depth, least privilege, secure defaults, never trust input) that generate most of the specific rules.
- The OWASP Top 10 — the industry-standard shortlist of the risks that actually cause breaches, each with a one-line defense.
- The common vulnerability classes — injection, XSS, CSRF, broken access control, SSRF, misconfiguration, sensitive data exposure — with safe-vs-unsafe examples.
- The browser and transport controls — CORS, CSP, HTTPS/TLS, HSTS, and the security-header suite.
- Hardening — secrets management, dependency/supply-chain security, rate limiting, and hashing for the right purpose.
Authentication, session management, and password hashing are covered in depth in Authentication & Authorization; API-specific concerns in APIs; and the underlying HTTP/TLS mechanics in Internet & Web Fundamentals. This note cross-links to all three rather than repeating them.
Fundamentals
The security mindset
Most concrete security rules fall out of four principles. Internalize these and you can reason about vulnerabilities you have never seen a checklist item for.
- Defense in depth. Never rely on a single control. Assume any one layer will fail and make sure the next one still protects you. Validate input and use parameterized queries and run the DB user with minimal privileges and encrypt at rest. If the WAF is bypassed, the query is still parameterized; if the query is malformed, the DB user still can’t drop tables.
- Least privilege. Every component — a DB account, an API token, a service, a container, a human — gets the minimum access needed to do its job, and nothing more. A reporting service that only reads should have a read-only DB role. A CI token that deploys one app should not be an org admin. This shrinks the blast radius of any single compromise.
- Secure defaults. The safe configuration should be the one you get by doing nothing. New endpoints deny by default until explicitly allowed. Cookies are
Secure/HttpOnlyunless there’s a reason otherwise. TLS is on everywhere. Debug modes, verbose errors, and default admin credentials are off in production. If security requires remembering to flip a switch, someone will forget. - Never trust input. Treat every byte from outside your trust boundary as hostile until proven otherwise — request bodies, query strings, headers, cookies, file uploads, and even data from other internal services. Validate on the server (client-side checks are UX, not security). Prefer allow-lists (“only these values are permitted”) over deny-lists (“everything except these”), because you cannot enumerate every bad input.
Two supporting ideas complete the picture:
- Fail securely. When something goes wrong, fail closed, not open. An authorization check that throws an exception must result in denied, not allowed. Don’t leak stack traces, internal paths, or SQL to the user.
- Minimize attack surface. Every open port, enabled feature, dependency, and endpoint is something you must defend. Turn off what you don’t use.
The CIA triad and trust boundaries
Security controls exist to preserve three properties: Confidentiality (only authorized parties can read the data), Integrity (data cannot be tampered with undetectably), and Availability (the system stays usable). Every vulnerability breaks one or more of these — an SQL injection that dumps the users table breaks confidentiality; a CSRF that changes an email breaks integrity; an unthrottled endpoint that gets hammered breaks availability.
A trust boundary is any line where data or control passes between zones of different trust — browser → server, server → database, your service → a third-party API. Vulnerabilities cluster at these boundaries because that’s where assumptions (“the client already validated this”) get violated. Draw the boundaries explicitly and put a control on every crossing.
Key Concepts
OWASP Top 10 (2021)
The OWASP Top 10 is the most widely referenced awareness document for web application risk. Learn it as a checklist of “have I thought about this?” Each row below pairs the risk with its primary defense.
| # | Risk | What it is | One-line defense |
|---|---|---|---|
| A01 | Broken Access Control | Users act outside their intended permissions (IDOR, missing checks, privilege escalation) | Deny by default; enforce authorization server-side on every request against the authenticated user |
| A02 | Cryptographic Failures | Sensitive data exposed via weak/absent crypto (plaintext, weak ciphers, no TLS) | Encrypt in transit (TLS) and at rest; use strong, standard algorithms; don’t roll your own crypto |
| A03 | Injection | Untrusted input interpreted as code/commands (SQL, NoSQL, OS, LDAP) | Parameterized queries / prepared statements; validate and encode input |
| A04 | Insecure Design | Missing or flawed security controls at the design level | Threat-model early; use secure design patterns and reference architectures |
| A05 | Security Misconfiguration | Default creds, verbose errors, open cloud buckets, unnecessary features | Hardened, repeatable config; least functionality; disable defaults; automate |
| A06 | Vulnerable & Outdated Components | Known-vulnerable libraries/frameworks/OS packages | Inventory dependencies; patch promptly; scan (SCA) in CI |
| A07 | Identification & Authentication Failures | Weak login, session, or credential handling | Strong auth, MFA, secure session management — see ./07 |
| A08 | Software & Data Integrity Failures | Unverified updates, insecure deserialization, compromised CI/CD | Verify signatures/integrity; secure the pipeline; avoid unsafe deserialization |
| A09 | Security Logging & Monitoring Failures | Attacks go undetected due to missing logs/alerts | Log security events; centralize, alert, and retain; don’t log secrets |
| A10 | Server-Side Request Forgery (SSRF) | Server is tricked into making requests to unintended destinations | Validate/allow-list outbound URLs; block internal ranges and metadata endpoints |
Injection (SQL / NoSQL / command)
Injection happens whenever untrusted input is concatenated into an interpreter’s instruction — a SQL query, a shell command, a NoSQL filter — so the input can change the structure of the command instead of being treated as pure data. The fix is always the same idea: keep code and data separate.
SQL injection — unsafe (never do this):
# String concatenation lets input become SQL. Input like ' OR '1'='1
# turns the WHERE clause into something that always matches.
username = request.args["username"]
query = "SELECT * FROM users WHERE username = '" + username + "'"
cursor.execute(query) # VULNERABLE
SQL injection — safe (parameterized query / prepared statement):
# The driver sends the query structure and the value separately.
# The value can never be parsed as SQL — it is always just data.
username = request.args["username"]
cursor.execute(
"SELECT * FROM users WHERE username = %s", # placeholder
(username,), # bound parameter
)
The same in other ecosystems:
// Node.js (pg) — parameterized
await client.query("SELECT * FROM users WHERE username = $1", [username]);
// Java (JDBC) — PreparedStatement
PreparedStatement ps = conn.prepareStatement(
"SELECT * FROM users WHERE username = ?");
ps.setString(1, username);
Rules for injection defense:
- Always parameterize. Use prepared statements / bound parameters for every query with variable input. This alone stops the overwhelming majority of SQLi.
- Use an ORM/query builder correctly. ORMs (SQLAlchemy, Prisma, Hibernate, ActiveRecord) parameterize by default — but raw-query escape hatches (
.raw(), string interpolation intoWHERE) reopen the hole. Never build SQL strings from user input, even inside an ORM. - Validate and allow-list. Where the input is a known set (a sort column, an order direction), map it through an allow-list rather than passing it through — you cannot parameterize an identifier like a table or column name.
- NoSQL injection. MongoDB and friends are also injectable when a query object is built from raw input (e.g. an attacker sending
{"$gt": ""}where you expected a string). Cast/validate types, reject operator objects where you expect scalars, and use the driver’s typed query APIs. - OS command injection. Avoid shelling out with user input. If you must, pass arguments as an array (never a shell string), use APIs that don’t invoke a shell (
subprocess.run([...], shell=False)), and allow-list the command and its arguments.
Cross-Site Scripting (XSS)
XSS is injection into the browser: attacker-controlled data ends up in a page as executable script, running in the victim’s session with their cookies and permissions. Three variants:
| Type | Where the payload lives | Example vector |
|---|---|---|
| Stored | Persisted server-side, served to many users | Malicious comment saved to DB, shown to every viewer |
| Reflected | Bounced off the server from the request | Payload in a URL/search param echoed into the page |
| DOM-based | Never reaches the server; client JS writes it | innerHTML = location.hash in front-end code |
Defenses, layered:
- Contextual output encoding is the primary defense. Encode data for the context it lands in — HTML body, HTML attribute, JavaScript, URL, CSS each have different escaping rules. Modern template engines (React JSX, Vue, Jinja2 with autoescape, Thymeleaf, Razor) escape HTML by default. Let them.
- Do not disable escaping.
dangerouslySetInnerHTML(React),v-html(Vue),|safe(Jinja),innerHTML— every one of these is an XSS invitation if fed untrusted data. If you must render user-supplied HTML (a rich-text field), sanitize it with a vetted library (DOMPurify) using an allow-list of tags/attributes. - Content-Security-Policy (CSP) is your safety net when encoding is missed — see below.
- Set cookies
HttpOnlyso that even a successful XSS cannot read the session cookie viadocument.cookie. - Validate input as a secondary control (e.g. a URL field must actually be an
http(s)URL, blockingjavascript:payloads).
{# Jinja2 autoescape ON (the default): safe by construction #}
<p>{{ user_comment }}</p> {# < > & are escaped #}
{# DANGEROUS — only after sanitizing user_comment server-side #}
<div>{{ user_comment | safe }}</div>
Cross-Site Request Forgery (CSRF)
CSRF abuses the browser’s habit of automatically attaching cookies to any request to your domain. If a user is logged in to your site and visits an attacker’s page, that page can silently submit a form or fire a request to your app, and the browser sends the session cookie along — so the action runs as the victim. CSRF targets state-changing requests (POST/PUT/DELETE), not reads.
Defenses:
- SameSite cookies. Set
SameSite=Lax(a sensible default that still allows top-level navigation) orSameSite=Stricton session cookies. This tells the browser not to send the cookie on cross-site requests, neutralizing most CSRF. Combine withSecureandHttpOnly. - Anti-CSRF tokens (synchronizer token pattern). The server issues a random, per-session (or per-request) token; forms include it as a hidden field, and the server rejects any state-changing request whose token is missing or wrong. An attacker’s page cannot read the token (same-origin policy), so it cannot forge a valid request. Most frameworks have this built in (Django
{% csrf_token %}, Railsprotect_from_forgery, Spring Security CSRF). - Double-submit cookie is a stateless alternative: the token is sent both as a cookie and as a header/field; the server checks they match.
- For APIs using bearer tokens (a token in the
Authorizationheader rather than a cookie), CSRF largely does not apply, because the browser does not attach that header automatically. Cookie-based sessions are the ones that need CSRF protection. - Check
Origin/Refererheaders on sensitive requests as a defense-in-depth signal.
Set-Cookie: session=...; HttpOnly; Secure; SameSite=Lax; Path=/
Broken authentication & session management
Weak login flows, guessable session IDs, tokens that never expire, and credentials in URLs let attackers become other users. Because this is a large topic, the full treatment — password hashing (bcrypt/argon2), MFA, session lifecycle, JWT pitfalls, OAuth/OIDC — lives in Authentication & Authorization. The one-line rules: use a vetted auth library, hash passwords with a slow adaptive function, rotate and expire sessions, and enforce MFA on sensitive accounts.
Broken access control & IDOR
Access control decides what an authenticated user is allowed to do. It breaks when the server trusts the client to enforce limits. The classic form is IDOR (Insecure Direct Object Reference): an endpoint like GET /api/invoices/1043 returns the invoice with that ID without checking it belongs to the caller, so changing the number to 1044 reveals someone else’s data.
Defenses:
- Enforce authorization on the server, on every request, against the authenticated identity — never a client-supplied user ID or role.
- Deny by default. New routes are inaccessible until a rule explicitly permits them.
- Check ownership, not just authentication. “Is this user logged in?” is not “does this invoice belong to this user?” Scope queries to the current user:
WHERE invoice.id = ? AND invoice.owner_id = ?. - Don’t rely on obscurity. Unguessable IDs (UUIDs) help but are not access control. Enforce the check regardless.
- Centralize the policy. Middleware/decorators/policy objects that run on every request are harder to forget than ad-hoc
ifchecks scattered per handler.
Server-Side Request Forgery (SSRF)
SSRF happens when your server fetches a URL supplied (directly or indirectly) by the user — image imports, webhooks, PDF renderers, link previews — and the attacker points it inward: at http://169.254.169.254/ (cloud metadata, often holding credentials), at internal admin panels, or at localhost services that trust local traffic.
Defenses:
- Allow-list destinations. Only permit requests to known, expected hosts/domains. This is the strongest control.
- Block internal ranges. Reject private IPs (
10/8,172.16/12,192.168/16,127/8,169.254/16, IPv6 equivalents) after resolving DNS — validate the resolved IP, not just the hostname (to defeat DNS rebinding and redirects). - Disable unneeded URL schemes (
file://,gopher://) and follow redirects cautiously (re-validate each hop). - Lock down cloud metadata. Require IMDSv2 on AWS, or block the metadata IP at the network level.
- Give the fetching service its own restricted network egress.
Security misconfiguration
The most common category in practice: default admin passwords, directory listing enabled, verbose stack traces in production, unnecessary services running, overly permissive cloud storage (public S3 buckets), missing security headers, and dev tooling exposed. Defenses: a hardened, version-controlled baseline configuration; the same config across environments (minus secrets); turning off everything you don’t need; generic error pages in production; and automated scanning for drift.
Sensitive data exposure / cryptographic failures
Data leaks through plaintext storage, weak or absent encryption, secrets in logs or source control, and over-broad API responses. Defenses:
- Classify data so you know what needs protection (PII, credentials, payment data, health data).
- Encrypt in transit with TLS everywhere (see Transport security) and at rest for sensitive stores.
- Minimize what you collect and return. Don’t send fields the client doesn’t need; don’t store data you don’t need.
- Keep secrets out of code and logs (see Secrets management).
- Use standard, current algorithms and libraries — never invent your own cryptography.
CORS (Cross-Origin Resource Sharing)
The browser’s Same-Origin Policy (SOP) blocks JavaScript on https://a.com from reading responses from https://b.com. CORS is the controlled, server-driven mechanism to relax that — it lets your API tell the browser which other origins are allowed to read its responses. CORS is a browser enforcement layer; it does not protect your server from non-browser clients (curl, servers), and it is not a substitute for authentication or authorization.
How it works. For “simple” requests the browser sends the request with an Origin header and checks the response’s Access-Control-Allow-Origin before revealing it to JS. For requests that can change state or use custom headers, the browser first sends a preflight OPTIONS request asking permission:
# Preflight request (browser -> your API)
OPTIONS /api/orders HTTP/1.1
Origin: https://app.example.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Content-Type, Authorization
# Preflight response (your API -> browser)
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Allow-Credentials: true
Access-Control-Max-Age: 600
Configure it safely:
- Allow-list specific origins. Reflect only known-good origins. Never combine
Access-Control-Allow-Origin: *withAccess-Control-Allow-Credentials: true— the browser forbids it, and reflecting arbitrary origins with credentials is effectively opening your authenticated API to any site. - Don’t blindly echo the
Originheader back intoAllow-Originwithout checking it against your allow-list — that is the same as allowing everyone. - Restrict methods and headers to what the API actually uses.
- Set
Access-Control-Max-Ageto cache preflights and reduce overhead.
# Flask-CORS — explicit allow-list, not "*"
CORS(app,
origins=["https://app.example.com"],
methods=["GET", "POST", "PUT", "DELETE"],
allow_headers=["Content-Type", "Authorization"],
supports_credentials=True)
Content-Security-Policy (CSP)
CSP is an HTTP response header that tells the browser which sources of content (scripts, styles, images, frames, connections) are trustworthy. Its headline benefit: it is a strong second line of defense against XSS — even if an attacker injects a <script>, a good CSP prevents it from executing (no inline scripts, only allow-listed origins). It also mitigates data exfiltration and clickjacking.
A reasonable starting policy:
Content-Security-Policy:
default-src 'self';
script-src 'self';
style-src 'self';
img-src 'self' data:;
connect-src 'self' https://api.example.com;
frame-ancestors 'none';
base-uri 'self';
form-action 'self';
object-src 'none';
upgrade-insecure-requests;
Notes:
- Avoid
'unsafe-inline'and'unsafe-eval'for scripts — they defeat CSP’s main purpose. If you need inline scripts, use per-request nonces (script-src 'nonce-<random>') or hashes. frame-ancestors 'none'is the modern anti-clickjacking control (supersedesX-Frame-Options).- Roll out with
Content-Security-Policy-Report-Onlyfirst, collect violation reports, then enforce — a strict CSP often breaks things on the first try.
Transport security: HTTPS / TLS and HSTS
All traffic should be encrypted with TLS (HTTPS), always, everywhere — not just on the login page. Without it, anyone on the network path can read or modify traffic, steal session cookies, and inject content. See Internet & Web Fundamentals for the handshake mechanics.
- HTTPS everywhere. Redirect HTTP → HTTPS. Use TLS 1.2+ (prefer 1.3), strong cipher suites, and valid, automatically-renewed certificates (Let’s Encrypt / ACME).
- HSTS (HTTP Strict Transport Security). A response header that tells browsers to only ever connect over HTTPS to your domain for a given duration, defeating SSL-stripping downgrade attacks.
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
Roll out carefully: once a browser sees HSTS it will refuse plain HTTP for max-age seconds; preload (submitting to the browser preload list) is effectively permanent, so only enable it when you are certain every subdomain supports HTTPS.
Security response headers
A small set of headers hardens every response. Send them globally (at the reverse proxy or app middleware).
| Header | Purpose |
|---|---|
Strict-Transport-Security | Force HTTPS for future visits (HSTS) |
Content-Security-Policy | Restrict content sources; mitigate XSS/clickjacking |
X-Content-Type-Options: nosniff | Stop the browser from MIME-sniffing responses into an unintended type |
X-Frame-Options: DENY | Prevent framing (clickjacking); frame-ancestors is the CSP successor |
Referrer-Policy: no-referrer (or strict-origin-when-cross-origin) | Limit how much URL info leaks in the Referer header |
Permissions-Policy | Disable powerful browser features (camera, geolocation) you don’t use |
Cache-Control: no-store (on sensitive responses) | Prevent caching of private data |
Set-Cookie: ... HttpOnly; Secure; SameSite | Protect session cookies (see CSRF/XSS) |
Do not expose Server/X-Powered-By version banners — they help attackers fingerprint known-vulnerable versions.
Application & server hardening
- Secrets management. Never commit secrets (API keys, DB passwords, private keys) to source control. Load them from environment variables injected at runtime, or from a secrets manager (HashiCorp Vault, AWS Secrets Manager, GCP Secret Manager, cloud KMS). Rotate regularly, and scan repos/CI for accidentally committed secrets (git-secrets, trufflehog, gitleaks). If a secret leaks, rotate it — don’t just delete the commit.
- Dependency & supply-chain security. Most of your code is other people’s code. Maintain a dependency inventory (SBOM), pin/lock versions, and run Software Composition Analysis (Dependabot, Snyk,
npm audit,pip-audit, OWASP Dependency-Check) in CI to catch known-vulnerable libraries. Verify integrity of what you pull (lockfile hashes, signed artifacts), and be wary of typosquatting and compromised packages. Harden the CI/CD pipeline itself — it has broad access. - Rate limiting & abuse protection. Throttle requests per IP / per account to blunt brute-force login attempts, credential stuffing, scraping, and application-layer DoS. Apply stricter limits to auth and other expensive endpoints, add exponential backoff / lockouts on repeated failures, and put a WAF / CDN in front for volumetric protection. Rate limiting protects availability and authentication.
- Least-privilege everywhere. Run services as non-root; give DB accounts only the rights they need; scope API tokens narrowly; use separate credentials per environment.
- Logging & monitoring. Log security-relevant events (logins, authz failures, admin actions) with enough context to investigate — but never log secrets, passwords, tokens, or full PII. Centralize logs, alert on anomalies, and retain them long enough to investigate incidents (A09).
Hashing: integrity vs. passwords
Both use hash functions, but the requirements are opposite, and mixing them up is a real vulnerability.
| Use case | Goal | Use | Do NOT use |
|---|---|---|---|
| Integrity / checksums (file fingerprints, cache keys, deduplication, ETags) | Fast, deterministic | SHA-256 (fast is fine and desirable) | — |
| Message authentication (verify a payload wasn’t tampered with, using a shared key) | Fast + keyed | HMAC-SHA256 | plain SHA (no key) |
| Password storage | Slow, salted, memory-hard | argon2id, bcrypt, scrypt (or PBKDF2) | MD5, SHA-1, SHA-256 (far too fast → brute-forceable) |
The key insight: for integrity you want a fast hash. For passwords you want a deliberately slow, salted, memory-hard function so that offline brute-forcing is infeasible — using a fast general-purpose hash (even SHA-256) for passwords is a serious mistake. MD5 and SHA-1 are broken for security-sensitive integrity too (collisions) and should be avoided for signatures/certificates. Full password-hashing guidance and parameters are in Authentication & Authorization.
Best Practices
- Never trust input. Validate on the server with allow-lists; treat all external data as hostile.
- Parameterize every query. No string concatenation into SQL/NoSQL/commands, ever — even inside an ORM.
- Encode output for its context and let framework autoescaping do its job; sanitize any HTML you must render.
- Enforce authorization server-side on every request, scoped to the authenticated user; deny by default; check ownership.
- Protect state-changing requests with
SameSitecookies and anti-CSRF tokens. - TLS everywhere, plus HSTS; redirect HTTP to HTTPS; auto-renew certs.
- Ship the security-header suite (CSP, HSTS,
nosniff, frame protection,Referrer-Policy) globally. - Configure CORS with an explicit origin allow-list; never
*with credentials. - Manage secrets outside source control; rotate them; scan for leaks.
- Patch dependencies and scan them (SCA) in CI; secure the build pipeline.
- Rate-limit auth and expensive endpoints; add lockouts/backoff.
- Hash passwords with argon2/bcrypt (slow); use fast hashes only for integrity.
- Fail securely, log security events (never secrets), and monitor/alert.
- Threat-model new features and keep attack surface minimal.
Security checklist
| Area | Check |
|---|---|
| Input | All external input validated server-side (allow-list) |
| Injection | Parameterized queries / prepared statements everywhere |
| XSS | Output encoded; autoescape on; user HTML sanitized; CSP enforced |
| CSRF | SameSite cookies + anti-CSRF tokens on state-changing requests |
| Auth | Vetted library, MFA, secure sessions (see ./07) |
| Access control | Server-side authz on every request; deny by default; ownership checked |
| SSRF | Outbound URLs allow-listed; internal ranges & metadata blocked |
| Transport | TLS 1.2+ everywhere; HTTP→HTTPS redirect; HSTS |
| Headers | CSP, HSTS, nosniff, X-Frame-Options/frame-ancestors, Referrer-Policy |
| CORS | Explicit origin allow-list; no * + credentials |
| Secrets | Out of source control; in a secrets manager; rotated; leak-scanned |
| Dependencies | Inventoried, pinned, SCA-scanned, patched promptly |
| Cookies | HttpOnly, Secure, SameSite set |
| Passwords | argon2id/bcrypt/scrypt with salt (never MD5/SHA) |
| Rate limiting | Applied to auth and expensive endpoints |
| Config | Hardened baseline; defaults off; generic prod errors |
| Logging | Security events logged; no secrets in logs; monitored/alerted |