Files
jobtrackingapp/docs/audits/security-threat-model.md
T
cesnimda 8ef8b098c8
CI and Deploy / test (pull_request) Failing after 5m23s
CI and Deploy / deploy (pull_request) Has been skipped
fix(security): harden public CV edges
2026-08-15 20:53:16 +02:00

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 Historical docs/_archive/artifacts/s06-acceptance/.dev-auth-token.txt Removed from the tracked tree on 2026-08-15 and explicitly ignored; older Git history still contains the expired pattern Keep generated acceptance tokens untracked. Any history rewrite requires separate repository-owner coordination.
ASP.NET Data Protection keys Historical JobTrackerApi/keys/... and keys/... paths Historical key material may decrypt data protected under the matching ring The operator reports the production ring was rotated. Preserve incident evidence; independent production verification remains part of release operations.
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.