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