Implement B01-B06 with regression coverage and retained validation evidence. Preserve the production ingress preflight; F04 runtime verification remains open.
27 KiB
Threat model, abuse, data lifecycle and invariants
This model reflects inspected source at the audit baseline. It does not certify deployed network boundaries, encryption or legal compliance. F identifiers link to findings.
Trust boundaries
flowchart TB
U[Untrusted browser / imported job text / document] --> P[Operator TLS proxy]
U -. published host port 3000 .-> N[nginx frontend]
P --> N
N --> A[ASP.NET Core API: session, CSRF, owner, entitlement]
A --> D[(SQLite or external MariaDB)]
A --> F[Private files, exports, package copies]
A --> K[Data Protection keys / provider secrets]
A --> Q[(Durable operations / delivery attempts)]
Q --> W[In-process workers with explicit owner scope]
W --> A
A --> S[Token-protected Python AI sidecar]
S --> X[Bounded parser child process]
S --> L[Local Ollama / summarization model]
S -. consent + configured routing .-> E[External AI provider]
A --> M[SMTP / Gmail / Graph / IMAP]
A --> B[Stripe signed webhook + API]
A --> J[Untrusted public job URL / NAV / translation]
D --> R[Backup recovery set]
F --> R
K --> R
R --> O[Off-host destination and custody]
A --> G[Operational logs]
CI[CI runner + deployment SSH] --> A
CI --> R
The backend is highly privileged within this design: it can read document contents, decrypt connected-account credentials and modify all owners' database rows. A parser child should not inherit those capabilities. Public CV rendering deliberately crosses the private-to-public boundary, and AI/provider calls cross organizational/privacy boundaries. Browser flags and localStorage do not grant authorization. The dashed ingress route is the F04 topology risk.
Assets and intended access
| Asset / significant fields | Who should access | Who must not | Exposure route and current control | Missing control / action |
|---|---|---|---|---|
| Account identity, email, pending email, phone, profile name | Owner; minimal account administrator | Other users, anonymous clients | Local identity/session, owner filtering; admin user DTO | Security audit/recent auth for privileged changes; no unnecessary private profile export to support |
| PasswordHash, SecurityStamp, session sid, recovery hashes | Auth subsystem only; limited security operator | Browser exports, ordinary admin UI, AI | Identity hashing, live session validation, token/2FA tests | Verify operational key custody; security-event trail |
| TOTP encrypted seed, OAuth refresh/access tokens, IMAP password | Corresponding auth/provider service | Clients, unrelated workers, parser child | Data Protection and purpose-specific credential handling | Keys and encrypted values share host boundary; test rotation/restore |
| Career profile, CV text/structure, history, drafts | Owner; explicitly enabled AI processing | Other users; support by default | Owner-scoped services, AI consent gates | Conflict preconditions, retention, minimal copies |
| CV public slug and published fields | Anonymous holder while sharing enabled | Visitors after revocation; users guessing private IDs | Unpredictable slug; private by default; slug rotates when republished; noindex HTML | Explicit field review/expiry policy; PDF noindex/no-store; do not equate noindex with access control |
| Jobs, notes, cover letters, correspondence, interviews, reminders | Owner; explicit employer send recipient | Other users; unrelated support staff | Parent-owner filters and ownership tests | Global admin event feed exposes content; duplicate/concurrent writes |
| Uploaded bytes, rendered PDF, submitted package copies | Owner or deliberate public/share recipient | Other owners, arbitrary path requester | Managed paths, staging, quarantine, MIME checks, owner authorization | Global quotas, output cache policy, bounded renderer, isolated malware decision |
| Subscription/customer IDs, plan and usage | Owner sees status; billing worker/admin sees minimum | Client cannot set entitlement, unrelated users | Stripe signature and current subscription lookup; role-derived policy | Checkout dedup, customer reconciliation, deletion cancellation |
| API/JWT/SMTP/Stripe/AI/CI secrets, Data Protection keys | Narrow service/operator | Frontend, logs, untrusted PRs, parser | Config/env, some child-env stripping, tracked-secret scan | Encrypted custody, rotation drills, artifact/runner policy; runtime proof pending |
| Backup/tombstone sets | Recovery operator | Application users, unrelated containers | Separate tombstone volume, restricted backup modes, retention script | Consistency/restore proof and off-host encryption/custody |
| Logs/diagnostic IDs | Support uses safe metadata; security team limited event access | Public users and unrestricted private-content search | Request trace IDs, structured production console | PII/URL filtering, immutable security trail, retention and access ownership |
Attacker analysis
| Actor | Likely route / intent | Most useful controls |
|---|---|---|
| Anonymous internet attacker / bot | Credential stuffing, signup/reset floods, public PDF overload, Host/header spoofing | Canonical origin, trusted ingress, layered auth/public admission and delivery quotas |
| Malicious registered user / abusive paying user | ID substitution, storage/database growth, PDF/AI exhaustion, repeated imports | Owner filters at every verb, aggregate limits, atomic quotas, global cost/concurrency bounds |
| Compromised user account | Export private career/mail data; send unwanted messages; link provider | Recent auth for export/security actions, revoke all sessions, mail confirmation, safe audit and anomaly counts |
| Malicious uploaded document | Parser crash, resource exhaustion, embedded content or renderer escape | File type and decompression bounds, child isolation, network denial, renderer encoding and separate privilege |
| Malicious job advertisement | SSRF through source URL; prompt injection; XSS; spreadsheet formula export | Connected-IP validation, treat text as data, encode outputs, fact review, spreadsheet-safe export |
| Compromised dependency / CI runner | Read secrets, alter delivered frontend/backend, exfiltrate personal data | Locked/pinned builds, protected runner/workflow, least SSH privilege, artifact provenance, detection and rotation |
| Malicious/compromised administrator | Bulk private event access, roles/resets, host/database theft | Minimal admin DTOs, recent authentication, auditable privileged actions, separate host custody |
STRIDE is used to ensure coverage, not generate duplicate findings: spoofing (session/ingress), tampering (stale edits/forged identifiers), repudiation (missing security audit), disclosure (admin/log/cache/provider boundaries), denial of service (PDF/storage/import), elevation (roles/paid state). Highest combined risk comes from the backend host: compromise reaches database, files and decrypting keys together.
Abuse matrix
Likelihood/impact are qualitative estimates for an openly registered single-host deployment, not observed incident rates. H/M/L = high/medium/low. R/Q/M columns state recommended rate limit, quota, monitoring respectively; “yes” does not mean currently implemented.
| Abuse case | L/I | Current defence | Recommendation | R / Q / M and monitoring |
|---|---|---|---|---|
| Automated registration | H/M | Registration switch, auth-login policy, configurable Turnstile | Per-IP + normalized-account attempt control; staged registration | yes/yes/yes; accepted/rejected signup bursts |
| CAPTCHA bypass/unavailability | M/M | Server verification when configured | Fail closed; accessible retry/recovery, independent rate limiting | yes/no/yes; challenge error rate |
| Disposable email / multi-account / trial cycling | M/M | Verified-email option; no reputation policy seen | Require verified email before cost; measure before buying detection | yes/yes/yes; account/cost anomalies, no intrusive fingerprinting |
| Login enumeration/credential stuffing | H/H | Identity/session tests, auth limit | Uniform errors/timing review; account + IP throttles, accessible 2FA | yes/no/yes; failure/success ratio |
| Reset / verification flooding | H/M | 5/15min by IP | Recipient/account cooldown, send queue, no existence leak | yes/yes/yes; sends per recipient/provider |
| Reminder flooding | M/M | Owner preference, daily sent date, worker switches | Durable daily dedup; per-account/channel send ceiling | yes/yes/yes; duplicate and suppression counts |
| Employer send abuse | M/H | Explicit send, provider owner guard, durable send attempts, email-send policy | Fix policy ordering; retain unknown delivery state | yes/yes/yes; provider errors/complaints |
| AI calls / credit drain | H/H | Pro gate, 250 calls/1M estimated tokens/month; queue caps | Bound input/output and synchronous concurrency; enforce reservations across replicas | yes/yes/yes; costs/reservations/retries |
| Expensive-model selection / oversized output | M/H | Config routing, allowlisted external tasks, prompt bounds | Pin evaluated model per workload; no arbitrary client model/provider | yes/yes/yes; task-model costs/latency |
| Repeat document/PDF generation | H/H | Per-process timeout; public IP+slug limit | Shared concurrency and per-account/global budget F03 | yes/yes/yes; processes, queue, output bytes |
| Storage exhaustion / malicious hosting | H/H | 10MB attachment, 5MB CV; partial attachment quota | Atomic aggregate quota, bounded file types, safe download, quarantine decision | yes/yes/yes; bytes/category, orphan scan |
| API scraping / analytics scraping | M/M | Auth/owner isolation; some pagination | Category sustained limits and query bounds | yes/yes/yes; scanned rows, response bytes |
| Job-import URL fetching | H/H | HTTP(S), DNS private-IP rejection, no redirects, 4MB stream bound | Pin validated connect destination, per-owner/import/global concurrency | yes/yes/yes; denies, duration, destinations by class |
| Search/regex expensive input | M/H | Owner filters; fixed regex patterns; some bounded import input | Max query length, timeout/non-backtracking where needed, query-plan fixtures | yes/yes/yes; p95/p99, rows scanned |
| Notification / queue flooding | M/M | AI queue 10/account and 100/global by default | Cover all queues and persistent record retention; dedup notifications | yes/yes/yes; pending age, rejected admissions |
| Webhook replay / out-of-order events | H/H | Signed events and current subscription re-fetch | Durable inbox + customer reconciliation/transaction F02 | yes/yes/yes; duplicates, lag, divergence |
| Repeated checkout / payment actions | M/H | Existing-Pro check | Purchase-intent idempotency, active-session reuse | yes/yes/yes; active subscriptions/account |
| Database-growth attack / 100k jobs | H/H | Paged list; no record quota found | Row/byte admission and bounded board/export/history | yes/yes/yes; owner/table growth |
| CSV formula injection | M/H | CSV structural quoting | Spreadsheet-safe text escaping F05 | no/no/yes; regression gate, no content telemetry |
| Log/error endpoint flooding | M/M | 32KB body/field truncation | Rate limit, sampled safe codes, bound log volume | yes/yes/yes; ingestion/dropped events |
| Session/export abuse after compromise | M/H | Recent sign-in for account ZIP; session revocation | Atomic account limit and safe export audit, notify where appropriate | yes/yes/yes; export counts/bytes |
Rate strategy
Current fixed windows: auth-login 10/5min/IP (configurable); auth-email 5/15min/IP; 2FA challenge 5/5min/IP; employer-send 10/min intended owner; account export/deletion 2/hour intended owner; public PDF 3/min/IP+slug. The two intended owner policies fall back to IP due to middleware order (observed F06). There is no universal request limiter or distributed limiter. AI admission and monthly accounting are distinct controls.
Proposed starting limits, tune with staging measurements: auth keep tight IP bursts plus per-account cooldowns without permanent victim lockout; normal API 120/min/owner with burst 30; search/analytics 30/min/owner and input bounds; import 10/min/owner with 2 in flight; PDF 5/min/owner, 1 active/owner, 2 active globally and a small bounded queue; AI 2 active/owner plus existing durable capacity and monthly limits; public PDF 3/min/IP plus a shared renderer cap. These are initial policy values, not capacity claims. Add a more permissive edge per-IP flood limit to protect application parsing, and distinguish webhook signature admission from browser API traffic.
Return 429 with retry timing, accessible explanation and preserved inputs. Global PDF/AI budgets matter even with many accounts/IPs. Keep process-local controls only while there is one backend; use database reservations before replicas. Adopt distributed rate storage only when multiple processes actually enforce the same budget.
Data classification
These engineering categories are not determinations of statutory “special category” data.
| Class | Major fields/examples | Handling |
|---|---|---|
| Public by explicit decision | Published CV fields/slug, marketing copy, template metadata | Review before publish; revoke links; retain noindex for token links; still treat public content as untrusted input |
| Internal operational | Build version, task type, duration, safe error code, queue state | No arbitrary user text/query strings; bounded logs; support can diagnose by reference |
| Personal | Name, email, phone, company/contact details, salary preferences, provider subjects, IP/session metadata | Owner access; minimal support visibility; purpose-based retention; protected backup and export |
| Sensitive career/private content | CV/profile/history, notes, applications, job-search activity, mail bodies, letters, interview notes, uploaded files and AI prompts/responses | No analytics/log capture; explicit third-party use; no-store; restricted at-rest storage/backup; deletion through every copy |
| Authentication/security | Password hashes, TOTP/recovery material, sessions, CSRF tokens, OAuth access/refresh tokens, IMAP credential | Never export to user-data ZIP or send to AI; hash where verification-only, protect reversible secrets; recent auth and audited rotation |
| Secrets | JWT signing key, Data Protection key ring, provider/Stripe/SMTP/CI credentials | No logs/source/frontend; separate restricted recovery custody; rotate with tested dependency plan |
At-rest encryption of database/files/host/backup is not demonstrated. Data Protection protects selected credential values but cannot defend against a host attacker who also obtains its keys. Encrypt host volumes and off-host backups with recoverable custody; do not encrypt every searchable career field in application code without a specific threat and query plan. Browser-to-proxy TLS is operational; proxy-to-API is HTTP on Docker networking; external-provider TLS is used by service clients where configured. MariaDB transport encryption Needs runtime verification.
Data lifecycle and copy map
All persistent DB/file rows may enter recovery backups; exclusions must be explicit. “Export” below means account ZIP unless noted. Retention details follow in the next table.
| Data group | Enters / validation | Primary storage | Copies/cache/browser | Logs / third parties | Export / deletion |
|---|---|---|---|---|---|
| Account identity/settings | Auth/admin/profile DTO; Identity email/password checks; verified email-change workflow | AspNetUsers, roles, claims/logins | /auth/me; owner key and UI preferences in localStorage; cookies |
Some email logs; identity/OAuth and optional mail providers | Selected safe account fields; staged account purge and session invalidation |
| Security material | Password/2FA/OAuth/IMAP flows, token validation | Identity hashes, encrypted connection credentials, sessions/trusted devices; Data Protection key files | HttpOnly auth cookie; readable CSRF cookie; service memory | Never intentionally AI/export; log exception/URL canaries needed | Safe session metadata only; revoke/remove on security changes/deletion; key ring has independent retention |
| Career Profile / CV information | Manual structured DTO validator or extraction review | CareerProfiles + relational children + version snapshots | Profile JSON and raw text also on ApplicationUser; editor state; sidecar cache if used | AI only through policy; no purpose for operational content logs | JSON profile and versions; account deletion removes rows/files; conflict/retention gaps |
| Uploaded CV / extraction | Multipart 5MB, file/parser/ZIP bounds, bounded child | CvUploadArtifacts/CvExtractionRuns + artifact files | Raw file, extracted text, structured result, current artifact pointer | Local parser/AI; external processing only policy-approved; parser errors sanitized | Original/current artifacts plus safe metadata; completed extraction history pruned, current artifact preserved |
| CV variants / generated PDFs | Settings normalization and theme/owner rules | CvVariants/CvVariantVersions; owner/date PDF files | Settings history; render against current profile; in-memory HTML/bytes; iframe | Public share if enabled; browser attempted Google Fonts in audit | JSON/files; immediate variant delete/history removal; rendered outputs expire lazily on later exports |
| Jobs / descriptions / companies | Manual/import DTOs; owner check; URL DNS/type/body guard | Jobs + JobApplications duplicated compatibility fields; Companies | Search/list/board memory; browser filters; derived AI summaries | Job sites/NAV/translation; optional AI; admin event feed exposes title/company/notes | JSON/CSV/ZIP; soft delete/trash differs from purge; F05 formula risk |
| Letters / notes / AI responses | Human edit/review and structured AI parsing | JobApplication drafts, CoverLetterVersions, AiWorkspaceNotes/Interactions | Versions, submitted packages, prompt context, local component state | AI provider as allowed; no automatic employer send authorized by model output | Safe readable data export; delete all related copies; persistent version retention undecided |
| Correspondence / email drafts | Gmail/Graph/IMAP sync, owner-bound review/import/send | Correspondence, connection/review rows, email drafts/attempts | Provider mailbox remains a separate copy; extracted AI context | Mail providers; SMTP logging can expose recipient/subject | Application-owned copies exported/deleted; deleting Jobjakt does not delete remote mailbox |
| Interviews / reminders / notifications | Manual/scheduler/calendar input and owner rules | Interview prep/events, job dates, notification/settings rows | Calendar/provider copy, browser pending state, reminder email | Email/calendar provider; delivery F07 and timed-event conversion F21 | JSON/ZIP; owner purge; already sent email/calendar copies need provider-specific policy |
| Submitted packages | Explicit capture, owner render, managed attachment paths | Immutable package fields + copied file bytes and hashes | Profile/settings/job description/notes/rendered HTML duplicated intentionally for provenance | Should stay private except explicit download/send | Historical JSON/files; deletion follows account; live CV edits do not rewrite captured package |
| AI operations / usage | Pro, privacy and capacity reservation | UserOperations, AiUsageRecords and referenced result rows | In-memory execution state, sidecar one-hour cache; counts in UI | Provider/model/task/duration metadata; prompts may contain career/mail data | Safe operation/result data; deletion cancels/purges and clears sidecar cache; provider retention separately governed |
| Billing | Signed webhook + current subscription lookup | Stripe IDs/status on user; roles | Stripe is billing authority, UI role/status cache | Stripe retains its own records; no card details stored here | Safe billing identifiers; current deletion omits cancellation F02 |
| Backup/audit/analytics | DB snapshots, file archives, job events, request/client logs | Data-root backup + external recovery sets, console/container logs; analytics derived from jobs | Off-site destination; audit event feed; support screenshots | Operator/provider custody; avoid sending private analytics elsewhere | Backups expire by policy; tombstone replay prevents erased account restoration; audit retention needs decision |
Unnecessary or risky copies: routine /auth/me returning career-related material deserves payload minimization; ApplicationUser career projections require explicit repair/consistency ownership; all-history JSON export materializes extra buffers; unbounded settings/AI history accumulates. Submitted-package copies are justified provenance and should not be deleted as “duplication.” Do not retroactively claim live CV history is a frozen rendered document: variant settings re-render against the current master profile.
Retention matrix
| Data | Storage / purpose | Current retention | Delete behavior | Backup behavior / target decision |
|---|---|---|---|---|
| Account, profile, jobs, companies | DB; core service | No general time expiry found | Staged account purge; jobs may remain in trash until purge | Included; disclose delayed backup expiry |
| CV settings/profile/letter histories | DB; recovery/provenance | No general bounded history retention found | Variant/account paths remove related rows | Define revision cap/age and pinned historical packages before pruning |
| CV extraction results | DB/files; import review | Newest 20 completed runs, current artifact preserved | Expired/unreferenced artifact cleanup | Verify backup copies age independently |
| Generated PDF | Owner/date folders | Default 30 days, configurable 1–365; pruning invoked on export | File cleanup/account lifecycle | Idle systems can retain files beyond interval; schedule cleanup if promising strict expiry |
| Attachments/package snapshots | Managed files + DB | While retained by product/account | Stage/quarantine/commit/purge; reconcile interrupted states | Include manifests/hashes and original/published copies |
| User account ZIP | Owner export folder | Stream uses DeleteOnClose after generation | Successful download disposes/removes file | Crash before streaming/disposal needs stale-artifact cleanup verification |
| AI sidecar cache | Process memory | 1h TTL / 1024 entries | Cache purge endpoint, process restart | Not a DB backup; request/result records are separate |
| AI usage/operations/notifications | DB; metering/recovery | No general purge policy found | Owner deletion; operation state deadlines are not row retention | Propose short result/diagnostic retention with longer aggregate usage; owner decision |
| Email/provider connection | DB plus remote provider | Until disconnect/deletion; provider has own policy | Local token/copy removal | Cannot promise removal of mail already delivered elsewhere |
| Sessions/trusted devices | DB/security | Credential validity expiry; row expiry cleanup not established | Revoke/delete on relevant account actions | Never restore usable sessions without security policy review |
| Security audit / operational logs | Proposed separate ledger / current logs | Docker rotation 10MB x3 for defined services; time policy absent | Current timeline deletions are not immutable audit handling | Proposal: 30-day operational, 90-day security initial review; not legal requirements |
| Local SQLite scheduled backups | Data-root backups | Default 14 retained snapshots | Old snapshots pruned | Local host loss still loses these; off-host complete sets needed |
| Deploy recovery sets | Host backup directory/off-host copy option | Default 30-day daily + 12 monthly sets | Retention script pruning | Actual off-host encryption, lifecycle and restore proof required |
| Deletion tombstones | Independent volume | No expiry established | Must survive older application restore | Keep through longest recoverable backup horizon; avoid logging owner personal content |
| Billing metadata | User row + Stripe | Local tied to account; provider policy external | F02 unresolved subscription cancellation | Retained billing/security fields and statutory periods require owner/legal decision |
Access/correction/restriction/deletion requests need a documented operator channel, identity verification proportional to risk, export validation and a tracked outcome. Technical functionality does not establish compliance. Consent/privacy copy must identify local versus external AI, remote mail/calendar copies, backup lag, disabled deletion where applicable and optional public fields. Do not add a cookie banner solely because essential session cookies exist; assess actual non-essential services and jurisdiction with qualified review.
Product invariants and enforcement
| Invariant | Current enforcement / proof | Gap / permanent gate |
|---|---|---|
| No cross-user read/write/download | DbContext owner filters, CurrentUserService, many authorization suites; CV probes 404 | Extend owner substitution to every newly added endpoint/background subject |
| Referenced company/CV/job belongs to owner | Service validations, association tests; ordinary FKs | No blanket DB-level composite ownership proof; test joins/IgnoreQueryFilters/admin paths |
| Private data not published by default | IsPublic false; explicit share and rotating slug | Preview fields and revocation/noindex/file cache tests |
| Newer edit cannot be silently overwritten | No client revision contract | F01 currently violates; 409/412 and draft preservation |
| History is truthful | Append snapshots and submitted-package frozen bytes | Settings history is not immutable rendering; duplicate version race remains |
| Retry does not duplicate irreversible effects | AI keys/leases and employer send attempt states | Job create/checkout/reminder paths need intent dedup |
| Only trusted server state grants paid access | Pro handler reads roles; queue rechecks before work | Concurrent billing synchronization/deletion F02 |
| Failed generation leaves valid content intact | Draft/review paths, structured parsing and queue status | Fault-inject every overwrite boundary; no “ready” without valid artifact |
| Deleted account cannot regain access after restore | Session/public revocation, staged purge, independent tombstones, tests | Billing cancellation and actual full restore drill |
| Owner work survives worker restart | Durable operations/lease recovery tests | Synchronous work/mail and file-in-progress behavior need explicit policy |
| Quota cannot be overspent concurrently | Process-local AI gates; partial upload check | Cross-pipeline/replica DB reservation; all-category byte accounting |
| AI does not invent/send facts autonomously | Prompts, normalization, explicit review/send APIs and synthetic eval fixtures | Live model/factuality/Bokmål evaluation still required; substring fixtures are not semantic proof |
| Startup cannot silently select wrong storage | Deploy script validation | F19 application-level configuration fallback |
Property/fuzz candidates: URL canonicalization and blocked IP families; status/date normalization; salary/numeric bounds; schema round-trip and revision monotonicity; filename/CSV quoting; bounded DOCX/PDF metadata. Use seeded bounded loops and existing frameworks first. Mutation testing is worthwhile only for owner predicates, quota comparison, session revocation and revision checks; do not optimize a whole-repo mutation or line-coverage percentage.