# M013 Adversarial Security Assessment

## Scope

Tested as requested:

- Input validation issues
- Authentication flaws
- Authorization issues
- API security
- File upload vulnerabilities
- Data exposure

Assessment style: hostile, exploit-oriented, evidence-first.

## Confirmed Findings

### 1. Authenticated SSRF via hostname-based loopback bypass in job import preview

- **Category:** API security / input validation
- **Component:** `JobTrackerApi/Services/JobImport/JobImportService.cs`
- **Endpoint:** `POST /api/jobimport/preview`
- **Risk:** **High**

#### Vulnerability

`JobImportService.TryValidateUrl(...)` blocks literal loopback and private IPs, but it does **not** resolve hostnames before allowing the request. That means a hostname that resolves to a loopback/private address can bypass the protection.

The validator rejects:

- `http://127.0.0.1:5202/...`
- `http://[::1]:5202/...`
- `http://2130706433:5202/...`

But it accepted hostnames resolving to loopback, including:

- `http://127.0.0.1.nip.io:5202/api/auth/config`
- `http://localhost.localdomain:5202/api/auth/config`

#### Example exploit input

```http
POST /api/jobimport/preview
Authorization: Bearer <valid local token>
Content-Type: application/json

{
  "url": "http://127.0.0.1.nip.io:5202/api/auth/config"
}
```

Observed result:

- request was **not** rejected as local/private
- server fetched the internal endpoint
- response progressed to parser failure (`No JobPosting schema found`), which is enough to prove the internal fetch happened

#### Why this matters

An authenticated attacker can use the server as an HTTP client against internal-only services or private network resources reachable from the API host. Depending on deployment, this can expose:

- internal admin/debug endpoints
- cloud metadata services
- internal service meshes
- localhost-only ports
- network topology and response behavior

#### Clear fix

- Resolve DNS before allowing the request.
- Reject any hostname whose resolved addresses are loopback, link-local, RFC1918 private, or otherwise internal.
- Re-resolve after redirects, or disable redirects entirely.
- Consider an allowlist of supported job domains instead of general outbound fetching.
- Log and rate-limit preview fetch attempts.

---

### 2. Subjectless signed local JWTs authenticate successfully and can disable owner scoping

- **Category:** Authentication flaws / authorization issues
- **Components:**
  - `JobTrackerApi/Program.cs`
  - `Data/JobTrackerContext.cs`
  - several owner-scoped controllers relying on EF query filters
- **Risk:** **High**

#### Vulnerability

Local JWT validation accepts a correctly signed token **without** a required subject / nameidentifier claim.

Runtime proof:

- a signed local JWT with **no** `ClaimTypes.NameIdentifier` / `sub`
- but with valid issuer/audience/signature
- was accepted by the API
- `GET /api/auth/me` returned `200`

Observed response shape:

- provider: `external`
- id: `null`
- email echoed from token

At the same time, owner scoping in `Data/JobTrackerContext.cs` is defined like this:

```csharp
.HasQueryFilter(x => CurrentUserId == null || x.OwnerUserId == CurrentUserId)
```

If `CurrentUserId` is null, the filter collapses to **allow all rows** for owner-scoped entities.

That is a dangerous composition:

1. token is authenticated
2. current user id is null
3. owner filters disable themselves
4. endpoints that rely on implicit owner filtering become potentially cross-tenant

#### Example exploit input

A signed HS256 JWT using the app signing key, but **omitting** the nameidentifier claim.

Payload example:

```json
{
  "iss": "JobTrackerApi",
  "aud": "job-tracker-ui",
  "nbf": 1775910345,
  "exp": 1775913945,
  "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress": "ghost@example.com",
  "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name": "ghost@example.com"
}
```

Observed runtime request:

```http
GET /api/auth/me
Authorization: Bearer <signed token without subject>
```

Observed result:

- `200 OK`
- request treated as authenticated even though no user identity key existed for owner scoping

#### Why this matters

If an attacker can forge or obtain a valid local signing key, this flaw is not just “login bypass” — it can become a **tenant-boundary bypass** because owner filters stop applying.

This is especially serious in environments where:

- the dev signing key is reused
- a staging or preview environment leaks the local JWT key
- operational mistakes deploy development auth settings

