# JobTracker security threat model

Audit date: 2026-08-02

Scope: technical threat model and bounded local security audit. It is not penetration-test certification and did not touch production, real users, mailboxes, payment accounts, or paid AI providers.

## System and trust boundaries

```mermaid
flowchart LR
  V[Anonymous visitor] --> N[nginx / static UI]
  U[Authenticated browser] --> N
  N --> A[ASP.NET Core API]
  A --> D[(SQLite or MariaDB/MySQL)]
  A --> F[Local data volume: attachments, CVs, exports, keys, backups]
  A --> I[Private AI sidecar]
  I --> O[Ollama or Gemini/Groq]
  A --> G[Google/Gmail]
  A --> M[Microsoft identity/Graph]
  A --> E[IMAP/SMTP]
  A --> S[Stripe]
  A --> W[Job-advert/NAV web sources]
  S -->|signed webhook| A
  B[Hosted workers] --> D
  B --> F
  B --> I
```

Important boundaries:

1. anonymous public pages/public-CV slugs versus authenticated tenant data;
2. browser cookies/CSRF token versus the API session store;
3. user/administrator roles;
4. tenant-owned EF queries versus background/admin cross-tenant work;
5. untrusted job descriptions, email, files, URLs, and AI output versus parsers/renderers;
6. API versus private sidecar and cloud AI recipients;
7. local database versus separately stored files, keys, and backups;
8. external identity, mailbox, payment, and job-source providers.

## Sensitive assets

- Password hashes, session records, TOTP secrets/recovery codes, trusted devices, provider subjects, email verification/reset tokens.
- OAuth refresh/access tokens for Gmail/Microsoft and IMAP/SMTP credentials.
- Job searches, applications, notes, contacts, deadlines, salary expectations, correspondence and mailbox metadata.
- Career Profile, imported CV text/files, generated variants, public CV slugs, exports and attachments.
- AI prompts/results, usage records, job/profile text sent to a configured provider, and provider API keys.
- Stripe customer/subscription identifiers and webhook secret.
- JWT signing material, Data Protection key ring, SMTP credentials, database credentials, service token, deployment SSH key.
- Backups and logs containing identifiers or derived usage/activity data.

## Roles and privileged operations

| Role | Capabilities | Sensitive operations |
|---|---|---|
| Anonymous visitor | Landing/auth config, registration/reset requests, public CV/PDF by slug, health | Trigger password/verification mail; fetch explicitly public CV |
| Authenticated user | Own jobs/profile/CVs/attachments/correspondence/integrations/AI/settings | Upload/parse files, import URLs, connect mailbox, publish CV, generate/send drafts, change identity/security settings |
| Administrator | User/role/system/audit management | Create/delete users, reset password, SMTP/system settings, cross-user audit data |
| API/hosted service | Cross-component data and file access | Migrate/reconcile schema, backups, exports, rules/reminders/enrichment, provider calls |
| AI sidecar | Parse files and generate text; cloud-provider egress | Consume AI keys, process private prompts/files |
| External provider/webhook | Identity/mail/payment/job data | Assert identity, deliver mailbox/payment state, return untrusted content |

## Entry points

- Public/local auth, Google/Microsoft token exchange/linking, email verification/reset, TOTP challenge/recovery.
- Tenant REST APIs for jobs, companies, profiles, CVs, workspaces, messages, attachments, analytics, settings and billing.
- Public-CV HTML/PDF endpoints.
- Stripe webhook.
- Gmail/Graph OAuth callbacks, IMAP/SMTP settings and provider responses.
- URL job import/NAV discovery and translation.
- Multipart CV/image/document upload and attachment upload.
- AI prompt/context fields and AI sidecar HTTP endpoints.
- Admin APIs, startup configuration/environment, migration reconciler, Docker/CI/deploy scripts.

## Attacker capabilities considered

- Anonymous internet client controlling URL, headers, Host, body, rate and navigation.
- Authenticated malicious tenant user controlling own job text, uploads, AI context and import URLs.
- User attempting to alter/guess another tenant's integer IDs or public slugs.
- Attacker with a copied session cookie.
- Attacker with a valid token issued by another Microsoft tenant for the configured multitenant client.
- Malicious or compromised external provider returning hostile HTML/text/files/redirects.
- Supply-chain compromise of mutable action/image/script/package sources.
- Compromised container attempting lateral movement or secret/egress abuse.

## Highest-risk attack paths

