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>
6.7 KiB
SECURITY_REPORT.md — Session Change Review
Phase 6 deliverable. Scope: security review of the changes made in this work session (Wave 0 + roadmap H1–H4), 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 filterCurrentUserId != null && OwnerUserId == CurrentUserId(deny-on-null, hardened in M013-2). A cross-user id returnsNotFound, not another tenant's data. The correspondence lookup instatus-suggestionand theJobEventlookup in analytics are likewise filtered through their parent's owner. Verified byJobApplicationsAuthorizationTests(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 revisedSkillTaggerC#/.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 Integrity —
JobPipeline.Normalizecanonicalizes 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
- match-score input size (low).
GetMatchScoredoes 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. - New read endpoints are not rate-limited (low).
match-score/status-suggestionare 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. - 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(commitsecurity: 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
.envremains git-ignored;appsettings.Development.jsoncontains onlyCHANGE_ME_*placeholders. No live secrets are tracked.
7. Confirmed intact from prior assessments
Spot-checked that the M013–M015 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).