#### Clear fix

- Require a non-empty subject / nameidentifier claim during local JWT validation.
- Reject authenticated requests whose token does not map to a concrete application user identity.
- Change query filters so `CurrentUserId == null` means **deny**, not allow-all, for owner-scoped entities.
- Avoid relying on implicit global filters alone for sensitive raw-id endpoints; add explicit owner predicates in controller queries.

Example direction:

- validate a required identity claim in the JWT bearer events pipeline
- make owner filter logic equivalent to `CurrentUserId != null && x.OwnerUserId == CurrentUserId`

---

## High-Risk Candidates Not Fully Confirmed In This Runtime

These are not counted as confirmed findings yet, but they remain serious candidates.

### A. Raw-id child endpoints rely on implicit owner scoping

- **Category:** Authorization issues
- **Components:**
  - `JobTrackerApi/Controllers/AttachmentsController.cs`
  - `JobTrackerApi/Controllers/CorrespondenceController.cs`
  - selected job-linked endpoints
- **Risk:** **Medium to High**

Patterns observed:

- existence checks like `AnyAsync(j => j.Id == jobId)`
- later child fetches through parent relationships
- reliance on EF global filters instead of explicit per-request owner predicates

If the owner filter is ever bypassed, weakened, or accidentally ignored, these become cross-user read/write primitives.

This runtime could not prove the full cross-user exploit path because the active SQLite file is missing core domain tables (`Companies`, `JobApplications`, `RuleSettings`) and those requests fail before authorization behavior can be fully exercised.

#### Suggested fix

Add explicit owner predicates in the endpoint queries themselves instead of trusting global filters as the only boundary.

---

## Tested Surfaces With No Confirmed Finding In This Pass

### Anonymous API reachability

Observed anonymous results in this runtime:

- `GET /api/export/jobs` → `401`
- `POST /api/backup/encrypted` → `401`
- `POST /api/jobimport/preview` → `401`
- `POST /api/client-errors` → `401`

This is better than the earlier pre-hardening posture.

### Gmail OAuth callback

- `GET /api/gmail/oauth/callback?code=fake&state=fake` returned a generic failure page
- no secret data exposure observed in this pass

### File upload path traversal via visible filename

Code review on:

- `AuthController.UploadAvatar`
- `AttachmentsController.Upload`
- `AttachmentsController.Rename`
- `ProfileCvController.Upload`

Current handling uses `Path.GetFileName(...)` and generated storage names, which is a reasonable defense against straightforward path traversal through user-supplied filenames.

No confirmed traversal exploit in this pass.

## Recommended Remediation Order

1. **Fix authenticated SSRF in job import preview**
   - hostname resolution checks
   - no internal/private destinations
   - preferably domain allowlist

2. **Fix JWT subjectless-auth acceptance and owner-filter allow-all behavior**
   - require subject/nameidentifier
   - reject tokens that do not map to a real app identity
   - change owner filters to deny on null current user

3. **Harden raw-id owner-sensitive endpoints with explicit owner predicates**
   - attachments
   - correspondence
   - job-linked child endpoints

4. **Run a second exploit pass after the fixes**
   - repeat cross-user probes
   - retest SSRF bypasses
   - fuzz upload/parser surfaces further

## Evidence Summary

### Runtime probes performed

- anonymous reachability checks against auth/config, csrf, auth/me, client-errors, jobimport preview, export, backup, and Gmail callback
- authenticated SSRF probes against job import preview using loopback-resolving hostnames
- authenticated malformed-token probe using a signed local JWT without subject/nameidentifier

### Key observed outputs

- `/api/jobimport/preview` accepted `http://127.0.0.1.nip.io:5202/api/auth/config`
- `/api/auth/me` returned `200` for a signed local JWT without subject/nameidentifier
- owner filters in `JobTrackerContext` explicitly allow all rows when `CurrentUserId == null`

## Honest Boundaries

- Some cross-user raw-id probes were limited by the local runtime using an incomplete SQLite schema.
- Those areas are reported as **high-risk candidates**, not falsely upgraded to confirmed findings.
- The two confirmed findings above are supported by direct runtime evidence plus code-path verification.
