diff --git a/docs/security-assessments/M014-security-remediation-verification.md b/docs/security-assessments/M014-security-remediation-verification.md new file mode 100644 index 0000000..9b15e38 --- /dev/null +++ b/docs/security-assessments/M014-security-remediation-verification.md @@ -0,0 +1,123 @@ +# 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.