# 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.
