Mổ gói NuGet trước khi cài: tìm code chạy lúc build và lúc nạp
Một gói NuGet lạ có thể chạy code trên máy dev và CI của BHPay ngay lúc build, trước khi API gọi hàm nào của nó. SoiGoi.cs đọc tĩnh file .nupkg, chấm cả 4 gói mẫu có code tự chạy từ 4 điểm, gói sạch 0 điểm, và soi 4 gói mới của một PR trong 89 ms.
Mục lục
- 1. Vấn đề: một gói lạ sắp vào API ví
- 2. Mục đích: biết gói chạy gì mà không chạy gói
- 3. Cơ sở lý thuyết: code trong gói chạy ở đâu, lúc nào
- 4. Cách giải quyết: soi tĩnh, chấm điểm, chặn giữa restore và build
- 5. Cách cài đặt: một file C#, không gói ngoài
- 6. Chứng minh: file đánh dấu thật và điểm của công cụ
- 7. Kết luận
- Đọc tiếp
- Nguồn
Đọc nhanh
- Vấn đề: Một dev đề xuất thêm gói tiện ích lạ vào API
BHPay; đội cần biết gói chạy gì trên máy dev và CI lúc restore, build và lúc API nạp nó. - Cách giải: Mở
.nupkgnhư file zip, đọc.nuspec,.targets, analyzer, dùngSystem.Reflection.Metadataliệt kê lời gọi ra ngoài và module initializer, rồi chấm điểm mà không chạy gì. - Chứng minh: Trên 5 gói tự dựng, file đánh dấu cho thấy 2 gói chạy code lúc build, 2 gói lúc API chạy; công cụ chấm cả 4 gói từ 4 điểm, gói sạch 0 điểm.
- Trong .NET:
SoiGoi.cschạy bằngdotnet run, không gói ngoài, đặt giữadotnet restore --locked-modevàdotnet build --no-restoređể soi mọi gói mới trong lock file.
1. Vấn đề: một gói lạ sắp vào API ví
Sáng 2026-10-02, một PR vào API của BHPay thêm gói chuẩn hóa số điện thoại và số tiền, trong bài gọi là GoiLa.ChuanHoa 2.3.1 (tên hư cấu). Gói nằm trên nguồn công khai, có một tác giả, chưa ai trong đội đọc mã.
Người duyệt cần biết gói chạy gì, ở đâu, lúc nào. Ngay khi PR mở, agent CI chạy dotnet restore rồi dotnet build. Agent giữ khóa ký webhook thử bhpay_test_whsec_… (khóa giả), chuỗi kết nối tới database BHPay của môi trường thử, và khóa đẩy gói lên feed nội bộ; máy dev cũng vậy. Code của gói, nếu chạy lúc build, chạy với đúng những quyền đó.
Đọc repo của gói không đủ: thứ NuGet tải về là file .nupkg, có thể chứa file không có trong repo. Đội cần xem chính file đó trước khi máy nào build với nó, và làm lại được cho mọi gói mới, kể cả gói kéo theo bắc cầu.
2. Mục đích: biết gói chạy gì mà không chạy gói
- Không nạp, không build, không chạy gói, vẫn liệt kê được mọi chỗ code của gói tự chạy (
.targets, analyzer, module initializer) và các lời gọi cần xem (Process.Start, mạng, nạp code động, P/Invoke). - Trên 5 gói tự dựng: chấm từ 4 điểm đúng những gói có file đánh dấu xuất hiện thật khi build hoặc chạy, 0 điểm cho gói sạch.
- Trong CI: soi mọi gói có trong
packages.lock.jsoncủa PR mà nhánh chính chưa có, phần soi dưới 1 giây, chặn build bằng mã thoát khác 0. - Ngoài phạm vi: quan sát hành vi trong sandbox, đọc mã máy native, kết luận một gói là an toàn.
3. Cơ sở lý thuyết: code trong gói chạy ở đâu, lúc nào
Gói là file zip. Theo tài liệu NuGet, .nupkg là file ZIP đổi đuôi, kèm manifest .nuspec ở gốc. dotnet restore tải gói, giải nén vào thư mục global packages, rồi sinh obj/project.assets.json cùng hai file obj/<project>.nuget.g.props và .nuget.g.targets. Restore không nhập file build của gói: NuGet.targets trong SDK 10.0.401 đánh giá project cho restore với ExcludeRestorePackageImports=true, và hai file nuget.g.* chỉ nhập khi property đó khác true. Binlog của lần restore thứ hai, khi obj/ đã đủ file, không có lần nhập nào tới .targets của gói.
build/ và buildTransitive/. File <id gói>.props hoặc .targets ở đây được nhập vào project dùng gói, qua hai file nuget.g.*. File đó móc Target của mình vào bất kỳ bước nào bằng BeforeTargets hay AfterTargets, và MSBuild chạy nó trong tiến trình build, với quyền của người chạy build; task Exec là chạy lệnh shell. File trong buildTransitive/ (NuGet 5.0+) còn đi sang project tham chiếu project dùng gói. Gói mẫu có cả hai thư mục, và nuget.g.targets chỉ nhập bản buildTransitive. Phần "code" của gói đó là tám dòng:
<Project>
<!-- Mẫu vô hại: chỉ ghi một file đánh dấu vào thư mục tạm mỗi lần project dùng gói được build. -->
<Target Name="BHPayCauHinh_DanhDau" BeforeTargets="CoreCompile">
<WriteLinesToFile File="$([System.IO.Path]::GetTempPath())soi-goi-danh-dau\cauhinh-targets.txt"
Lines="targets chay luc build: $(MSBuildProjectName) $([System.DateTime]::Now.ToString('HH:mm:ss.fff'))"
Overwrite="false" />
</Target>
</Project>
analyzers/. DLL trong analyzers/dotnet/cs/ được trình biên dịch C# nạp làm analyzer hoặc source generator. Trên máy đo, file đánh dấu của gói mẫu ghi tên VBCSCompiler.dll, server biên dịch dùng chung của dotnet build. Tài liệu Roslyn viết analyzer chạy ngay khi dev đang gõ code, tức là cả trong IDE. Luật RS1035 trong Microsoft.CodeAnalysis.Analyzers 3.11.0 cấm analyzer dùng System.IO.File, System.IO.Directory, Path.GetTempPath, System.Environment, System.Diagnostics.Process và Assembly.Load, nên analyzer của gói lạ gọi chúng là tín hiệu mạnh.
Module initializer. Gắn [ModuleInitializer] lên một hàm static void thì trình biên dịch sinh <Module>..cctor, hàm khởi tạo của kiểu đặc biệt <Module>, gọi hàm đó. Phần bổ sung ECMA-335 của .NET hứa nó chạy "tại, hoặc trước, lần đầu truy cập field static hay lần đầu gọi method" của module. Đo trên .NET 10.0.12: Assembly.LoadFrom, GetTypes, GetMethod chưa kích hoạt nó, lần Invoke đầu tiên thì có. API chỉ cần gọi một hàm bất kỳ của gói. Khi đóng gói mẫu, SDK cảnh báo CA2255: thư viện không nên dùng thuộc tính này.
tools/install.ps1. NuGet chỉ chạy script này với packages.config. Với PackageReference, kiểu mặc định của project SDK, install.ps1 và uninstall.ps1 không chạy.
Lời gọi trong lib/. Code thư viện chỉ chạy khi app gọi, mà với API nhận 300 request mỗi giây lúc cao điểm thì đó là mỗi request. Đáng xem là Process.Start, HttpClient và socket, Assembly.Load(byte[]), P/Invoke.
Đọc tĩnh bằng metadata. Assembly .NET là file PE chứa các bảng metadata theo ECMA-335 phần II. PEReader và MetadataReader trong System.Reflection.Metadata đọc các bảng đó như byte, không nạp assembly, nên không module initializer nào chạy. Mỗi lời gọi sang method ở assembly khác, như Process.Start, là token trỏ vào một dòng MemberRef ghi kiểu cha và tên. Hàm P/Invoke là dòng MethodDef có cờ PinvokeImpl, tên DLL native nằm ở bảng ImplMap.
4. Cách giải quyết: soi tĩnh, chấm điểm, chặn giữa restore và build
| Cách | Ưu | Nhược | Khi nào dùng |
|---|---|---|---|
| Đọc repo của gói | Nhanh | .nupkg có thể khác repo |
Bước đầu |
| Mở gói bằng tay, đọc IL bằng ILSpy | Thấy hết | Chậm, không lặp lại được trong CI | Gói đã bị chặn |
| Chạy thử trong máy ảo | Thấy hành vi thật | Tốn hạ tầng; code có thể chỉ chạy khi gặp điều kiện riêng | Gói rủi ro cao |
Soi tĩnh tự động, SoiGoi.cs |
Không chạy gì; vài gói soi dưới 0,1 giây | Chỉ thấy tín hiệu trong metadata | Cổng CI cho mọi gói mới |
BHPay chọn soi tĩnh tự động làm cổng, rồi mở bằng tay gói bị chặn. Cổng đặt sau dotnet restore, trước dotnet build: lúc đó đã có đủ file mà chưa code nào của gói chạy. Mỗi gói qua năm bước:
- Mở zip, đọc
id,version,authorstrong.nuspec. - Với mỗi
.props,.targetstrongbuild*/: liệt kêTarget, chỗ nó móc vào, task nó gọi. - Với mỗi DLL: native thì ghi nhận; .NET thì đọc metadata tìm analyzer, module initializer, P/Invoke, và đối chiếu
MemberRefvới bảng luật. - Chấm điểm: mỗi nhóm lấy mức cao nhất, cộng các nhóm. Từ 4 điểm thì chặn, chờ người duyệt.
- Trong CI, chỉ soi gói có trong lock file của PR mà nhánh chính chưa có, sau khi kiểm
contentHashkhớp file.nupkg.
| Nhóm | Dấu hiệu trong gói | Chạy lúc | Điểm |
|---|---|---|---|
| build | Target trong build*/*.targets, DLL trong build*/ |
build | 4; 6 nếu có Exec, DownloadFile, UsingTask; 1 nếu chỉ in log hoặc không có Target |
| analyzer | DLL trong analyzers/ |
build, IDE | 4; thêm 2 nếu gọi API RS1035 cấm |
| module initializer | <Module>..cctor |
lời gọi đầu vào gói | 4 |
| tiến trình | Process.Start |
khi app gọi | 4 |
| nạp code động | Assembly.Load(byte[]), LoadFrom, LoadFromStream |
khi app gọi | 3 |
| mạng | HttpClient, WebClient, Socket, TcpClient |
khi app gọi | 2 |
| native | NativeLibrary.Load 2; P/Invoke 1; file native 1 |
khi app gọi | 1–2 |
| khác | .cs trong contentFiles/ 2; .ps1 trong tools/ 1 |
build; không chạy | 1–2 |
Mọi thứ chạy mà API không gọi đều tự đủ 4 điểm. Mạng, nạp động, P/Invoke đứng riêng thì dưới ngưỡng vì nhiều thư viện hợp lệ có chúng; hai nhóm cộng lại thì chạm. Điểm không kết luận gói xấu, nó chọn gói cần người đọc.
5. Cách cài đặt: một file C#, không gói ngoài
Môi trường: .NET SDK 10.0.401 (runtime 10.0.12), MSBuild 18.9.11, NuGet 7.9.0-rc.42413 đi kèm SDK, Windows 11. SoiGoi.cs là file-based app chỉ dùng thư viện có sẵn trong .NET 10: System.IO.Compression, System.Xml.Linq, System.Reflection.Metadata, System.Text.Json. File không có #:package, nên build công cụ không kéo thêm gói nào. Lõi là phần đọc một DLL .NET:
// Module initializer: kiểu <Module> (dòng 1 bảng TypeDef) có .cctor.
var module = md.GetTypeDefinition(MetadataTokens.TypeDefinitionHandle(1));
if (module.GetMethods().Any(m => md.GetString(md.GetMethodDefinition(m).Name) == ".cctor"))
{
var goi = md.MethodDefinitions.Select(md.GetMethodDefinition)
.Where(m => m.GetCustomAttributes().Any(c => TenAttribute(md, md.GetCustomAttribute(c)) == "System.Runtime.CompilerServices.ModuleInitializerAttribute"))
.Select(m => $"{md.GetString(md.GetTypeDefinition(m.GetDeclaringType()).Name)}.{md.GetString(m.Name)}");
yield return ("module initializer", 4, $"{p}: <Module>..cctor chạy trước lời gọi đầu tiên vào assembly; [ModuleInitializer]: {string.Join(", ", goi.DefaultIfEmpty("không rõ"))}");
}
// Lời gọi ra ngoài assembly: bảng MemberRef, mỗi dòng là kiểu::thành viên.
var goiRa = md.MemberReferences.Select(md.GetMemberReference)
.Select(m => (Kieu: TenKieu(md, m.Parent), Ten: md.GetString(m.Name), Sig: md.GetBlobBytes(m.Signature))).ToList();
foreach (var nhom in goiRa.Select(m => PhanLoai(m.Kieu, m.Ten, m.Sig, laAnalyzer)).OfType<(string Nhom, int Diem, string Api)>()
.GroupBy(x => (x.Nhom, x.Diem)))
yield return (nhom.Key.Nhom, nhom.Key.Diem, $"{p}: {string.Join(", ", nhom.Select(x => x.Api).Distinct())}");
PhanLoai là bảng luật ở mục 4, viết bằng switch trên cặp kiểu và tên. Assembly.Load chỉ bị tính 3 điểm khi blob chữ ký có hai byte 0x1D 0x05, tức tham số byte[]. Đối số là một file .nupkg, một thư mục, hoặc một lock file kèm --goc. Với gói generator mẫu, dotnet run SoiGoi.cs -- feed\BHPay.TienIch.MaLoi.1.0.0.nupkg in:
BHPay.TienIch.MaLoi 1.0.0 (4.541 byte, 5 file, tác giả: BHPay thu nghiem)
điểm 6: CHẶN, cần người duyệt
[4] analyzer: analyzers/dotnet/cs/BHPay.TienIch.MaLoi.dll: chạy trong trình biên dịch; MaLoiGenerator [Generator]
[2] I/O trong analyzer: analyzers/dotnet/cs/BHPay.TienIch.MaLoi.dll: Path.GetTempPath, Directory.CreateDirectory, Environment.GetCommandLineArgs, Process.GetCurrentProcess, Process.get_Id, File.AppendAllText
1 gói, 1 gói từ 4 điểm (1 gói có code tự chạy lúc build hoặc lúc nạp), 0 lỗi, 71 ms
Trong CI, công cụ tìm .nupkg trong NUGET_PACKAGES, không có thì trong %USERPROFILE%\.nuget\packages, nên restore và bước soi phải thấy cùng thư mục:
git show origin/main:src/BHPay.Api/packages.lock.json > lock-main.json
dotnet restore src/BHPay.Api --locked-mode
dotnet run tools/SoiGoi.cs -- src/BHPay.Api/packages.lock.json --goc lock-main.json
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # 2: có gói cần duyệt, 3: không soi được gói nào đó
dotnet build src/BHPay.Api --no-restore
Gói đã duyệt vào lock file nhánh chính khi PR được merge, nên lock file đó cũng là danh sách gói đã duyệt. dotnet build mặc định restore ngầm; --no-restore giữ đúng thứ tự restore, soi, build.
SoiGoi.cs: toàn bộ công cụ, chạy bằng dotnet run SoiGoi.cs -- <file.nupkg | thư mục | packages.lock.json>
// SoiGoi.cs: xem tĩnh gói NuGet trước khi cài. Chỉ đọc byte: không nạp assembly, không chạy code nào của gói.
// dotnet run SoiGoi.cs -- <file.nupkg | thư mục chứa .nupkg>
// dotnet run SoiGoi.cs -- packages.lock.json [--goc lock-cua-nhanh-chinh.json] (CI: chỉ soi gói mới)
// dotnet run SoiGoi.cs -- <thư mục> --tong-hop (chỉ đếm, không in tên gói)
// Mã thoát: 0 khi mọi gói dưới ngưỡng, 2 khi có gói từ ngưỡng trở lên (cần người duyệt), 3 khi không soi được gói nào đó.
using System.Diagnostics;
using System.Globalization;
using System.IO.Compression;
using System.Reflection.Metadata;
using System.Reflection.Metadata.Ecma335;
using System.Reflection.PortableExecutable;
using System.Security.Cryptography;
using System.Text.Json;
using System.Xml.Linq;
const int Nguong = 4;
CultureInfo.CurrentCulture = new CultureInfo("vi-VN");
string vao = args[0];
string? goc = args.SkipWhile(a => a != "--goc").Skip(1).FirstOrDefault();
bool tongHop = args.Contains("--tong-hop");
var dsGoi = new List<(string Ten, string File, string? Hash)>();
if (vao.EndsWith(".json", StringComparison.OrdinalIgnoreCase))
{
string kho = Environment.GetEnvironmentVariable("NUGET_PACKAGES")
?? Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".nuget", "packages");
var cu = goc is null ? [] : DocLock(goc).Select(g => g.Ten).ToHashSet(StringComparer.OrdinalIgnoreCase);
foreach (var (ten, id, ver, hash) in DocLock(vao).Where(g => !cu.Contains(g.Ten)))
dsGoi.Add((ten, Path.Combine(kho, id.ToLowerInvariant(), ver.ToLowerInvariant(), $"{id}.{ver}.nupkg".ToLowerInvariant()), hash));
}
else if (Directory.Exists(vao))
dsGoi.AddRange(Directory.EnumerateFiles(vao, "*.nupkg", SearchOption.AllDirectories).Select(f => (Path.GetFileName(f), f, (string?)null)));
else
dsGoi.Add((Path.GetFileName(vao), vao, null));
var dem = new SortedDictionary<string, int>();
int chan = 0, tuChay = 0, loi = 0;
var dongHo = Stopwatch.StartNew();
foreach (var (ten, file, hash) in dsGoi)
{
try
{
if (hash is not null && Convert.ToBase64String(SHA512.HashData(File.ReadAllBytes(file))) != hash)
throw new InvalidDataException("contentHash trong lock file không khớp file .nupkg");
var (tieuDe, phatHien) = Soi(file);
var theoNhom = phatHien.GroupBy(p => p.Nhom).Select(g => (Nhom: g.Key, Diem: g.Max(p => p.Diem))).ToList();
int diem = theoNhom.Sum(n => n.Diem); // mỗi nhóm lấy mức cao nhất, cộng các nhóm
foreach (var n in theoNhom) dem[$"{n.Nhom}, {n.Diem} điểm"] = dem.GetValueOrDefault($"{n.Nhom}, {n.Diem} điểm") + 1;
if (diem >= Nguong) chan++;
if (theoNhom.Any(n => n.Diem >= 4 && n.Nhom is "build" or "analyzer" or "module initializer")) tuChay++;
if (tongHop) continue;
Console.WriteLine($"{tieuDe}\n điểm {diem}: {(diem == 0 ? "không thấy code tự chạy" : diem < Nguong ? "xem lại" : "CHẶN, cần người duyệt")}");
foreach (var p in phatHien) Console.WriteLine($" [{p.Diem}] {p.Nhom}: {p.MoTa}");
}
catch (Exception e) { loi++; if (!tongHop) Console.WriteLine($"{ten}\n LỖI: {e.Message}"); }
}
Console.WriteLine($"\n{dsGoi.Count} gói, {chan} gói từ {Nguong} điểm ({tuChay} gói có code tự chạy lúc build hoặc lúc nạp), {loi} lỗi, {dongHo.ElapsedMilliseconds} ms");
if (tongHop) foreach (var (nhom, n) in dem) Console.WriteLine($" {nhom}: {n} gói");
return loi > 0 ? 3 : chan > 0 ? 2 : 0;
// ---------- Soi một gói ----------
static (string TieuDe, List<(string Nhom, int Diem, string MoTa)> PhatHien) Soi(string file)
{
using var zip = ZipFile.OpenRead(file);
var ph = new List<(string Nhom, int Diem, string MoTa)>();
var nuspec = XDocument.Load(zip.Entries.Single(e => !e.FullName.Contains('/') && e.Name.EndsWith(".nuspec", StringComparison.OrdinalIgnoreCase)).Open());
string Meta(string ten) => nuspec.Descendants().FirstOrDefault(x => x.Name.LocalName == ten)?.Value ?? "";
string tieuDe = $"{Meta("id")} {Meta("version")} ({new FileInfo(file).Length:N0} byte, {zip.Entries.Count} file, tác giả: {Meta("authors")})";
foreach (var e in zip.Entries.Where(e => e.Length > 0))
{
string p = e.FullName.Replace('\\', '/'), thuMuc = p.Split('/')[0].ToLowerInvariant(), duoi = Path.GetExtension(p).ToLowerInvariant();
bool laBuild = thuMuc is "build" or "buildtransitive" or "buildmultitargeting" or "buildcrosstargeting";
if (laBuild && duoi is ".targets" or ".props")
ph.AddRange(SoiMsBuild(p, XDocument.Load(e.Open())));
else if (duoi == ".ps1" && thuMuc == "tools")
ph.Add(("script tools/", 1, $"{p}: chỉ chạy với packages.config, không chạy với PackageReference"));
else if (thuMuc == "contentfiles" && duoi == ".cs")
ph.Add(("contentFiles", 2, $"{p}: mặc định buildAction Compile, biên dịch thẳng vào project"));
else if (duoi is ".dll" or ".exe" or ".so" or ".dylib")
{
if (thuMuc == "ref") continue; // reference assembly: chỉ có chữ ký, không có thân hàm
using var ms = new MemoryStream();
e.Open().CopyTo(ms);
ph.AddRange(SoiAssembly(p, ms.ToArray(), thuMuc == "analyzers", laBuild));
}
}
return (tieuDe, ph);
}
// .props/.targets: Target nào chạy, móc vào đâu, gọi task gì.
static IEnumerable<(string, int, string)> SoiMsBuild(string p, XDocument x)
{
string[] nguyHiem = ["Exec", "DownloadFile", "UsingTask"];
var targets = x.Descendants().Where(t => t.Name.LocalName == "Target").ToList();
if (targets.Count == 0) { yield return ("build", 1, $"{p}: chỉ đặt property/item, không có Target"); yield break; }
foreach (var t in targets)
{
var tasks = t.Elements().Select(c => c.Name.LocalName).Where(n => n is not ("PropertyGroup" or "ItemGroup")).Distinct().ToList();
string moc = string.Join(" ", new[] { "BeforeTargets", "AfterTargets", "DependsOnTargets" }
.Select(a => t.Attribute(a) is { } v ? $"{a}={v.Value}" : null).OfType<string>());
bool nang = tasks.Any(nguyHiem.Contains) || x.Descendants().Any(u => u.Name.LocalName == "UsingTask");
bool chiGhiLog = tasks.All(n => n is "Message" or "Warning" or "Error"); // task có sẵn, chỉ in ra log
yield return ("build", nang ? 6 : chiGhiLog ? 1 : 4, $"{p}: Target {t.Attribute("Name")?.Value} ({moc}) gọi {string.Join(", ", tasks.DefaultIfEmpty("không task nào"))}");
}
}
// Một file nhị phân: native thì chỉ báo có; managed thì đọc metadata.
static IEnumerable<(string, int, string)> SoiAssembly(string p, byte[] bytes, bool laAnalyzer, bool laBuild)
{
if (!LaAssemblyNet(bytes)) { yield return ("mã máy", 1, $"{p}: native (PE không metadata, ELF, Mach-O), không đọc được"); yield break; }
using var pe = new PEReader(new MemoryStream(bytes));
var md = pe.GetMetadataReader();
if (laBuild) yield return ("build", 4, $"{p}: DLL task MSBuild, chạy trong tiến trình build");
// Analyzer và source generator: lớp gắn [DiagnosticAnalyzer] hoặc [Generator].
var lop = md.TypeDefinitions.Select(md.GetTypeDefinition)
.SelectMany(t => t.GetCustomAttributes().Select(c => TenAttribute(md, md.GetCustomAttribute(c)))
.Where(a => a is "Microsoft.CodeAnalysis.GeneratorAttribute" or "Microsoft.CodeAnalysis.Diagnostics.DiagnosticAnalyzerAttribute")
.Select(a => $"{md.GetString(t.Name)} [{a.Split('.')[^1].Replace("Attribute", "")}]")).ToList();
if (laAnalyzer)
yield return ("analyzer", 4, $"{p}: chạy trong trình biên dịch; {(lop.Count > 0 ? string.Join(", ", lop) : "DLL phụ trong analyzers/")}");
// Module initializer: kiểu <Module> (dòng 1 bảng TypeDef) có .cctor.
var module = md.GetTypeDefinition(MetadataTokens.TypeDefinitionHandle(1));
if (module.GetMethods().Any(m => md.GetString(md.GetMethodDefinition(m).Name) == ".cctor"))
{
var goi = md.MethodDefinitions.Select(md.GetMethodDefinition)
.Where(m => m.GetCustomAttributes().Any(c => TenAttribute(md, md.GetCustomAttribute(c)) == "System.Runtime.CompilerServices.ModuleInitializerAttribute"))
.Select(m => $"{md.GetString(md.GetTypeDefinition(m.GetDeclaringType()).Name)}.{md.GetString(m.Name)}");
yield return ("module initializer", 4, $"{p}: <Module>..cctor chạy trước lời gọi đầu tiên vào assembly; [ModuleInitializer]: {string.Join(", ", goi.DefaultIfEmpty("không rõ"))}");
}
// P/Invoke: method có cờ PinvokeImpl, kèm DLL và tên hàm native.
var pinvoke = md.MethodDefinitions.Select(md.GetMethodDefinition)
.Where(m => (m.Attributes & System.Reflection.MethodAttributes.PinvokeImpl) != 0)
.Select(m => m.GetImport()).Select(i => $"{md.GetString(md.GetModuleReference(i.Module).Name)}!{md.GetString(i.Name)}")
.Distinct().ToList();
if (pinvoke.Count > 0) yield return ("P/Invoke", 1, $"{p}: {pinvoke.Count} hàm native: {string.Join(", ", pinvoke.Take(5))}{(pinvoke.Count > 5 ? ", ..." : "")}");
// Lời gọi ra ngoài assembly: bảng MemberRef, mỗi dòng là kiểu::thành viên.
var goiRa = md.MemberReferences.Select(md.GetMemberReference)
.Select(m => (Kieu: TenKieu(md, m.Parent), Ten: md.GetString(m.Name), Sig: md.GetBlobBytes(m.Signature))).ToList();
foreach (var nhom in goiRa.Select(m => PhanLoai(m.Kieu, m.Ten, m.Sig, laAnalyzer)).OfType<(string Nhom, int Diem, string Api)>()
.GroupBy(x => (x.Nhom, x.Diem)))
yield return (nhom.Key.Nhom, nhom.Key.Diem, $"{p}: {string.Join(", ", nhom.Select(x => x.Api).Distinct())}");
}
// Bảng luật: lời gọi nào đáng xem và nặng bao nhiêu.
static (string, int, string)? PhanLoai(string kieu, string ten, byte[] sig, bool laAnalyzer) => (kieu, ten) switch
{
("System.Diagnostics.Process", "Start") => ("tiến trình", 4, "Process.Start"),
("System.Net.Http.HttpClient" or "System.Net.WebClient" or "System.Net.Sockets.Socket" or "System.Net.Sockets.TcpClient", _)
=> ("mạng", 2, $"{kieu.Split('.')[^1]}.{ten}"),
("System.Reflection.Assembly", "Load") when sig.AsSpan().IndexOf([(byte)0x1D, (byte)0x05]) >= 0
=> ("nạp code động", 3, "Assembly.Load(byte[])"), // 0x1D 0x05 = byte[] trong chữ ký
("System.Reflection.Assembly", "LoadFrom" or "LoadFile" or "UnsafeLoadFrom")
or ("System.Runtime.Loader.AssemblyLoadContext", "LoadFromStream" or "LoadFromAssemblyPath")
=> ("nạp code động", 3, $"{kieu.Split('.')[^1]}.{ten}"),
("System.Reflection.Assembly", "GetManifestResourceStream") => ("nạp code động", 1, "GetManifestResourceStream"),
("System.Runtime.InteropServices.NativeLibrary", "Load" or "TryLoad")
or ("System.Runtime.InteropServices.Marshal", "GetDelegateForFunctionPointer") => ("native động", 2, $"{kieu.Split('.')[^1]}.{ten}"),
// Trong analyzer: các API mà luật RS1035 cấm analyzer dùng (file, thư mục tạm, biến môi trường, tiến trình).
("System.IO.File" or "System.IO.Directory" or "System.Environment" or "System.Diagnostics.Process", _)
or ("System.IO.Path", "GetTempPath") when laAnalyzer
=> ("I/O trong analyzer", 2, $"{kieu.Split('.')[^1]}.{ten}"),
_ => null,
};
static bool LaAssemblyNet(byte[] bytes)
{
try { using var pe = new PEReader(new MemoryStream(bytes)); return pe.HasMetadata; }
catch (BadImageFormatException) { return false; } // không phải PE: .so, .dylib
}
static string TenKieu(MetadataReader md, EntityHandle h) => h.Kind switch
{
HandleKind.TypeReference => md.GetTypeReference((TypeReferenceHandle)h) is var t ? $"{md.GetString(t.Namespace)}.{md.GetString(t.Name)}" : "",
HandleKind.TypeDefinition => md.GetTypeDefinition((TypeDefinitionHandle)h) is var d ? $"{md.GetString(d.Namespace)}.{md.GetString(d.Name)}" : "",
_ => "",
};
static string TenAttribute(MetadataReader md, CustomAttribute a) => a.Constructor.Kind switch
{
HandleKind.MemberReference => TenKieu(md, md.GetMemberReference((MemberReferenceHandle)a.Constructor).Parent),
HandleKind.MethodDefinition => TenKieu(md, md.GetMethodDefinition((MethodDefinitionHandle)a.Constructor).GetDeclaringType()),
_ => "",
};
// packages.lock.json: mọi gói (trực tiếp và bắc cầu) của mọi target framework, kèm contentHash.
static IEnumerable<(string Ten, string Id, string Ver, string Hash)> DocLock(string file)
{
using var doc = JsonDocument.Parse(File.ReadAllText(file));
return doc.RootElement.GetProperty("dependencies").EnumerateObject()
.SelectMany(tfm => tfm.Value.EnumerateObject())
.Where(g => g.Value.TryGetProperty("resolved", out _) && g.Value.TryGetProperty("contentHash", out _))
.Select(g => (Ten: $"{g.Name}@{g.Value.GetProperty("resolved").GetString()}", Id: g.Name,
Ver: g.Value.GetProperty("resolved").GetString()!, Hash: g.Value.GetProperty("contentHash").GetString()!))
.DistinctBy(g => g.Ten, StringComparer.OrdinalIgnoreCase).ToList();
}
6. Chứng minh: file đánh dấu thật và điểm của công cụ
Năm gói mẫu và cách đo
Năm gói BHPay.TienIch.* tự dựng, đều vô hại. DinhDang sạch, chỉ định dạng tiền. CauHinh mang file .targets ở mục 3. MaLoi là source generator sinh hằng mã lỗi 50010, 50011. NhatKy có [ModuleInitializer]. MoiTruong gọi Process.Start tới cmd /c echo. CauHinh, MaLoi, NhatKy ghi file đánh dấu vào %TEMP%\soi-goi-danh-dau\, MoiTruong chỉ in một dòng. Gói nằm trên feed thư mục cục bộ; nuget.config của project thử chỉ có nguồn đó, NUGET_PACKAGES trỏ tới thư mục riêng.
Năm gói mẫu: file project, đóng gói bằng dotnet pack <project> -c Release --configfile goi\nuget.config
<!-- goi\Directory.Build.props -->
<Project>
<PropertyGroup>
<Version>1.0.0</Version>
<Authors>BHPay thu nghiem</Authors>
<Description>Goi tu dung cho bai Mo goi NuGet truoc khi cai. Vo hai.</Description>
<PackageOutputPath>$(MSBuildThisFileDirectory)..\feed</PackageOutputPath>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
</Project>
<!-- goi\DinhDang\BHPay.TienIch.DinhDang.csproj; NhatKy và MoiTruong giống hệt, chỉ đổi PackageId -->
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<PackageId>BHPay.TienIch.DinhDang</PackageId>
</PropertyGroup>
</Project>
<!-- goi\CauHinh\BHPay.TienIch.CauHinh.csproj: đóng file .targets ở mục 3 vào hai thư mục -->
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<PackageId>BHPay.TienIch.CauHinh</PackageId>
</PropertyGroup>
<ItemGroup>
<None Include="BHPay.TienIch.CauHinh.targets" Pack="true" PackagePath="build/;buildTransitive/" />
</ItemGroup>
</Project>
<!-- goi\MaLoi\BHPay.TienIch.MaLoi.csproj: DLL đặt vào analyzers/dotnet/cs -->
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<PackageId>BHPay.TienIch.MaLoi</PackageId>
<LangVersion>latest</LangVersion>
<ImplicitUsings>disable</ImplicitUsings>
<IncludeBuildOutput>false</IncludeBuildOutput>
<SuppressDependenciesWhenPacking>true</SuppressDependenciesWhenPacking>
<NoWarn>$(NoWarn);NU5128</NoWarn>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.14.0" PrivateAssets="all" />
<None Include="$(OutputPath)\$(AssemblyName).dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
</ItemGroup>
</Project>
<!-- goi\nuget.config: chỉ dùng để đóng gói MaLoi, cần Microsoft.CodeAnalysis.CSharp -->
<configuration>
<packageSources>
<clear />
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
</packageSources>
</configuration>
Năm gói mẫu: mã nguồn C#
// ===== goi\DinhDang\DinhDang.cs =====
using System.Globalization;
namespace BHPay.TienIch;
public static class DinhDang
{
static readonly NumberFormatInfo Vn = new() { NumberGroupSeparator = ".", NumberDecimalSeparator = "," };
// 1500000m -> "1.500.000 đ"
public static string Tien(decimal soTien) => soTien.ToString("#,0", Vn) + " đ";
}
// ===== goi\CauHinh\CauHinh.cs =====
namespace BHPay.TienIch;
public static class CauHinh
{
public const string MoiTruong = "thu-nghiem";
}
// ===== goi\MaLoi\MaLoiGenerator.cs =====
using System;
using System.IO;
using Microsoft.CodeAnalysis;
namespace BHPay.TienIch;
// Source generator sinh hằng mã lỗi của BHPay.
[Generator]
public sealed class MaLoiGenerator : IIncrementalGenerator
{
public void Initialize(IncrementalGeneratorInitializationContext context)
{
// Mẫu vô hại: ghi một file đánh dấu để thấy code của gói chạy bên trong trình biên dịch.
string thuMuc = Path.Combine(Path.GetTempPath(), "soi-goi-danh-dau");
Directory.CreateDirectory(thuMuc);
File.AppendAllText(Path.Combine(thuMuc, "maloi-generator.txt"),
$"generator chay trong {Path.GetFileName(Environment.GetCommandLineArgs()[0])} pid {System.Diagnostics.Process.GetCurrentProcess().Id} {DateTime.Now:HH:mm:ss.fff}\n");
context.RegisterPostInitializationOutput(ctx => ctx.AddSource("MaLoi.g.cs",
"namespace BHPay.TienIch { public static class MaLoi { public const int SoDuKhongDu = 50010; public const int ViKhongTonTai = 50011; } }"));
}
}
// ===== goi\NhatKy\NhatKy.cs =====
using System.Runtime.CompilerServices;
namespace BHPay.TienIch;
public static class NhatKy
{
public static string Dong(string noiDung) => $"[nhat-ky] {noiDung}";
}
static class KhoiDong
{
// Mẫu vô hại: ghi một file đánh dấu khi module được khởi tạo, trước mọi lời gọi của app.
[ModuleInitializer]
internal static void Chay()
{
string thuMuc = Path.Combine(Path.GetTempPath(), "soi-goi-danh-dau");
Directory.CreateDirectory(thuMuc);
File.AppendAllText(Path.Combine(thuMuc, "nhatky-module-init.txt"),
$"module initializer chay trong {Path.GetFileName(Environment.ProcessPath)} {DateTime.Now:HH:mm:ss.fff}\n");
}
}
// ===== goi\MoiTruong\MoiTruong.cs =====
using System.Diagnostics;
namespace BHPay.TienIch;
public static class MoiTruong
{
// Mẫu vô hại: gọi cmd chỉ để in một dòng.
public static string KiemTra()
{
var psi = new ProcessStartInfo("cmd.exe", "/c echo BHPay.TienIch.MoiTruong da goi cmd")
{
RedirectStandardOutput = true,
UseShellExecute = false,
};
using var p = Process.Start(psi)!;
string dong = p.StandardOutput.ReadToEnd().Trim();
p.WaitForExit();
return dong;
}
}
ThuNghiem.ps1 đặt từng gói vào một project console .NET 10 mới, chạy dotnet restore hai lần, dotnet build --no-restore, dotnet run --no-build, và đọc thư mục đánh dấu sau mỗi pha. NapThu.cs nạp DLL của NhatKy bằng reflection để tách lúc nạp khỏi lúc gọi.
ThuNghiem.ps1: thử từng gói qua bốn pha, đọc file đánh dấu sau mỗi pha
param([string[]]$Goi = @("DinhDang","CauHinh","MaLoi","NhatKy","MoiTruong"))
# Thử từng gói trong một project console .NET 10 mới, ghi lại file đánh dấu xuất hiện sau mỗi pha.
$re = $PSScriptRoot # thư mục chứa script, feed\ và goi\
$env:NUGET_PACKAGES = "$re\nuget-cache"
$env:DOTNET_CLI_TELEMETRY_OPTOUT = "1"
$danhDau = Join-Path ([IO.Path]::GetTempPath()) "soi-goi-danh-dau"
$code = @{
DinhDang = 'Console.WriteLine(BHPay.TienIch.DinhDang.Tien(1500000m));'
CauHinh = 'Console.WriteLine(BHPay.TienIch.CauHinh.MoiTruong);'
MaLoi = 'Console.WriteLine(BHPay.TienIch.MaLoi.SoDuKhongDu);'
NhatKy = 'Console.WriteLine("truoc loi goi dau tien, co danh dau: " + System.IO.Directory.Exists(System.IO.Path.Combine(System.IO.Path.GetTempPath(), "soi-goi-danh-dau"))); Goi(); static void Goi() => Console.WriteLine(BHPay.TienIch.NhatKy.Dong("da goi"));'
MoiTruong = 'Console.WriteLine(BHPay.TienIch.MoiTruong.KiemTra());'
}
function DanhDau { if (Test-Path -LiteralPath $danhDau) { (Get-ChildItem -LiteralPath $danhDau | ForEach-Object { "$($_.Name): " + ((Get-Content -LiteralPath $_.FullName) -join ' | ') }) -join '; ' } else { "-" } }
function XoaDanhDau { if (Test-Path -LiteralPath $danhDau) { Get-ChildItem -LiteralPath $danhDau -File | ForEach-Object { [IO.File]::Delete($_.FullName) }; [IO.Directory]::Delete($danhDau) } }
foreach ($g in $Goi) {
$dir = "$re\thu\$g"
if (Test-Path -LiteralPath $dir) { [IO.Directory]::Delete($dir, $true) }
New-Item -ItemType Directory -Force $dir | Out-Null
@"
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<clear />
<add key="feed-cuc-bo" value="$re\feed" />
</packageSources>
</configuration>
"@ | Set-Content -Encoding utf8 "$dir\nuget.config"
@"
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="BHPay.TienIch.$g" Version="1.0.0" />
</ItemGroup>
</Project>
"@ | Set-Content -Encoding utf8 "$dir\Thu$g.csproj"
$code[$g] | Set-Content -Encoding utf8 "$dir\Program.cs"
XoaDanhDau
"== $g"
dotnet restore "$dir\Thu$g.csproj" -nologo -v q | Out-Null; "restore 1: $(DanhDau)"
dotnet restore "$dir\Thu$g.csproj" -nologo -v q | Out-Null; "restore 2: $(DanhDau)"
dotnet build "$dir\Thu$g.csproj" --no-restore -nologo -v q | Out-Null; "build : $(DanhDau)"
$out = dotnet run --project "$dir\Thu$g.csproj" --no-build 2>&1; "run out : $($out -join ' | ')"
"run : $(DanhDau)"
}
NapThu.cs: module initializer chạy lúc nạp hay lúc gọi, chạy bằng dotnet run NapThu.cs -- <đường dẫn BHPay.TienIch.NhatKy.dll>
// Kiểm module initializer chạy lúc nào: chỉ nạp assembly, đọc kiểu bằng reflection, hay khi gọi method.
using System.Reflection;
string dll = args[0];
string danhDau = Path.Combine(Path.GetTempPath(), "soi-goi-danh-dau", "nhatky-module-init.txt");
if (File.Exists(danhDau)) File.Delete(danhDau);
var asm = Assembly.LoadFrom(dll);
Console.WriteLine($"sau LoadFrom: {File.Exists(danhDau)}");
var kieu = asm.GetTypes();
Console.WriteLine($"sau GetTypes ({kieu.Length} kieu): {File.Exists(danhDau)}");
var dong = asm.GetType("BHPay.TienIch.NhatKy")!.GetMethod("Dong")!;
Console.WriteLine($"sau GetMethod: {File.Exists(danhDau)}");
dong.Invoke(null, ["x"]);
Console.WriteLine($"sau Invoke: {File.Exists(danhDau)}");
Gói nào chạy code, và lúc nào
Đo trên laptop Intel Core Ultra 5 125U, Windows 11, .NET SDK 10.0.401, runtime 10.0.12, ngày 2026-10-04. Mỗi ô là dấu vết mới xuất hiện sau pha đó, output đầy đủ ở khối thu gọn dưới bảng:
| Gói | Restore, 2 lần | Build | App chạy |
|---|---|---|---|
DinhDang |
không | không | không |
CauHinh |
không | file đánh dấu, project ThuCauHinh |
không thêm |
MaLoi |
không | file đánh dấu, VBCSCompiler.dll pid 43092 |
không thêm |
NhatKy |
không | không | file đánh dấu sau lời gọi đầu |
MoiTruong |
không | không | dòng in từ cmd |
Output nguyên văn của ThuNghiem.ps1
== DinhDang
restore 1: -
restore 2: -
build : -
run out : 1.500.000 đ
run : -
== CauHinh
restore 1: -
restore 2: -
build : cauhinh-targets.txt: targets chay luc build: ThuCauHinh 17:22:04.172
run out : thu-nghiem
run : cauhinh-targets.txt: targets chay luc build: ThuCauHinh 17:22:04.172
== MaLoi
restore 1: -
restore 2: -
build : maloi-generator.txt: generator chay trong VBCSCompiler.dll pid 43092 17:22:11.603
run out : 50010
run : maloi-generator.txt: generator chay trong VBCSCompiler.dll pid 43092 17:22:11.603
== NhatKy
restore 1: -
restore 2: -
build : -
run out : truoc loi goi dau tien, co danh dau: False | [nhat-ky] da goi
run : nhatky-module-init.txt: module initializer chay trong ThuNhatKy.exe 17:22:21.730
== MoiTruong
restore 1: -
restore 2: -
build : -
run out : BHPay.TienIch.MoiTruong da goi cmd
run : -
Không gói nào chạy code lúc restore, kể cả lần thứ hai khi obj/ đã có nuget.g.targets. Hai gói chạy code lúc build, trước khi một dòng nào của app chạy. VBCSCompiler pid 43092 khởi động lúc 17:20:05 khi đóng gói các mẫu và vẫn chạy khi phép đo kết thúc: code analyzer nằm trong một tiến trình dùng chung, sống lâu hơn một lần build. NhatKy in False trước lời gọi đầu, rồi file đánh dấu mới xuất hiện; NapThu.cs in False sau LoadFrom, GetTypes, GetMethod và True sau Invoke.
Phép thử bắc cầu: project HaTang dùng gói CauHinh, project Api chỉ có ProjectReference tới HaTang. Build Api ghi hai dòng đánh dấu, HaTang và Api: buildTransitive/ đưa Target vào cả project không khai báo gói.
Công cụ bắt được gì
dotnet run SoiGoi.cs -- feed trên năm gói, đối chiếu với bảng trên:
| Gói | Đo được | SoiGoi.cs báo |
Điểm | Kết quả |
|---|---|---|---|---|
DinhDang |
không | không gì | 0 | đúng |
CauHinh |
build | Target trước CoreCompile, gọi WriteLinesToFile |
4 | đúng |
MaLoi |
build | MaLoiGenerator, 6 lời gọi RS1035 cấm |
6 | đúng |
NhatKy |
lời gọi đầu | <Module>..cctor gọi KhoiDong.Chay |
4 | đúng |
MoiTruong |
khi gọi | Process.Start |
4 | đúng |
Ở chế độ CI, nhánh chính chỉ có DinhDang, PR thêm bốn gói còn lại:
| Lần chạy | Gói được soi | Thời gian soi, kể cả SHA-512 | Mã thoát |
|---|---|---|---|
Lock file PR, --goc lock nhánh chính |
4 gói mới | 89 ms | 2 |
| Lock file nhánh chính | 1 gói | 74 ms | 0 |
Lock file sửa một ký tự contentHash |
báo hash không khớp | 6 ms | 3 |
Cả lệnh dotnet run, tính cả bước kiểm tra build của file-based app, mất 0,51–1,59 giây qua ba lần chạy.
Trên gói thật: cổng chặn bao nhiêu
Năm gói mẫu do chính người viết công cụ dựng, nên 4 trên 4 chỉ chứng minh luật khớp mẫu, không đo được tỉ lệ sót trên gói thật. Để biết cổng ồn đến đâu, công cụ chạy --tong-hop trên cache NuGet của máy đo: 1.950 phiên bản gói thật của 740 id, chỉ đếm, không in tên. Lần chạy cuối mất 35,3 giây, 0 lỗi.
228 gói, tức 11,7%, chạm ngưỡng; 127 gói có code tự chạy lúc build hoặc lúc nạp. Luật "chỉ in log" giữ con số đó thấp: 540 gói có .props hay .targets chỉ đặt property hoặc in cảnh báo; tính mọi Target là 4 điểm thì 647 gói chạm ngưỡng.
Vì vậy cổng chỉ soi gói mới. Soi lại cả cây ở mỗi PR thì hơn một phần mười số gói đang dùng chặn mọi PR; soi theo lock file thì mỗi gói chờ duyệt một lần, lúc mới vào.
Giới hạn của phân tích tĩnh
- Code tải từ mạng lúc chạy không có trong gói; công cụ chỉ thấy lời gọi trung gian như
HttpClientvàAssembly.Load(byte[]). - Mã máy native chỉ được ghi nhận là có.
- Metadata cho biết có lời gọi, không cho biết khi nào nó chạy.
- Công cụ không đánh giá
Conditioncủa MSBuild, nên đếm cảTargetkhông bao giờ chạy với project củaBHPay. - 0 điểm nghĩa là không thấy dấu hiệu trong bảng luật, không nghĩa là gói an toàn.
7. Kết luận
Code của gói NuGet chạy được ở ba lúc mà API không gọi nó: khi MSBuild nhập .targets, khi trình biên dịch nạp analyzer, và khi lời gọi đầu tiên kích hoạt module initializer. Cả ba để lại dấu trong .nupkg, đọc được trước khi build mà không chạy gì.
Trong dự án .NET của bạn:
- Bật
RestorePackagesWithLockFile, commitpackages.lock.json; trong CI chạydotnet restore --locked-mode, bước soi, rồidotnet build --no-restore. - Bước soi dùng
SoiGoi.cshoặc công cụ tương tự trênSystem.Reflection.Metadata, so lock file PR với nhánh chính, chặn khi mã thoát khác 0. - Gói bị chặn: đọc
.targets, mở bằng ILSpy đúng method mà báo cáo nêu, nhưKhoiDong.Chay. - Agent build PR giữ ít bí mật nhất có thể; khóa ký webhook thật không nằm ở đó.
- Lớp phòng thủ lúc cài đi kèm, không thay bước soi: package source mapping (NuGet 6.0+) ghim mỗi tiền tố id vào một nguồn,
--locked-modegiữ đúng phiên bản và hash, NuGet Audit chỉ báo lỗ hổng đã công bố.
Những chỗ hay hiểu sai
- "Module initializer chạy ngay khi assembly được nạp." Trên .NET 10.0.12 nó chạy ở lời gọi đầu tiên vào assembly, không ở
LoadFrom. - "
PackageReferencekhông chạyinstall.ps1nên gói không chạy gì lúc cài.".targetsvà analyzer vẫn chạy lúc build. - "Gói gián tiếp không ảnh hưởng build."
buildTransitive/đi theoProjectReferencevào project không khai báo gói.
Đọc tiếp
- Xác thực webhook thanh toán: khóa ký HMAC là loại bí mật không nên nằm trên agent build PR.
- Chuyển tiền giữa hai ví: API
BHPaymà gói sẽ đi vào. - Model Context Protocol: cùng nguyên tắc, code không tin thì không cầm connection string.
Nguồn
Đọc và đối chiếu ngày 2026-10-04.
- NuGet: Create a package, MSBuild props and targets in a package, Analyzer formats, .nuspec reference.
- NuGet: Migrate packages.config to PackageReference, PackageReference in project files, Package Source Mapping, Auditing packages.
- .NET: dotnet build, ModuleInitializerAttribute, CA2255, MetadataReader, .NET Compiler Platform SDK.
- ECMA-335 phần II; dotnet/runtime, ECMA-335 augments, mục Module Initializer.
- Mã nguồn:
NuGet.targetstrong .NET SDK 10.0.401, dòng 109; dotnet/roslyn-analyzers, PR #6115 và danh sách API cấm trongMicrosoft.CodeAnalysis.Analyzers3.11.0.