Files
cesnimda b176a44627 docs: reorganize tree, restore architecture + research from archive, add Phase 0 reports
Active docs/ was stub scaffolding while the real docs sat in docs/_archive/.
Restore and correct them, and record the Phase 0 work.

- docs/architecture/current.md: verified system map (from archived SYSTEM_OVERVIEW,
  9 corrections against code).
- docs/research/competitors.md: sourced competitor analysis (from archived
  PRODUCT_RESEARCH, feature matrix corrected).
- docs/decisions/ADR-002-job-application-model.md: the Job/JobApplication split.
- docs/application-discovery-report.md, docs/implementation-roadmap.md,
  docs/phase-0-foundation-report.md, docs/career-workspace-branch-assessment.md.
- Remove 10 zero-byte placeholder files that advertised content that never existed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 17:04:32 +02:00

6.7 KiB
Raw Permalink Blame History

SECURITY_REPORT.md — Session Change Review

Phase 6 deliverable. Scope: security review of the changes made in this work session (Wave 0 + roadmap H1H4), plus confirmation that the tenant-isolation model still holds. Date: 2026-07-03. Complements the prior standalone assessments in docs/security-assessments/ (M013 adversarial, M014 remediation, M015 authorization replay).

This is not a full re-audit of the whole application — those live in docs/security-assessments/. It is a focused review of the new/changed surface so nothing shipped this session introduces a regression.


1. Summary

No new vulnerabilities were introduced. Tenant isolation on the new endpoints is carried by the existing JobTrackerContext global query filters and is now covered by regression tests. One latent correctness issue (a routable background-service method) was closed, and leaked runtime secrets were removed from version control (rotation recommended — see §6).

Severity Count Items
Critical 0
High 0
Medium 1 (mitigated) DataProtection keys present in git history (untracked this session; rotation recommended)
Low / hardening 3 see §5

2. New/changed attack surface reviewed

Change Surface Verdict
GET /jobapplications/{id}/match-score route int id; reads own CV + job Safe — tenant-scoped
GET /jobapplications/{id}/status-suggestion route int id; reads own correspondence Safe — tenant-scoped
GET /jobapplications/pipeline none (static metadata) Safe
PATCH .../status, Create/Update (status normalization) user string → JobPipeline.Normalize Safe — no injection, values stored parameterized
Structured salary fields numeric + short strings, NormalizeSalary Safe — clamps negatives, whitelists period
Automated DB backup (VACUUM INTO) server-controlled path Safe — see §4
Dev OpenAPI (/openapi/v1.json) schema Safe — Development environment only
EmailStatusClassifier reads stored correspondence text Safe — deterministic, no eval/injection

3. OWASP-oriented checklist for the new code

  • A01 Broken Access Control — The two new data endpoints load the job via _db.JobApplications.FirstOrDefaultAsync(j => j.Id == id), which is filtered by the global query filter CurrentUserId != null && OwnerUserId == CurrentUserId (deny-on-null, hardened in M013-2). A cross-user id returns NotFound, not another tenant's data. The correspondence lookup in status-suggestion and the JobEvent lookup in analytics are likewise filtered through their parent's owner. Verified by JobApplicationsAuthorizationTests (match-score + status-suggestion).
  • A03 Injection — All new persistence goes through EF Core parameterized queries. The only raw SQL added is VACUUM INTO '<path>' with a fully server-derived path (see §4). No string concatenation of user input into queries.
  • A03 ReDoS — New regexes (JobCvMatchService.TokenPattern, the revised SkillTagger C#/.NET patterns with fixed-width look-behinds) are linear with no catastrophic backtracking.
  • A04 Insecure Design — Status suggestions and match scoring are deterministic and human-confirmed (a status only changes when the user clicks). No automated outbound actions.
  • A05 Security Misconfiguration — OpenAPI is exposed only under IsDevelopment(); production deployments (ASPNETCORE_ENVIRONMENT=Production) do not serve it.
  • A08 Data IntegrityJobPipeline.Normalize canonicalizes status on write but preserves unknown custom values (no silent data loss).
  • A09 Logging — No secrets or PII added to logs by the new code.

4. Database backup — path handling

SqliteDatabaseBackupRunner runs VACUUM INTO '<target>'. The target is <Data:Root>/backups/jobtracker_backup_<UTC-timestamp>.db — no user input reaches it — and single quotes are escaped defensively. Backups contain the full database (sensitive) and are written to the same data volume as the live DB, i.e. the same trust boundary; they are git-ignored. For defense in depth, operators should ship backups off-host with transport encryption and restrict volume permissions. Recommendation (low): document an off-host, encrypted backup rotation in the deployment guide.


5. Low / hardening findings

  1. match-score input size (low). GetMatchScore does not cap job-description length before tokenizing. Descriptions are bounded in practice (imported/typed), and the algorithm is linear, so this is not a DoS, but a defensive cap (e.g. 50 KB) would be prudent.
  2. New read endpoints are not rate-limited (low). match-score/status-suggestion are cheap and deterministic (no AI, one indexed query), and auth-gated in production, so abuse potential is low. Consider a general authenticated-read limiter if the API is exposed publicly.
  3. status-suggestion is conservative for custom statuses (informational). A job in a non-canonical custom status (pipeline order = max) never receives a suggestion. This is safe (fails closed) but slightly under-surfaces; acceptable given custom statuses are rare.

6. Secrets hygiene (actioned this session)

  • Committed ASP.NET DataProtection key XML files (keys/, JobTrackerApi/keys/) and daily export JSON were removed from tracking and added to .gitignore (commit security: untrack DataProtection keys and runtime exports…).
  • These key files remain in git history. DataProtection keys sign auth/session artifacts, so rotating them on the production host is recommended (generate fresh keys; the app regenerates the key ring in the persisted keys/ directory on next start). Until rotated, anyone with history access could read the old key material.
  • Local .env remains git-ignored; appsettings.Development.json contains only CHANGE_ME_* placeholders. No live secrets are tracked.

7. Confirmed intact from prior assessments

Spot-checked that the M013M015 remediations are still in force after this session's changes:

  • Owner query filters still deny on null CurrentUserId (Data/JobTrackerContext.cs).
  • Local JWT still requires a concrete subject claim (LocalAuthIdentity, Program.cs).
  • Job-import SSRF guard (DNS resolution + private-range rejection, redirects disabled) untouched.
  • CSRF double-submit middleware and CORS allowlist untouched.

8. Retest

All backend tests pass (135), including the two new tenant-isolation tests for the new endpoints. No fix in this report required code changes beyond what already landed; the residual action for the operator is DataProtection key rotation (§6).