15 KiB
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:
- anonymous public pages/public-CV slugs versus authenticated tenant data;
- browser cookies/CSRF token versus the API session store;
- user/administrator roles;
- tenant-owned EF queries versus background/admin cross-tenant work;
- untrusted job descriptions, email, files, URLs, and AI output versus parsers/renderers;
- API versus private sidecar and cloud AI recipients;
- local database versus separately stored files, keys, and backups;
- 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-99setsValidateIssuer=false, checks only hostname/suffix, omitstidbinding, usesoidwithout tenant namespace, and treatspreferred_usernamepresence as verified email.AuthController.cs:298-344then auto-links by email. Microsoft states that issuer validation mitigates cross-tenant forwarding, multitenant applications must tie issuer totid, andtidmust be part of the data key; it also statespreferred_usernameis mutable and must not drive authorization decisions: issuer validation, multitenant validation, claims reference. See JT-001.AuthController.cs:158-180authenticates the newly created unconfirmed user; reproduced with verification required.AuthController.cs:421-439changes email directly and leaves confirmation true; reproduced. See JT-007.AuthController.cs:347-355clears cookies only. A copied pre-logout session remained valid. Password change/reset paths atAuthController.cs:655-674,723-743also do not revoke other sessions. See JT-008.- Registration returns
User already existsatAuthController.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-424applies 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.