| Path | Execution path and prerequisites | Existing mitigation | Assessment |
|---|---|---|---|
| Microsoft auto-link account takeover | Obtain a valid configured-audience token whose mutable email/preferred-username value matches a victim local account; weak issuer shape accepts it; exchange auto-links by email | Signature, audience, lifetime, Microsoft hostname shape; victim 2FA still challenges | High; code defect confirmed, external exploit not reproduced (JT-001) |
| Password-reset link poisoning | Public base URL blank; arbitrary Host reaches nginx (`server_name _`, forwarded `$host`); request victim reset; victim clicks attacker-host link and discloses token | Email-request rate limit; explicit public URL avoids path; 2FA limits password-only takeover | High; complete code/config path, no real email sent (JT-002) |
| Crafted document resource exhaustion | Authenticated user uploads renamed/malicious PDF/image/multipart; vulnerable pypdf/Pillow/parser path runs in sidecar without container resource limits | Authentication, private network/service token, extension/8 MB limits | High availability risk; known versions and direct parser path, exploit not attempted (JT-006) |
| Cross-user IDOR | Authenticated user guesses A's integer IDs | Explicit controller auth, owner predicates, deny-on-null global filters, owned-parent filters | No disclosure confirmed in meaningful two-user results; some paths blocked by unrelated 500s |
| AI prompt/private-data abuse | Untrusted advert/email/context influences prompt or provider receives broader profile | Prompt guardrail, React Markdown rendering, explicit user generation, attachment `UseForAi`, no automatic application | Residual privacy/injection risk; provider response not adversarially tested (JT-022/JT-025) |
| URL/import SSRF | User supplies private IP or DNS name that changes after validation | http/https-only, loopback/private/reserved address checks, redirects disabled | Direct SSRF mitigated; DNS re-resolution/rebinding remains unverified hardening risk (JT-024) |

## Authentication and session review

Strengths:

- ASP.NET Identity password policy, failed-login lockout, rate-limited auth-email routes, optional Turnstile.
- HttpOnly JWT session cookie plus readable double-submit CSRF cookie/header; unsafe API methods enforce CSRF.
- Server-tracked sessions with expiry/revocation endpoints, trusted devices, TOTP and one-time recovery-code design.
- Login uses generic 401 for missing/wrong/locked accounts; reset request is enumeration-resistant.

Findings:

