Chuyển tới nội dung chính

Đổi id trên URL ra dữ liệu người khác? Chặn IDOR ở một tầng duy nhất

· 16 phút để đọc
Nguyễn Huỳnh Minh Tiến
Fullstack Developer @ Utop.io
Tóm tắt

IDOR xảy ra khi endpoint nhận id từ request rồi nạp thẳng bản ghi, chỉ kiểm tra người gọi đã đăng nhập chứ không kiểm tra người gọi có quyền trên đúng bản ghi đó. Đổi id sang GUID hay mã hoá id không sửa được lỗi, vì id vẫn rò ra ở chỗ khác. Cách sửa là bắt buộc mọi truy vấn phải đi qua một tầng ràng buộc quyền sở hữu ở server, thay vì rải lệnh kiểm tra trong từng controller.

Bạn có một endpoint rất đỗi bình thường: GET /api/orders/1043 trả về chi tiết đơn hàng. Nó nằm sau [Authorize], token hợp lệ mới gọi được, và trên giao diện thì người dùng chỉ bấm được vào đơn của chính họ.

Rồi một người đăng nhập bằng tài khoản thật của họ, sửa 1043 thành 1044 và bấm gửi. Nếu server trả về đơn hàng của người khác thì đó là Insecure Direct Object Reference, gọi tắt là IDOR. Không có payload, không có kỹ thuật gì, chỉ là một con số bị đổi.

Singleton, Scoped hay Transient? Chọn sai là DbContext sống mãi

· 15 phút để đọc
Nguyễn Huỳnh Minh Tiến
Fullstack Developer @ Utop.io
Tóm tắt

Chọn lifetime là chọn thời điểm container tạo instance và thời điểm nó dispose. Transient tạo mới mỗi lần resolve, Scoped một instance cho mỗi HTTP request, Singleton một instance cho cả vòng đời process. Quy tắc quan trọng nhất: service sống lâu không được nhận trực tiếp service sống ngắn. Inject một repository Scoped vào một Singleton là bạn vừa giữ DbContext đó sống tới lúc app tắt. Cách sửa là inject IServiceScopeFactory rồi tự mở scope khi cần.

Dòng đăng ký gây ra lỗi này trông vô hại tới mức review không ai dừng lại:

builder.Services.AddDbContext<CrmDbContext>(o => o.UseSqlServer(conn)); // Scoped
builder.Services.AddScoped<ICustomerRepository, EfCustomerRepository>();
builder.Services.AddSingleton<CustomerCacheRefresher>(); // ← thuốc nổ

App qua được CI và lên staging. Lên môi trường có tải, log bắt đầu xuất hiện A second operation was started on this context instance before a previous operation completed, hoặc Cannot access a disposed context instance, hoặc tệ hơn là không có exception nào cả mà chỉ có dữ liệu cũ trả về mãi không đổi. Bài này giải thích cơ chế đằng sau, và code của cả bản hỏng lẫn bản sửa.

Dựng một nền tảng CRM multi-tenant từ con số không: chuyện nghề 20 tháng

· 36 phút để đọc
Nguyễn Huỳnh Minh Tiến
Fullstack Developer @ Utop.io
Tóm tắt

Tháng 01/2025 mình được giao init một sản phẩm, lần đầu tiên trong nghề, là một nền tảng CRM multi-tenant mà lúc nhận thì repo còn trống trơn. Gần một năm đầu nó sống ở chế độ đi demo cho nhiều khách thuộc nhiều ngành, rồi tới gần cuối 2025 thì chốt được khách ngành y tế và chuyển sang delivery thật. Hai mươi tháng sau nó là 18 microservice trên ABP Framework / .NET 9 và 13 Angular library, của một team đã lớn lên tới 25 người. Bài này không phải bài kỹ thuật mà là chuyện nghề kể theo thứ tự thời gian, trong đó có cái màn hình export bị quên phân quyền, có hai tuần làm Angular xong phải revert, có ba tháng giữa năm mình được chuyển sang dự án loyalty khác nên không commit dòng nào, và có một lần migration nhàm chán tới mức chẳng ai để ý là nó đã xảy ra. Trong bài cũng có đoạn về kiến trúc EAV cho phép khách tự cấu hình trường dữ liệu trên portal, kèm mấy tháng mình vật lộn với performance của nó và của bộ dynamic filter nằm trên nó, đoạn kể quãng mình lần đầu làm mentor cho bốn bạn intern, và đoạn kể cách mình viết code đổi dần từ gõ tay sang dùng ChatGPT, rồi Cursor, rồi Claude.

