## https://sploitus.com/exploit?id=090923AF-6D8A-5AFA-9AFD-82B08520323E
# SafeVault โ Secure Coding, Authentication & Vulnerability Remediation
> **Capstone project โ Coursera "Security and Authentication" (Microsoft Full-Stack Developer, Course 8).**
> A small ASP.NET Core application that stores sensitive user records, built to demonstrate three security disciplines end to end: **secure input handling**, **authentication + role-based authorization**, and a **debug-and-remediate audit** of real SQL-injection and stored-XSS vulnerabilities.
**.NET 10 ยท xUnit ยท 89 passing tests ยท advisory-clean build**
---
## What this capstone demonstrates
This repository is the combined result of the three graded activities from the course. Each maps to a pillar of the application:
| Activity | Theme | Where it lives |
| --- | --- | --- |
| **1 โ Writing Secure Code** | Allow-list input validation, parameterized SQL, output encoding | `SafeVault.Core/Validation`, `SafeVault.Data/Sql` |
| **2 โ Authentication & Authorization** | bcrypt password hashing, RBAC, live access matrix | `SafeVault.Core/Security`, `SafeVault.Core/Authorization`, `SafeVault.Web/Authorization` |
| **3 โ Debugging & Resolving Security Issues** | Reproduce โ fix โ prove SQL injection & stored XSS | `SafeVault.Data/Search`, `SafeVault.Core/Notes`, `SafeVault.Web/wwwroot/audit.html` |
Full write-ups for each activity โ with attack payloads, screenshots, and design rationale โ are in [`docs/`](docs/).
---
## The three security pillars
### 1. Secure input (Activity 1)
- **`SafeVaultInputValidator`** โ *allow-list* validation, never blocklist. Usernames match `^[A-Za-z0-9._-]+$`; emails match a bounded standard pattern. Input is trimmed and control characters are stripped **before** validation. Unsafe values are rejected outright rather than "cleaned".
- **`SafeVaultSqlBuilder`** โ builds fixed SQL text and passes every user value as a **parameter** (`@Username`, `@Email`). User input never becomes part of the SQL grammar.
- **XSS** โ accepted values are returned through `WebUtility.HtmlEncode`, and the UI writes them with `textContent`, so returned data is never interpreted as HTML.
### 2. Authentication & authorization (Activity 2)
- **`BCryptPasswordHasher`** โ bcrypt with work factor 12; each hash carries its own random salt. Passwords are **only** stored and compared as hashes โ never plaintext.
- **`AuthenticationService`** โ one generic error for both "wrong password" and "unknown user", plus a **timing guard** (verifies against a throwaway hash for unknown usernames) so response time can't be used to enumerate accounts.
- **`RoleAuthorizer.Evaluate(role, level)`** โ a pure function that is the *single source of truth* for access. The web pipeline's `RoleAuthorizationHandler` calls it and only ever `Succeed`s on `Allow`, letting ASP.NET Core naturally return **401** (not signed in) vs **403** (signed in, wrong role).
- A **live access matrix** in `webform.html` probes every protected endpoint with the current session cookie and shows the real `200 / 401 / 403` results per demo account.
### 3. Debug & remediate (Activity 3)
Two features added *after* the auth work carried the vulnerabilities โ and each risky operation ships in **two implementations behind one interface** so the "before" is honest and executable:
| Concern | Vulnerable (audit subject, `[Obsolete]`) | Secure (shipped) |
| --- | --- | --- |
| User search | `ConcatenatingUserSearchQuery` โ `LIKE '%{term}%'` string-built | `ParameterizedUserSearchQuery` โ bound `@pattern`, LIKE-wildcards escaped |
| Note rendering | `RawNoteHtmlRenderer` โ content interpolated into HTML | `EncodedNoteHtmlRenderer` โ content routed through `EncodeForHtml` |
- The SQL injection is reproduced against a **real in-memory SQLite database** โ the tautology (`' OR '1'='1`) and a UNION payload that **exfiltrates password hashes** genuinely execute against the vulnerable query and return **0 rows** against the parameterized one.
- The vulnerable classes are `[Obsolete]` and are **never bound by the running app** โ they exist only so the tests and the audit console can run the real attack and show the contrast.
- A transitive NuGet advisory (**NU1903 / GHSA-2m69-gcr7-jv3q**) surfaced by `Microsoft.Data.Sqlite` was resolved by *upgrading* to a patched `SQLitePCLRaw.bundle_e_sqlite3 3.0.3` โ not by suppressing the warning. The build is advisory-clean.
---
## The Security Audit Console
`SafeVault.Web/wwwroot/audit.html` (the landing page) is a live audit tool that fires the real attacks and shows the **vulnerable build beside the shipping build**. The vulnerable HTML is displayed as *escaped text* โ never executed โ and each vulnerable search runs against a throwaway, freshly-seeded database. The comparison endpoints (`/audit/*`) are **Development-only**; the shipping surface never runs the vulnerable code.
The Activity 2 access-control console remains available at `/webform.html`.
---
## Architecture
```
SafeVault.slnx
โโ SafeVault.Core/ Domain: validation, bcrypt auth, RBAC, notes/search abstractions (no I/O)
โ โโ Validation/ SafeVaultInputValidator (allow-list + EncodeForHtml)
โ โโ Security/ AuthenticationService, BCryptPasswordHasher, IUserAccountRepository
โ โโ Authorization/ RoleAuthorizer (pure decision logic), AccessLevel, AccessDecision
โ โโ Notes/ VaultNote, VaultNoteValidator, Raw[Obsolete]/Encoded HTML renderers
โ โโ Search/ IUserSearchQuery, UserSearchMatch
โโ SafeVault.Data/ Adapters: parameterized SQL, SQLite, repositories, seed data
โ โโ Sql/ SafeVaultSqlBuilder, SqliteSafeVault (real in-memory DB)
โ โโ Search/ Concatenating[Obsolete] vs Parameterized query implementations
โ โโ Repositories/ In-memory + ADO.NET user/account repositories
โโ SafeVault.Web/ ASP.NET Core minimal API + cookie auth + audit console
โ โโ Authorization/ AccessLevelRequirement, RoleAuthorizationHandler
โ โโ wwwroot/ audit.html (default), webform.html (access matrix)
โโ SafeVault.Tests/ 89 xUnit tests
```
**Dependency direction** follows the Dependency Inversion Principle: `IUserAccountRepository` lives in `SafeVault.Core` (next to `AuthenticationService`), not in `SafeVault.Data`. Since `Data` already depends on `Core`, the abstraction stays in the domain and the service is unit-testable with a stub repository.
---
## Build, test, run
```bash
dotnet build SafeVault.slnx
dotnet test SafeVault.slnx # 89 tests
dotnet run --project SafeVault.Web # open http://localhost:5070/ โ Security Audit Console
```
`dotnet run` defaults to the Development environment, which the `/audit/*` comparison endpoints require.
### Demo accounts
Public sandbox credentials (shown on quick-login buttons โ **not production secrets**; passwords are only ever stored as bcrypt hashes):
| Username | Password | Role |
| --- | --- | --- |
| `sarah.admin` | `Sarah!Admin123` | Admin |
| `payroll_viewer` | `Payroll!View123` | User |
---
## Test coverage โ 89 passing
| Suite | Tests | Proves |
| --- | --- | --- |
| `UserSearchInjectionTests` | 9 | Tautology & UNION injection succeed against the vulnerable query, neutralised by the parameterized one โ on a real SQLite DB |
| `NoteXssRenderingTests` | 4 | Raw renderer emits live ``; encoded renderer neutralises script/author/attribute payloads |
| `VaultNoteValidatorTests` | 6 | Content kept verbatim (not blocklisted); required/length rules; control chars stripped |
| `AuditEndpointTests` | 8 | `/search` gates 401/403/200 and neutralises payloads; `/notes` encodes stored XSS over real HTTP |
| `ExistingSurfaceAuditTests` | 4 | The carried-over SQL builder and encoder were already secure |
| `BCryptPasswordHasherTests` | 10 | Hash โ plaintext, salt differs per call, malformed hash โ `false` not throw, >72 bytes rejected |
| `AuthenticationServiceTests` | 10 | Valid login succeeds; wrong password & unknown user fail with the *same* message; case-insensitive username |
| `AuthEndpointTests` | 13 | Login 200/401; `/admin` 401/403/200; logout ends session; response never leaks a hash |
| `RoleAuthorizerTests` | 7 | anonymous โ 401, non-admin โ 403, admin โ 200 for admin resources |
| `InMemoryUserAccountRepositoryTests` | 4 | Demo accounts store bcrypt hashes; case-insensitive lookup; round-trips through `SaveAsync` |
| `SafeVaultInputValidatorTests` | 9 | Allow-list rejects SQLi/XSS usernames & unsafe emails; encodes XSS payloads |
| `SafeVaultSqlBuilderTests` | 4 | Injection payloads never appear in command text, only as parameter values |
| `SubmitEndpointTests` | 3 | `/submit` returns 400 for attack payloads, 200 for valid submissions |
Live end-to-end verification (Playwright) confirmed the audit console fires both attacks and that the neutralised XSS renders with **zero executable `` nodes** in the browser DOM.
---
## Lab boundaries (out of scope)
- Persistence is **in-memory** (plus an in-memory SQLite DB for the search demo). The ADO.NET repositories + `database.sql` are the parameterized production data layer but are not wired to a live server in the lab.
- Registration (`/submit`) and login are separate user stores; uniting them is future work.
- Role assignment is static (seed accounts only); there is no user-management endpoint.
- The `http` profile runs under Development; a production host would add `UseHttpsRedirection()`, `CookieSecurePolicy.Always`, and a non-developer exception page.
---
## Documentation
- [Activity 1 โ Writing Secure Code](docs/Using%20Microsoft%20Copilot%20to%20Generate%20Secure%20Code.md)
- [Activity 2 โ Implementing Authentication and Authorization](docs/Using%20Microsoft%20Copilot%20to%20Implement%20Authentication%20and%20Authorization.md)
- [Activity 3 โ Debugging and Resolving Security Vulnerabilities](docs/Using%20Microsoft%20Copilot%20to%20Debug%20and%20Resolve%20Security%20Vulnerabilities.md)