Contoso University is Microsoft's canonical ASP.NET Core Razor Pages tutorial app, a small CRUD site for students, instructors, courses and departments. We picked it for this walkthrough precisely because it's public and unremarkable: you can open every file yourself and check our work. It has no authentication, no payments, no crypto. Nothing here is designed to be a security showcase, and that's the point. This is what a scan of ordinary application code produces, end to end, and exactly what it costs.
We ran redmirror scan over the whole repository. Twelve minutes and $1.70 later, below is the report it produced, reproduced in full.
| Category | Count |
|---|---|
| Confirmed security defects | 5 |
| Low-impact / hardening | 3 |
| Needs further analysis | 14 |
| Investigated (not a security issue) | 7 |
29 candidates went in. Every one was verified against the real code before it was called a finding: a candidate is Confirmed only when the deciding code path was read and the defect follows from the code as written. Where the outcome depends on something defined elsewhere (a return type, a lock, a caller's guard, a platform sanitizer), that code was read too, not assumed. A candidate that a careful reading shows to be safe is demoted to “investigated” with the reason, never silently dropped. Severity is not inflated: something unreachable, or only dangerous outside a realistic threat model, is a hardening note, not a vulnerability.
Medium Pages/Instructors/Index.cshtml.cs:43 · CWE-388
Two independent if blocks each look correct, but they don't agree on the state they leave behind. InstructorData.Courses is only ever assigned inside the if (id != null) block, but a sibling block reads it, guarded only on courseID:
// Pages/Instructors/Index.cshtml.cs if (courseID != null) { CourseID = courseID.Value; var selectedCourse = InstructorData.Courses // null when id was absent .Where(x => x.CourseID == courseID).Single(); // -> NullReferenceException await _context.Entry(selectedCourse) .Collection(x => x.Enrollments).LoadAsync(); }
Root cause. InstructorIndexData.Courses has no default initializer and is populated only when an instructor id is supplied; the courseID block does not require id to also be set. So a request supplying courseID with no id (GET /Instructors?courseID=1050, no authentication required) reaches .Where() on a null collection and throws.
Fix. Nest the courseID block inside the id block (a course selection is only meaningful when an instructor is selected), or initialize Courses to an empty collection and guard the read.
Medium ×4 wwwroot/lib/jquery-validation-unobtrusive/ · CWE-1333
Four confirmed findings across two files, jquery.validate.unobtrusive.min.js:5 and jquery.validate.unobtrusive.js:33, in the vendored .NET Foundation validation library shipped with the template. The flagged pattern:
function splitAndTrim(value) { return value.replace(/^\s+|\s+$/g, "").split(/\s*,\s*/g); }
These weren't just flagged: they were executed in a sandbox. Matching a crafted ~50,000-character input exceeded 1.5 seconds (catastrophic backtracking), the signature of a denial-of-service vector. Because the code lives in a vendored dependency, the fix is to update or replace the library rather than edit your own source. An unconfirmed sibling lead on the same files, which the sandbox did not reproduce automatically, stayed in the “needs analysis” pile below rather than being promoted.
Instructors/Delete.cshtml.cs:46: SingleAsync(i => i.ID == id) throws InvalidOperationException if no match is found, so the if (instructor == null) check on line 48 is unreachable dead code. Fix: use SingleOrDefaultAsync so the null-check can handle the missing record.Instructors/Index.cshtml.cs:44: .Single() on the filtered course sequence throws when no course matches. Fix: initialize Courses to an empty collection and use .SingleOrDefault() with a null guard.Students/Delete.cshtml.cs:43: the format string uses {ID} where String.Format expects {0}, so the error message prints the literal text {ID} instead of the id.Candidates that couldn't be fully verified from the available code, shown for review, not confirmed.
| Candidate | Note |
|---|---|
| Array index out of boundsUtility.cs:7 | Accesses token[7] without checking the array has at least 8 elements, so IndexOutOfRangeException when token.Length ≤ 7. |
| Uninitialized variable in nonalphamin validatorjquery.validate.unobtrusive.js:356 | The validator function is truncated in the source window; the claim can't be verified without the full implementation. |
SelectList uses ID but the model may use InstructorIDDepartments/Edit.cshtml.cs:37 | Can't confirm the Instructor model's primary-key property name without reading Instructor.cs; the inconsistency stays unverified. |
Single() throws on an empty sequenceInstructors/Index.cshtml.cs:36 | Behavior depends on whether id is constrained to existing instructors or is arbitrary input, and route constraints weren't available to confirm. |
Division by zero when pageSize is 0PaginatedList.cs:13 | The scanner's DivideByZeroException claim is technically wrong (C# double division yields Infinity; the int cast throws OverflowException). Reachability of pageSize=0 unconfirmed. |
| Silent failure when course not foundCourses/Delete.cshtml.cs:49 | Whether a silent no-op on a missing resource is intended can't be decided from the shown code alone. |
| Course Title rendered without sanitizationInstructorCoursesPageModel.cs:24 | ASP.NET encodes @Model.Title / @Html.DisplayFor() by default; real only if the view uses @Html.Raw(). The PageModel alone doesn't decide it; the view does. |
| Deferred execution / possible N+1InstructorCoursesPageModel.cs:15 | Enumerated once while the context is alive; no navigation properties touched during iteration. Not a defect here. |
| Incorrect key access in addMinMax adapterjquery.validate.unobtrusive.js:306 | Plausible pattern, but the exact lines couldn't be retrieved to confirm. |
instructor.Courses null dereferenceInstructorCoursesPageModel.cs:17 | PopulateAssignedCourseData calls instructor.Courses.Select() with no null check; the Instructor class doesn't guarantee the property is initialized. |
| + 4 unconfirmed ReDoS leadsjquery.validate.unobtrusive[.min].js | Not reproduced automatically. Confirming a complexity lead means executing untrusted code on deliberately resource-exhausting input, done only in a capped, isolated sandbox, so these stay leads until reproduced. |
Each candidate below was surfaced by the scan and then disproven by reading the deciding code. Listed so the review is auditable and these aren't re-reported next run. This is the half of a scan that earns the other half's trust.
| Candidate | Why it is not a vulnerability |
|---|---|
| Incorrect param access in addSingleValjquery.validate.unobtrusive.js:332 | The attribute variable is defaulted to attribute || "val" before the capturing closure is defined, so by the time options.params[attribute] runs it already holds the defaulted value that matches the registered key, so there is no mismatch. |
NullReference in FullNameModels/Student.cs:27 | Both LastName and FirstMidName carry [Required] attributes, which guarantee non-null in a validated entity. The concatenation is safe under normal flow. |
| Missing validation for negative/zero pageIndexPaginatedList.cs:23 | A value of 0 or negative causes EF Core to throw (HTTP 500), a robustness/availability issue, but no data exposed and no authorization bypassed, so not a security vulnerability. |
| “Signature-verification flaw” (CWE-347)InstructorCoursesPageModel.cs:8 | The condition references non-existent variables (g_accepted, g_used_value…) and cites cryptographic signature verification, neither of which appears in or relates to a method that simply marks which courses are assigned to an instructor. |
| Uninitialized collection may throwModels/Student.cs:31 | Uninitialized ICollection navigation properties are the standard EF Core pattern; EF initializes them when entities are loaded. A usage error, not a model defect. |
| Unconditional redirect on validation failureInstructors/Create.cshtml.cs:72 | The confusing control flow was traced: ModelState persists across the redirect in the catch path, so validation errors are not lost. Intended behavior for this POST pattern. |
| “Course-assignment data population security”InstructorCoursesPageModel.cs:12 | The claimed fields used_value and registered_value do not exist in AssignedCourseData; the class holds only CourseID, Title, and Assigned. |
The fourth and seventh rows are worth pausing on. A broad model, asked to find bugs, will occasionally propose a scary-sounding vulnerability built on variables and a CWE that aren't in the file. That is the “wall of confident noise” that makes most AI scanners exhausting. Here the verification pass read the actual method, saw the claim had no basis in the code, and filed it under “not a vulnerability,” where you can see it was considered and dismissed, rather than having it dumped on you or hidden from you.
Locations examined include the app root, Models, and the Pages for Courses, Departments, Instructors and Students, plus the bundled wwwroot/lib/jquery-validation-unobtrusive.
That's what a whole-app scan of a mid-size C# project costs and produces. A small package runs a few cents; you always see the estimate before you spend anything. See the measured cost table for more real runs, or the vulnerabilities we've reported in real OSS.
Point RedMirror at your code and get the same thing back: confirmed defects with a reproducible path, an honest “investigated and ruled out” list, and a bill metered to the token. Check the cost first with redmirror estimate: local, free, no tokens spent.