Record security remediation verification

This commit is contained in:
2026-04-11 16:31:05 +02:00
parent 09e96ce381
commit ac217dab53
@@ -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.