Files
jobtrackingapp/docs/audits/security-threat-model.md
T

15 KiB

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

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, multitenant validation, 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.