LAB-AUTH-001 · 7 November 2026 · Version 1
.NET code review: invoice access control
Local demonstration using synthetic code and data. This report describes an internal exercise, with no client engagement or independent validation.
Scope and business rule
A C# console program targeting .NET 10, with no third-party NuGet dependencies, exposed service or real data. A synthetic invoice is accessible only to its owner within their organisation. ClaimsPrincipal identities are constructed by the tests, not supplied by an identity provider.
The scope covers the read function and ten scenarios. It excludes real authentication, JWTs, ASP.NET middleware, Entity Framework queries, Azure configuration and any third-party system.
Finding: authenticated does not mean authorised
The initial version checks that the user is authenticated, then looks up the invoice by ID only. It does not check the owner or organisation. In the lab, an identity from another organisation or owner therefore receives the synthetic data.
Illustrated risk: unauthorised reads across users or organisations. Potentially high impact in a system containing real invoices; no production CVSS score is assigned to this synthetic example. The scenario requires an authenticated identity and an existing invoice ID.
Fix
The fixed version requires exactly one non-empty value for the sub and tenant_id claims, then limits the lookup to the ID, organisation and owner using explicit comparisons. A missing or out-of-scope resource returns the same 404 status with no invoice body.
Missing identities are rejected (401), missing or ambiguous claims are rejected (403), and a non-positive ID is rejected (400). In production, claims must come from server-validated authentication and the restriction must be applied to the data query.
Observed results — the same assertions before and after the fix
| Scenario | Expected | Before | After |
|---|---|---|---|
| owner_can_read | 200 | 200 — PASS | 200 — PASS |
| other_tenant_denied | 404 | 200 — FAIL | 404 — PASS |
| other_owner_denied | 404 | 200 — FAIL | 404 — PASS |
| anonymous_denied | 401 | 401 — PASS | 401 — PASS |
| missing_tenant_denied | 403 | 200 — FAIL | 403 — PASS |
| missing_subject_denied | 403 | 200 — FAIL | 403 — PASS |
| duplicate_tenant_denied | 403 | 200 — FAIL | 403 — PASS |
| unknown_record_not_found | 404 | 404 — PASS | 404 — PASS |
| invalid_id_rejected | 400 | 400 — PASS | 400 — PASS |
| tenant_case_not_normalized | 404 | 200 — FAIL | 404 — PASS |
Before the fix, 4 tests passed and 6 failed, producing the expected exit code of 1. After the fix, all 10 tests passed, with exit code 0. These results confirm only the ten behaviours tested.
Reproduce the results
Download and extract the lab. With the .NET 10 SDK, from the extracted directory:
dotnet build -c Release
dotnet run -c Release --no-build -- --before
dotnet run -c Release --no-buildThe first run should fail with exit code 1, exposing the expected flaws in the initial educational code. The second should pass with exit code 0. Do not deploy the initial version as an accessible service.
Limitations and next steps
This unit-level validation does not assess a running API. For a real project, check the authentication chain, business roles, data filtering, other access paths and errors; add integration tests and checks suited to the authorised scope.
Not disclosing the response body does not establish the absence of side channels, log leaks or flaws in other functions. Results are self-produced without independent review. This lab illustrates a remediation approach; it does not replace verifiable professional references.