# M014 Security Remediation Verification This report retests the two confirmed findings from `M013` after code fixes landed in `M014`. Related assessment: - `docs/security-assessments/M013-adversarial-security-assessment.md` ## Fixed Findings ### 1. Job import preview SSRF via hostname-based loopback/private-address bypass - **Original issue:** `POST /api/jobimport/preview` accepted hostnames that resolved to loopback/private addresses and fetched internal targets. - **Fix status:** **Fixed** - **Primary code changes:** - `JobTrackerApi/Services/JobImport/JobImportService.cs` - `JobTrackerApi/Services/JobImport/IHostAddressResolver.cs` - `JobTrackerApi/Program.cs` #### What changed - URL validation now resolves hostnames before allowing outbound fetches. - Validation rejects loopback, private, link-local, and other internal destinations for both literal IPs and resolved hostnames. - Automatic redirects are disabled on the `jobimport` HTTP client. #### Retest inputs and outcomes | Exploit input | Expected after fix | Observed | | --- | --- | --- | | `http://127.0.0.1.nip.io:5202/api/auth/config` | reject | `400` with parser `none` and local/private-network rejection | | `http://localhost.localdomain:5202/api/auth/config` | reject | `400` with parser `none` and local/private-network rejection | | `http://[::1]:5202/api/auth/config` | reject | `400` with local/private-network rejection | | `http://2130706433:5202/api/auth/config` | reject | `400` with local/private-network rejection | | `https://example.com` | allow public fetch path | request reached parser path and failed only with `No JobPosting schema found.` | #### Verdict **Pass.** The original SSRF exploit shapes are now blocked and a normal external URL still follows the intended public-host path. --- ### 2. Subjectless signed local JWTs authenticate successfully and can disable owner scoping - **Original issue:** a validly signed local JWT without `nameidentifier` / `sub` was accepted, and owner filters were written to allow all rows when `CurrentUserId` was null. - **Fix status:** **Fixed** - **Primary code changes:** - `JobTrackerApi/Services/LocalAuthIdentity.cs` - `JobTrackerApi/Services/CurrentUserService.cs` - `JobTrackerApi/Program.cs` - `Data/JobTrackerContext.cs` #### What changed - Local JWT bearer validation now rejects tokens without a concrete subject/nameidentifier. - Current-user resolution uses the same required-identity rule. - Owner query filters now deny on null current user instead of allowing all rows. #### Retest input and outcomes Malformed token shape reused from `M013`: - valid local signature - valid issuer/audience/lifetime - **missing** `ClaimTypes.NameIdentifier` / `sub` Observed after fix: | Request | Expected after fix | Observed | | --- | --- | --- | | `GET /api/auth/me` with subjectless signed local JWT | reject | `401` | | `GET /api/companies` with subjectless signed local JWT | reject | `401` | | `GET /api/rules` with subjectless signed local JWT | reject | `401` | #### Automated proof Focused tests now cover: - required-subject local identity behavior - fail-closed owner query filters when current user is missing #### Verdict **Pass.** The malformed token no longer authenticates, and the owner filters fail closed behind the auth boundary. ## Focused Test Evidence ### SSRF-focused tests ```bash dotnet test JobTrackerApi.Tests/JobTrackerApi.Tests.csproj --filter JobImportServiceTests ``` Observed: - passed - covers loopback-resolving hostname rejection - covers private-address hostname rejection - covers normal public-host path ### Local-auth / owner-scope tests ```bash dotnet test JobTrackerApi.Tests/JobTrackerApi.Tests.csproj --filter "LocalAuthIdentityTests|AuthAndSystemControllerTests|OwnershipGuardTests" ``` Observed: - passed - covers required-subject behavior and fail-closed owner filter semantics ## Remaining Boundaries - The local runtime still uses a partial SQLite schema for some domain tables, so broader cross-user raw-id authorization retests remain best treated as a separate follow-up pass in a fuller environment. - Those unresolved candidates were not needed to close the two confirmed `M013` findings, because both confirmed exploit shapes were retested directly and now fail. ## Final Verdict `M014` closes the two confirmed `M013` vulnerabilities: 1. hostname-based authenticated SSRF in job import preview — **fixed** 2. subjectless local JWT authentication / owner-scope fail-open behavior — **fixed** Both fixes were verified with focused automated tests and hostile runtime retests using the original exploit shapes.