179 lines
15 KiB
Markdown
179 lines
15 KiB
Markdown
# 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.
|