- `MicrosoftTokenValidator.cs:52-99` sets `ValidateIssuer=false`, checks only hostname/suffix, omits `tid` binding, uses `oid` without tenant namespace, and treats `preferred_username` presence as verified email. `AuthController.cs:298-344` then auto-links by email. Microsoft states that issuer validation mitigates cross-tenant forwarding, multitenant applications must tie issuer to `tid`, and `tid` must be part of the data key; it also states `preferred_username` is mutable and must not drive authorization decisions: [issuer validation](https://learn.microsoft.com/en-us/dotnet/api/microsoft.identitymodel.tokens.tokenvalidationparameters.validateissuer), [multitenant validation](https://learn.microsoft.com/en-us/entra/identity-platform/access-tokens), [claims reference](https://learn.microsoft.com/en-us/entra/identity-platform/id-token-claims-reference). See JT-001.
- `AuthController.cs:158-180` authenticates the newly created unconfirmed user; reproduced with verification required. `AuthController.cs:421-439` changes email directly and leaves confirmation true; reproduced. See JT-007.
- `AuthController.cs:347-355` clears cookies only. A copied pre-logout session remained valid. Password change/reset paths at `AuthController.cs:655-674,723-743` also do not revoke other sessions. See JT-008.
- Registration returns `User already exists` at `AuthController.cs:155-156`, a low-severity enumeration difference from login/reset.
- External validator exception text is returned in 401 responses at `AuthController.cs:288-296`; restrict to a stable public error.
- Recovery-code/trusted-device updates lack explicit concurrency tokens/transactions; concurrent reuse is a lower-confidence risk requiring possession of a valid challenge and code.

## Authorization and tenant isolation

- Controllers are explicitly local-authorized or admin-role-authorized; the API does not rely only on the configurable fallback policy.
- `JobTrackerContext.cs:67-424` applies deny-on-null owner filters to tenant roots and most owned entities. Correspondence/events filter through the owned job.
- Direct User B tests denied User A jobs, companies, correspondence, attachment files, CV ID, workspace/checklist ID, settings and admin operations.
- UI hiding was not counted as authorization.
- The same filters accidentally hide all rows from background scopes with no HTTP user, causing JT-005. Cross-tenant worker/admin operations must use an explicit audited bypass and re-establish owner scope.
- Many tenant rows are not database-FK-linked to `AspNetUsers`; authorization therefore depends on correct application queries and deletion does not cascade (JT-009).

## Input, output, browser and network security

| Area | Evidence-based assessment |
|---|---|
| CSRF | Double-submit cookie/header enforced on unsafe authenticated requests; logout was exercised with CSRF. |
| CORS | Credentialed CORS is tied to configured origins; wildcard-with-credentials is rejected in startup validation. |
| XSS | React text/Markdown nodes avoid raw HTML; CV renderer HTML-encodes profile content and validates URLs. Same-origin `srcDoc` iframes are unsandboxed in two authenticated previews, but current renderer mitigates direct injection (JT-025). |
| SQL injection | EF parameterization dominates; custom startup DDL is static/provider-generated, not request input. No request-controlled raw SQL path found. |
| Command injection | No request-controlled process command found. PDF browser path comes from configuration, not a user field. |
| SSRF | Job import and IMAP reject literal/private/reserved hosts and job redirects are disabled. DNS rebinding between validation and connection is not eliminated (JT-024). |
| Path traversal | Upload names use `Path.GetFileName`, generated stored names and server-selected roots. Public/user IDs do not become arbitrary paths. |
| Open redirect | Application-return routes are generally internal; installed React Router version has moderate redirect advisories (JT-017). |
| File upload | Extension/size/storage-quota checks and generated filenames exist. Magic validation is uneven; sidecar reads whole body before size check and vulnerable parsers handle bytes (JT-006/JT-011). |
| Unsafe email rendering | Stored messages render as text/React content in reviewed UI. Provider content was not live-tested; no `dangerouslySetInnerHTML` path found. |
| Security headers | nginx adds baseline hardening headers and CSP-related configuration was inspected; effectiveness at a real public ingress was not measured. |
| Rate limiting | Auth email/login, public PDF and selected high-cost routes are limited. Public PDF limit is keyed by slug and can be consumed for all viewers of a known CV (JT-023). |

## External-service security

- Google tokens require signature/audience/lifetime and verified-email semantics; explicit link endpoints exist.
- Microsoft identity binding is JT-001.
- Stripe webhook validates signature and then refreshes current subscription state, reducing out-of-order webhook risk.
- AI sidecar is on a private two-member network, not host-published, and production Compose requires a shared token. It still runs as root, has cloud egress/keys, and lacks CPU/memory/PID constraints.
- Gmail/Graph/IMAP connections are owner-scoped. Provider tokens are protected with ASP.NET Data Protection; restoring/moving them requires the original key ring.
- SMTP settings are admin-only and password values are not returned by status APIs.

## Secret exposure review

No secret value is reproduced here.

| Secret type | Location | Exposure risk | Remediation |
|---|---|---|---|
| Expired local JWT artifact | `docs/_archive/artifacts/s06-acceptance/.dev-auth-token.txt:1` | Tracked/historical credential pattern; token is expired and lacks current required session ID | Remove from reachable source history according to repository policy; use redacted fixtures |
| ASP.NET Data Protection keys | Historical `JobTrackerApi/keys/...` and `keys/...` paths | Historical key material may decrypt data protected under the matching ring | Rotation is documented as completed but not independently verified; preserve incident evidence and rotate affected protected data/tokens as needed |
| Production/runtime secrets | Compose `.env` inputs and CI secret store | Values not present in audit output; mutable CI/deploy dependencies can access them | Pin provenance and least-privilege runners/actions; keep values out of repository/logs |

## AI-specific abuse cases

- Prompt injection from job adverts/email/additional context: guardrail says preserve facts and return suggestion only; cannot guarantee instruction hierarchy against a model. Treat output as untrusted and retain human approval.
- Excessive permissions: sidecar can spend cloud key and access provider egress; private network/token help, but resource/egress controls are absent.
- Private-data disclosure: most AI modules send the selected job plus full master-profile text; attachment inclusion is separately controlled. No per-user global AI opt-out or clear recipient/provider data summary is implemented (JT-022).
- Automatic enrichment would send job descriptions without a per-user opt-in if its worker were repaired; currently the worker cannot see tenant rows. Remediation must not silently activate that disclosure.
- AI output is stored append-only with owner ID and is not applied automatically; this is a strong trust-loop control.

## Two-user conclusion

No cross-user disclosure was confirmed. Protection exists at API, service-query, and EF-filter levels for the meaningful results. CV/AI/timeline/interview paths that returned 500 remain unverified rather than passed. Full matrix: `evidence/two-user-isolation.md`.

## Residual and blocked security checks

- No real multitenant Microsoft token, poisoned reset email, malicious parser exploit, DNS-rebinding host, provider webhook, mailbox, or cloud AI request was executed.
- No DAST, aggressive fuzzing, brute-force, load test, container CVE scan, or production ingress/header test.
- Browser manual XSS/keyboard/network observation blocked by missing browser client.
- MariaDB-specific authorization/migration behaviour not executed.

These limitations are reflected in finding confidence and classification; they are not represented as passes.