Ngày 14 tháng 1 năm 2025, mình mở một cái repo trống trơn mà trong đầu chẳng có gì ngoài mấy câu hỏi rất tầm thường, kiểu đặt tên solution là gì, thư mục nào để đâu, database thì chia bảng thế nào.

Trước đó mình toàn nhận những codebase đã chạy sẵn rồi, công việc chỉ là đọc, sửa và thêm màn hình, nên lần này thấy khác hẳn: không có gì để bắt chước, mà cái gì mình gõ ra thì sau này người khác lại cứ thế làm theo. Nghe kể thì oai, chứ lúc ngồi trước cái repo trống ấy mình hơi ngợp thật.

Commit hôm đó của mình chỉ có một dòng, Init - Databases, projects, và cũng chẳng có gì long trọng cả.

Hai mươi tháng sau, cái repo ấy có 18 microservice với 29.607 commit của 25 người. Mình vẫn còn ở đó, và thỉnh thoảng vẫn mở git log --reverse ra xem lại mấy dòng đầu.

git log --reverse --format="%h %ad %an %s" --date=short | head -5

Bài này mình viết để vài năm nữa đọc lại. Vì sản phẩm là của công ty nên tên riêng mình ẩn hết, từ tên sản phẩm, tên repo, tên khách hàng cho tới tên mấy library nội bộ, còn tech stack thì để nguyên vì đó là phần kể được.

Vì sao .NET ngốn 96% RAM container dù app đang rảnh? Server GC và cách giảm 65% bộ nhớ

· 13 phút để đọc
Nguyễn Huỳnh Minh Tiến
Fullstack Developer @ Utop.io
Tóm tắt

Một container ASP.NET Core 9 chiếm 987 MiB trên trần 1 GiB (96%) dù suốt 2 giờ không nhận request nào. Đây không phải rò rỉ bộ nhớ: mức RAM đứng yên tuyệt đối qua nhiều lần đo, dấu hiệu của heap bị giữ lại chứ không phải rò rỉ. Nguyên nhân là Server GC — mặc định của ASP.NET Core, tạo một heap cho mỗi CPU core và rất ít trả bộ nhớ về hệ điều hành. Đặt DOTNET_gcServer=0 và DOTNET_GCConserveMemory=5 giảm bộ nhớ anon từ 822 MB xuống 280 MB (−65%), đo ổn định sau 30 phút. Đánh đổi: Workstation GC cho throughput thấp hơn khi tải cao và nhiều core.

Bài này ghi lại một buổi chẩn đoán thật trên VPS 4 core / 8 GB RAM đang chạy song song 9 site WordPress và một API .NET. Mọi con số bên dưới là số đo thật, không phải ví dụ minh hoạ.

Scalar là gì? Tại sao nên dùng Scalar thay Swagger UI trong ASP.NET Core 10

· 9 phút để đọc
Nguyễn Huỳnh Minh Tiến
Fullstack Developer @ Utop.io
Tóm tắt

Scalar là open-source API Reference UI thay thế Swagger UI, được Microsoft khuyến nghị cho ASP.NET Core từ .NET 9 trở đi. Kể từ .NET 9, Microsoft loại bỏ Swashbuckle (Swagger UI) khỏi template mặc định của Web API và thay bằng Microsoft.AspNetCore.OpenApi kết hợp với Scalar. Bài viết này giải thích lý do thay đổi đó, so sánh tính năng Scalar vs Swagger UI, và hướng dẫn thiết lập Scalar trong ASP.NET Core 10 từng bước - từ project mới lẫn migration từ dự án cũ.

Nếu bạn tạo một project ASP.NET Core Web API mới với .NET 9 hoặc .NET 10 và ngạc nhiên khi không thấy Swagger UI nữa - bạn không phải người duy nhất. Microsoft đã chủ động thay thế Swashbuckle bằng một pipeline OpenAPI native, và Scalar là UI được khuyến nghị đi kèm.

Giao diện Scalar API Reference trên scalar.com