Compare commits

...

6 Commits

14 changed files with 278 additions and 2 deletions
@@ -159,6 +159,82 @@ public sealed class AdminProcessingControllerTests
Assert.Equal(expected, (await client.GetAsync(route)).StatusCode);
}
[Fact]
public async Task Audit_log_keeps_mail_subjects_in_the_owner_record()
{
await using var host = await Host.Start();
const string privateSubject = "Synthetic private correspondence subject";
const string operationalNote = "Synthetic workflow status change";
await using (var scope = host.Services.CreateAsyncScope())
{
var db = scope.ServiceProvider.GetRequiredService<JobTrackerContext>();
db.JobEvents.AddRange(
new JobEvent { JobApplicationId = 2, Type = "ReplyReceived", NewValue = "True:2026-11-03", Note = privateSubject, At = DateTime.UtcNow },
new JobEvent { JobApplicationId = 2, Type = "StatusChanged", OldValue = "Applied", NewValue = "Interview", Note = operationalNote, At = DateTime.UtcNow });
await db.SaveChangesAsync();
}
using var anonymous = host.Client(null);
using var ordinary = host.Client("owner");
foreach (var (client, status) in new[] { (anonymous, HttpStatusCode.Unauthorized), (ordinary, HttpStatusCode.Forbidden) })
{
using var response = await client.GetAsync("/api/admin/audit");
Assert.Equal(status, response.StatusCode);
Assert.DoesNotContain(privateSubject, await response.Content.ReadAsStringAsync());
}
using var admin = host.Client("admin");
var audit = await admin.GetFromJsonAsync<JsonElement>("/api/admin/audit");
var events = audit.EnumerateArray().ToDictionary(item => item.GetProperty("type").GetString()!);
Assert.DoesNotContain(privateSubject, audit.GetRawText());
var reply = events["ReplyReceived"];
Assert.Equal(JsonValueKind.Null, reply.GetProperty("note").ValueKind);
Assert.Equal("True:2026-11-03", reply.GetProperty("newValue").GetString());
Assert.Equal(2, reply.GetProperty("jobApplicationId").GetInt32());
Assert.Equal(operationalNote, events["StatusChanged"].GetProperty("note").GetString());
await using var verifyScope = host.Services.CreateAsyncScope();
var verifyDb = verifyScope.ServiceProvider.GetRequiredService<JobTrackerContext>();
Assert.Equal(privateSubject, await verifyDb.JobEvents.IgnoreQueryFilters().Where(item => item.Type == "ReplyReceived").Select(item => item.Note).SingleAsync());
}
[Fact]
public async Task User_polling_does_not_expose_another_owners_notifications_or_processing_records()
{
await using var host = await Host.Start();
var foreignNoticeId = Guid.NewGuid();
const string foreignNotice = "Synthetic notice for the other owner";
await using (var scope = host.Services.CreateAsyncScope())
{
var db = scope.ServiceProvider.GetRequiredService<JobTrackerContext>();
db.UserNotifications.AddRange(
new UserNotification { Id = Guid.NewGuid(), OwnerUserId = "owner", Kind = "operation_failed", Title = "Operation failed", Message = "Synthetic owner notice", CreatedAtUtc = DateTime.UtcNow },
new UserNotification { Id = foreignNoticeId, OwnerUserId = "other", Kind = "operation_failed", Title = "Operation failed", Message = foreignNotice, CreatedAtUtc = DateTime.UtcNow });
await db.SaveChangesAsync();
}
using var owner = host.Client("owner");
var notices = await owner.GetFromJsonAsync<JsonElement>("/api/notifications?owner=other");
Assert.Equal("Synthetic owner notice", Assert.Single(notices.EnumerateArray()).GetProperty("message").GetString());
var unread = await owner.GetFromJsonAsync<JsonElement>("/api/notifications/unread-count?owner=other");
Assert.Equal(1, unread.GetProperty("count").GetInt32());
Assert.Equal(HttpStatusCode.NotFound, (await owner.PostAsync($"/api/notifications/{foreignNoticeId}/read", null)).StatusCode);
Assert.Equal(HttpStatusCode.NotFound, (await owner.DeleteAsync($"/api/notifications/{foreignNoticeId}")).StatusCode);
Assert.Equal(HttpStatusCode.NotFound, (await owner.GetAsync($"/api/operations/{host.ForeignSubjectOperationId}")).StatusCode);
var ownOperation = await owner.GetStringAsync($"/api/operations/{host.OperationId}");
foreach (var internalValue in new[] { "SECRET_TOKEN", "SECRET_EXCEPTION", "SECRET_RESULT", "SECRET_CV_CONTENT" })
Assert.DoesNotContain(internalValue, ownOperation);
using var other = host.Client("other");
notices = await other.GetFromJsonAsync<JsonElement>("/api/notifications");
Assert.Equal(foreignNotice, Assert.Single(notices.EnumerateArray()).GetProperty("message").GetString());
await using var verifyScope = host.Services.CreateAsyncScope();
var verifyDb = verifyScope.ServiceProvider.GetRequiredService<JobTrackerContext>();
var notice = await verifyDb.UserNotifications.IgnoreQueryFilters().SingleAsync(item => item.Id == foreignNoticeId);
Assert.Null(notice.ReadAtUtc);
Assert.Null(notice.DismissedAtUtc);
}
[Fact]
public async Task Admin_projection_filters_owners_and_never_returns_raw_processing_contents_or_lease_data()
{
@@ -216,7 +292,9 @@ public sealed class AdminProcessingControllerTests
var builder = WebApplication.CreateBuilder();
builder.Logging.ClearProviders(); builder.WebHost.UseUrls("http://127.0.0.1:0");
builder.Services.AddControllers().AddApplicationPart(typeof(AuthController).Assembly);
builder.Services.AddAuthentication("test").AddScheme<AuthenticationSchemeOptions, TestAuthentication>("test", _ => { });
builder.Services.AddAuthentication("test")
.AddScheme<AuthenticationSchemeOptions, TestAuthentication>("test", _ => { })
.AddScheme<AuthenticationSchemeOptions, TestAuthentication>("local", _ => { });
builder.Services.AddAuthorization(); builder.Services.AddHttpContextAccessor();
builder.Services.AddRateLimiter(options =>
{
@@ -226,6 +304,8 @@ public sealed class AdminProcessingControllerTests
_ => new FixedWindowRateLimiterOptions { PermitLimit = permits, Window = TimeSpan.FromMinutes(1), QueueLimit = 0 }));
});
builder.Services.AddScoped<ICurrentUserService, CurrentUserService>();
builder.Services.AddScoped<UserOperationStore>();
builder.Services.AddScoped<UserNotificationStore>();
builder.Services.AddSingleton(TimeProvider.System);
builder.Services.AddDbContext<JobTrackerContext>(options => options.UseSqlite($"Data Source={Path.Combine(directory, "processing.db")};Pooling=False"));
var app = builder.Build();
@@ -51,7 +51,8 @@ public sealed class AdminAuditController : ControllerBase
e.Type,
e.OldValue,
e.NewValue,
e.Note,
// Imported replies keep the mailbox subject in the owner's timeline.
Note = e.Type == "ReplyReceived" ? null : e.Note,
e.At,
e.JobApplicationId
})
+41
View File
@@ -0,0 +1,41 @@
# Jobjakt Security Policy
## System and scope
Jobjakt manages private career profiles, CVs, letters, applications, correspondence and related account data. Review the repository-owned React/Next static-export UI, ASP.NET Core API, EF data and background processing, Python AI/document sidecar, browser extension, deployment/CI configuration and their trust boundaries. Architecture documentation describes nginx, MariaDB, private files and internal services; actual deployed exposure and identity must be verified separately. Retained SQLite and legacy document paths remain in scope while supported.
This policy supplies review context, not permission to run commands or test live targets. It does not certify existing controls. Root and component policies compose; nearest-path guidance takes precedence.
## Threat model and trust boundaries
Consider anonymous callers, two distinct authenticated owners, administrators, revoked or limited sessions, malicious document authors, incoming webhook senders and compromised external services. Treat route/body identifiers, uploaded files, HTML, job advertisements, external callbacks, logs and model output as untrusted.
Assets include canonical Career Profile facts/history, CV overrides and frozen revisions, application records, attachments, authentication/recovery material, provider tokens, billing entitlements, audit records and recoverable backups. Trace browser-to-API, API-to-database/files/workers, API-to-sidecar and provider callback/outbound fetch boundaries. Do not assume a role, a client UI flag or an internal-network label proves authorization.
## Security invariants
- Establish authenticated identity and owner scope before disclosing or changing private data. Caller-supplied owner IDs cannot replace server identity. Apply ownership to nested resources, lists/search, downloads, exports and background work, not only direct item reads.
- Enforce administrator and sensitive-operation permissions server-side, including self/last-admin restrictions. Session revocation, recovery, MFA and OAuth state/account-linking controls must preserve the intended identity boundary.
- Preserve canonical Career Profile ownership, null-as-inherit CV settings and immutable submitted/frozen snapshots. All applicable application/CV/letter writes must enforce existing revision preconditions and atomic persistence; stale operations must not partially commit or overwrite newer data.
- Bound untrusted document/upload/parser work. Validate type, size, owned paths and lifecycle state; reject traversal and unauthorized retrieval. Rendering/import must not execute active content, dereference unapproved URLs, expose local files or change another owner's data.
- Treat AI output as a proposal. Keep consent, entitlement, manual fallback, source/revision checks and explicit review/apply boundaries. Private content must not reach an unapproved model/provider; model or document text cannot authorize tool execution.
- Validate outbound destinations and redirects at network boundaries. Preserve job-provider rights/attribution and mailbox consent separately from sign-in identity. Validate webhook authenticity, identity binding and replay/idempotency where applicable.
- Preserve browser output encoding, safe HTML/rendering boundaries, origin/cookie/CSRF protections and validated return/navigation targets. UI visibility never substitutes for API authorization.
- Protect secrets, tokens and private document content in logs, diagnostics, browser bundles, exports and backups. Privacy deletion/retention must account for files, queued work and recovery copies; successful recovery claims require tested database/file/key consistency.
- Treat CI/deployment inputs and dependencies as supply-chain boundaries. Review least privilege, secret handling, provenance, image/runtime exposure and reversible recovery; test fixtures or historical reports do not prove production configuration.
## Reportable findings and severity context
Report reachable violations with attacker preconditions, affected assets and evidence. Cross-owner private-data disclosure/mutation, account takeover, privilege escalation, unsafe parser/rendering execution, exposed credentials and unauthorized external data transfer warrant impact-based investigation. Separate confirmed reproduction, reasoned likelihood and unverified assumptions.
Evaluate actual source/configuration and supported deployment modes. A package advisory, missing attribute or hypothetical path alone is not a reproduced exploit; equally, a passing mock test or limited deployment mitigation does not close a reachable issue. Record advisory/reachability and runtime evidence separately. Preserve failed, blocked, skipped and unexecuted checks explicitly.
## Exclusions and accepted risk
No vulnerability class, repository component or existing defect is implicitly suppressed. No new risk acceptance, severity downgrade or finding exclusion is granted by this policy. Historical deferred work and compatibility support remain reviewable.
Production, adjacent networks, real personal data and third-party systems are excluded from active testing unless separately and explicitly authorized. This execution restriction does not exclude review of repository-owned integration or deployment code. Synthetic local fixtures are distinct from approved staging and live-service qualification.
## Known limitations and qualification
The independent audit is incomplete. Source analysis, package checks, local fixtures, hosted CI, native accessibility, provider/model behavior, isolated staging and production recovery are separate evidence lanes. Final integrated security review follows relevant application changes. Baseline/deep/final scan results, cost allowance, staging scope and human release acceptance must be recorded separately; none is established by creating this file.
@@ -0,0 +1,14 @@
# Codex Security scan register
3 November2026 update: owner approved exact root policy creation/publication. Define-security-policy workflow COMPLETED for root policy only; source base88bcd6d9, exact candidate and resolver results in ../security/ROOT_POLICY_APPROVAL.md. Managed standalone artifact ID/path is documented there; there is no scan ID. Root now resolves for API/UI, nested CV-doc policy remains closest. Baseline/deep/final scans below remain NOT RUN/BLOCKED; no charges, active targets or release acceptance approved. Earlier policy-pending entries are historical.
2 November 2026. Source baseline `ba826de1e5c959352e4442ab2fec5d02445dd32d`, branch codex/security-offline-preflight. Owner confirms installation; actual tool metadata/skill files verified. Production/third parties excluded. Paid/deep compute allowance pending. ROE: ../security/00-rules-of-engagement.md.
| Phase/workflow | Status | Actual evidence and limits |
| --- | --- | --- |
| 00 define-security-policy review | NOT RUN (full workflow); resolver substep COMPLETED | Installed helper resolve-security-md --list exited 0, returned two docs-only policies. No root policy created; proposal/review/approval remains. No scan ID. |
| 01 baseline standard scan | NOT RUN | Skill/tool exposure checked; threat-model/policy and capability preflight pending. No scan artifacts or findings. |
| 03 deep scan | BLOCKED | Baseline and separately approved compute/cost scope missing. No scan launched. |
| 15 final standard and diff scans | NOT RUN | Await baseline, remediation range and independent verification. |
No scan findings, closure verdicts or human release acceptance are implied by tool availability or regression tests.
+11
View File
@@ -0,0 +1,11 @@
# Security Phase 00 — offline engagement draft
2 November 2026. Owner: Connor, direct-chat roadmap authorisation. Source baseline `ba826de1e5c959352e4442ab2fec5d02445dd32d`; isolated branch `codex/security-offline-preflight`. Phase 00 is PARTIAL, not accepted.
Authorised now: source inspection, harmless synthetic local fixtures, minimal ordinary remediation within approved roadmap boundaries, sanitized reports and branch publication. No network attack target has been approved. Production, adjacent LAN services, real accounts/data, identity/payment/email providers, live AI/models, orchestrator state/keys/configuration/queue and the pre-P10 acceptance worktree are excluded.
Active staging testing is BLOCKED until exact host/IP, test users/roles, rate/concurrency/duration limits, independent database/storage, disabled egress and recovery/stop controls are approved and verified. Do not infer those from example localhost URLs or repository credentials. Emergency stop: terminate only the named audit-owned fixture process; never stop existing production/user services.
Proposed fixture boundary: disposable synthetic tenant A, tenant B and administrator; separate temporary data root and database; inert CV files and canary records; mocked OAuth, billing, email, job providers and AI. No routable credentials. Snapshot and restore qualification must precede staged mutation. This plan has not been instantiated or approved for active network testing.
Evidence: only sanitized reports/test artifacts in Git. Do not inspect real .env values, auth/signing stores or personal documents. Findings require calibrated confidence and safe reproduction; absence of a scan result proves nothing. No root SECURITY.md policy edits, suppressed findings or accepted risks are authorised by this draft.
+17
View File
@@ -0,0 +1,17 @@
# Phase 01 — partial source-backed threat model
2 November 2026, baseline ba826de1e5c959352e4442ab2fec5d02445dd32d. PARTIAL; baseline scan NOT RUN. Read the complete Phase 01 prompt and architecture frontend/backend/data-flow/integrations, API reference, security notes and deployment overview. The graph query failed because the new isolated worktree has no graph; direct scoped source review followed. No active network request or live-service test.
Verified dependency and documented data-flow inventory is in ASSESSMENT_INDEX.md. Threat actors to test: anonymous caller, tenant A, tenant B, administrator, revoked/limited session, malicious document author, incoming webhook sender and compromised external service. Assets: canonical career profile and history, CV/letter snapshots, application records, attachments, OAuth/mail tokens, sessions, billing entitlements, backups and administrative privileges. Secret values have not been inspected.
Source boundary observations (not proof of a secure deployment):
- AttachmentsController requires the local authentication scheme. List first queries the requested job; download/rename call FindOwnedAttachmentAsync. Downloads reject unmanaged paths and pending/missing files. Global filter/helper implementation, alternate upload paths and negative tests still need independent audit.
- UsersController declares Admin role authorization. Role mutation and reset/email operations require endpoint-specific review; class attributes alone do not establish safe privilege transitions.
- BillingController status/checkout/portal explicitly use local authentication; webhook explicitly allows anonymous requests and caps request size at 1 MB. Webhook verifies the Stripe signature via ConstructEvent, rejects invalid signatures, then refreshes subscription state. This can trigger external Stripe egress when configured; do not execute it against a real provider during offline tests. Review identity binding, replay, price qualification and mock negative coverage before acceptance.
Initial attack tree: reach another tenant's data via object IDs, background execution or shared files; obtain higher privilege via role/session/OAuth flows; cross file/network/model boundaries through uploaded content; forge billing authority through webhook or stale subscription state; expose secrets through public bundles/logs/backups. Each branch requires source tracing plus synthetic negative proof. These are hypotheses, not confirmed vulnerabilities.
ASVS Level 2 is a proposed planning target for private employment data, not a compliance claim. No unverified requirement IDs or vulnerability identifiers are assigned. Partial endpoint matrix is ENDPOINT_AUTH_MATRIX.csv; tests listed there are candidate existing suites, NOT executed audit evidence. Full route enumeration, middleware/global filter review, deployed exposure and complete endpoint/test mapping remain open.
Next permitted review: resolve owner filters and attachment sibling paths, inspect auth/session middleware, and complete the source matrix. Baseline standard scan requires the documented capability preflight and recorded results; no scan pass is inferred from this threat model. Root policy changes, if proposed, require exact-diff approval under the policy skill; keep review-only work progressing meanwhile.
@@ -0,0 +1,29 @@
# Admin information isolation, offline increment
9 November 2026. Base `de3d5031c8208bc3841a528ae100ec2984d68501` on `codex/security-offline-preflight`. Compared the audit, Gmail, operations and fixture owners with A3 checkpoint `91e5a6bf`; their files were identical before this patch. Testing uses ephemeral loopback Kestrel and SQLite with synthetic owners and a test authentication scheme. No real mailbox, model, credential, provider, staging host or production service was accessed.
## Confirmed, privileged audit projection
The Gmail import creates a `ReplyReceived` event with the original mail subject in `JobEvent.Note`. The Admin-only Audit endpoint reads events across owners and previously sent that subject to administrators in the generic note field. A synthetic reply proved the subject was returned to an authenticated administrator. Anonymous and ordinary users were denied in the same test. This is excessive visibility to an existing administrative role, with a mailbox-import precondition. The fixture did not show an ordinary-user role bypass.
The read projection now omits `Note` for `ReplyReceived`. This also covers historical events without touching stored rows. The event type, response metadata, application association and ordinary status notes remain available. The owner's original event is preserved. Roles, undo/restore, notification producers and persisted data are unchanged.
## Covered boundaries
| Boundary | Evidence |
| --- | --- |
| Admin Audit | Loopback HTTP verifies 401/403 and the redacted Admin response. The allowed event metadata and a non-private status note remain. Stored owner subject still matches the synthetic input. |
| Admin Processing | Existing HTTP tests verify all three endpoints deny ordinary/anonymous callers, keep role and bounded rate enforcement, and omit raw CV content, file paths and lease data from allowed diagnostics. |
| User notifications | HTTP list/count ignore a forged owner query. A foreign notification ID returns 404 for mark-read and dismiss. The other owner still sees their own notice. Relational state confirms neither mutation occurred. |
| User operations | The owner cannot retrieve the other owner's operation by ID. The owner's own detail omits raw failure text, lease token, result reference and CV content. Public task stage/category remain part of the existing contract. |
| Pollers and producers | Reviewed EF owner filters and operation, saved-search and interrupted-mail producers. These write owner-bound user messages. No admin event producer was identified in the examined notification store. System and Users server role requirements were inspected and covered by existing authorization posture tests. |
| Public liveness/readiness | Source shows the existing minimal liveness version/status and readiness status contracts. Neither substitutes for the role-gated system diagnostic payload. No live checks run. |
The red test failed once on the original HTTP response. The final four selected suites passed 62 cases, with zero failures/skips and exit 0. Pinned .NET 9.0.317, locked restore from the local cache and Release tests with `--no-restore`. Dependency advisory checks were not run during the offline restore.
- [Red TRX](evidence/admin-information-isolation/red.trx.gz), uncompressed SHA-256 `3795be6331eb68ff9fef3648d1fea3d938f0333db7c755984eb5705c09f1cb56`.
- [Green TRX](evidence/admin-information-isolation/green.trx.gz), uncompressed SHA-256 `a46e1c00dbf0e486495bce7a11524fe5b75d9975788e35b63f8e82c074eaeaf6`.
## Open
Broader audit minimisation, administrative actions, email and realtime delivery boundaries, actual MariaDB and integrated role/session/deployment checks remain open. The Codex Security plugin is present. Formal baseline, diff/deep, integrated and staging scan activities were **NOT RUN** in this independent offline increment. Source and fixture results do not satisfy those later gates. No support-access or recovery permissions changed. [MFA recovery proposal](MFA_RECOVERY_PROPOSAL.md)
+21
View File
@@ -0,0 +1,21 @@
# Independent security assessment continuation
3 November2026 — root policy proposal explicitly approved and applied; ROOT_POLICY_APPROVAL.md records exact text identity and successful API/UI/nested policy resolution. This closes the root-policy decision only. Baseline/deep/final scans, current integration target/capability/cost preflight, full matrix and staging/human/release qualifications remain open. No new application/security fix or vulnerability closure is claimed.
Continuation: ATTACHMENT_OWNERSHIP_REVIEW.md traces the parent ownership filter and corrects the reflection-only test assumption. Fourteen existing relational fixture tests freshly passed on identical Phase 10 backend/test sources; native evidence published at db818e5e. Matrix distinguishes direct controller denials from unexecuted HTTP/list/upload/missing-owner/MariaDB checks. No full security scan or final integrated acceptance is implied.
Phase 00 offline preflight, 2 November 2026. Branch `codex/security-offline-preflight`, application baseline `ba826de1e5c959352e4442ab2fec5d02445dd32d`. No application/security implementation changed. No staging attacks or production access.
Read the external master, complete Phase 00 prompt, ROE/finding/release/scan-register templates, repository auditor skill, Codex Security policy and standard-scan skill instructions. Remaining stage prompts must be read before entering their stages. The old master heading “DO NOT START THIS NOW” is superseded for permitted offline work by the owner's later direct execution instruction; protected gates remain.
Verified inventory: package.json identifies Next 16.4.1, React 19/MUI/TypeScript; API csproj targets .NET 9 with EF SQLite and Pomelo MariaDB, Identity/JWT, Stripe, MailKit; Python requirements identify FastAPI/transformers/document dependencies. These are source dependencies, not deployed identities or proven reachable features. Architecture overview documents nginx/static export -> API -> DB/files/internal AI and provider boundaries; runtime exposure remains unverified. Preserve canonical Career Profile, CV null-as-inherit overrides, letter history and JobApplication.Version/RD21 contracts. No technology-version vulnerability claim is made without advisory/reachability analysis.
Baseline evidence reused from this exact source ancestry: P10-04 has 89 backend and 92 frontend tests passing, independently source-bound native artifacts. CLEAN-20 has five focused browser checks plus type/lint/build evidence. These are regression evidence, not a completed security audit; this new worktree did not rerun them. Backup parser repair exists on a separate published branch and is not integrated here.
Plugin exposure: standard/deep scan, draft/artifact/progress tools and documented skill files are available. The actual policy resolver ran successfully and found only docs/cv-import/SECURITY.md and docs/redesign/phase-9/p9-requal-01/SECURITY.md. Neither is an inherited root policy for API/UI source. No baseline/deep scan has run. Root policy creation needs exact proposed diff and separate owner approval under define-security-policy; defer that decision while independent source review continues. No new charge approved; deep scan remains blocked.
Execution order: finish inventory/threat boundaries and baseline policy review; standard scan after required preflight/gates; deep scan only with cost scope; phases 02–12 offline review/fixture work in source order; active phase 13 only with approved isolated target; remediation and independent retest; final full standard/diff scans; human release gate. Read every applicable subordinate prompt before its work. Do not treat these milestones as complete based on this plan.
Open: approved ROE/staging isolation, complete trust-boundary/endpoint matrix, root policy decision, baseline/final scans, deep-scan allowance, full independent audit, provider/device/CI/deployment identity and live backup/restore. Security release recommendation: BLOCKED/NO-GO pending evidence. No new vulnerability has been asserted in Phase 00.
Next permitted action: read Phase 01 and the linked architecture topics, verify endpoint/actor boundaries from source, and prepare the policy proposal without writing an unapproved root policy. No user input is immediately required for that review.
@@ -0,0 +1,11 @@
# Attachment ownership follow-through
2 November 2026. Source review plus bounded existing relational fixtures; not complete Phase 05, integrated security testing or deployed acceptance.
AttachmentsController.FindOwnedAttachmentAsync:42–47 includes JobApplication while selecting the raw child ID. JobTrackerContext:169–170 denies missing owners and filters JobApplication by CurrentUserId; the Attachment relation at 264–268 uses the parent foreign key. Attachment itself does not have a separate owner filter. Download, Rename and Delete all call this helper. ListForJob first checks the owned JobApplication exists, then limits child rows to that ID. Maintain these relationships; raw child queries without equivalent parent checks are not automatically safe.
AttachmentsControllerTests contains attribute/extension/size reflection tests, not cross-tenant behavioral proof. OwnershipGuardTests similarly checks helper presence. The stronger existing AttachmentConsistencyTests.Another_users_attachment_cannot_be_downloaded_renamed_or_deleted seeds a foreign job/file, clears tracking, checks three NotFound results and verifies the row/file remain.
Fresh targeted execution on the Phase 10 working tree: all 14 AttachmentConsistencyTests passed, zero failures/skips, exit 0, temporary SQLite and synthetic files only. Before execution, git diff between security and Phase 10 branches showed no JobTrackerApi/JobTrackerApi.Tests source difference. Existing Release assembly used --no-build/--no-restore. Evidence published and independently verified on Phase 10 at `db818e5ebd19eeedc05ae58dc3f7a908ce0e713d`, docs/redesign/phase-10/p10-04-evidence/attachment-ownership-and-storage.trx.gz. Uncompressed SHA256 `b589ad1139cb200d9de64b863d65e3304cea0230eda55d89476e5f79347fe84a`.
Result: selected local controller/storage behavior PASSED. Not an authenticated HTTP attack, complete negative list/upload/missing-owner matrix, MariaDB retest, production probe or final scan. Keep those gaps open. No new vulnerability or production exception is asserted; no backend fix was needed for these reviewed paths. Final integrated scan/testing stays deferred until relevant application changes are complete.
+10
View File
@@ -0,0 +1,10 @@
method,route,source,declared_auth,control_observed,sensitive_asset_or_effect,candidate_test_suite,audit_execution
GET,/api/attachments/{jobId:int},AttachmentsController.cs:87,local scheme,parent owner-filtered existence check before child listing,attachment metadata,AttachmentConsistencyTests,source reviewed; negative list and HTTP NOT RUN
GET,/api/attachments/download/{id:int},AttachmentsController.cs:103,local scheme,parent Include ownership filter and managed path checks,attachment bytes,AttachmentConsistencyTests,PASSED foreign-owner controller rejection; HTTP NOT RUN
PATCH,/api/attachments/{id:int},AttachmentsController.cs:125,local scheme,parent Include ownership filter via shared helper,metadata and AI-use flag,AttachmentConsistencyTests,PASSED foreign-owner controller rejection; HTTP NOT RUN
DELETE,/api/attachments/{id:int},AttachmentsController.cs:170,local scheme,parent Include ownership filter via shared helper,attachment removal,AttachmentConsistencyTests,PASSED foreign-owner controller rejection with row/file retained; HTTP NOT RUN
GET,/api/billing/status,BillingController.cs:42,local scheme,method declares local authentication,entitlement status,BillingControllerTests,NOT RUN
POST,/api/billing/checkout,BillingController.cs:58,local scheme,method declares local authentication; provider mocked only,checkout session,BillingControllerTests,NOT RUN
POST,/api/billing/portal,BillingController.cs:104,local scheme,method declares local authentication; provider mocked only,billing portal,BillingControllerTests,NOT RUN
POST,/api/billing/webhook,BillingController.cs:132,AllowAnonymous,signature verification and size limit; full binding audit pending,subscription entitlement,BillingControllerTests,NOT RUN
PUT,/api/users/{id}/roles,UsersController.cs:142,Admin role at class,privilege transition review pending,account roles,UsersControllerTests,NOT RUN
1 method route source declared_auth control_observed sensitive_asset_or_effect candidate_test_suite audit_execution
2 GET /api/attachments/{jobId:int} AttachmentsController.cs:87 local scheme parent owner-filtered existence check before child listing attachment metadata AttachmentConsistencyTests source reviewed; negative list and HTTP NOT RUN
3 GET /api/attachments/download/{id:int} AttachmentsController.cs:103 local scheme parent Include ownership filter and managed path checks attachment bytes AttachmentConsistencyTests PASSED foreign-owner controller rejection; HTTP NOT RUN
4 PATCH /api/attachments/{id:int} AttachmentsController.cs:125 local scheme parent Include ownership filter via shared helper metadata and AI-use flag AttachmentConsistencyTests PASSED foreign-owner controller rejection; HTTP NOT RUN
5 DELETE /api/attachments/{id:int} AttachmentsController.cs:170 local scheme parent Include ownership filter via shared helper attachment removal AttachmentConsistencyTests PASSED foreign-owner controller rejection with row/file retained; HTTP NOT RUN
6 GET /api/billing/status BillingController.cs:42 local scheme method declares local authentication entitlement status BillingControllerTests NOT RUN
7 POST /api/billing/checkout BillingController.cs:58 local scheme method declares local authentication; provider mocked only checkout session BillingControllerTests NOT RUN
8 POST /api/billing/portal BillingController.cs:104 local scheme method declares local authentication; provider mocked only billing portal BillingControllerTests NOT RUN
9 POST /api/billing/webhook BillingController.cs:132 AllowAnonymous signature verification and size limit; full binding audit pending subscription entitlement BillingControllerTests NOT RUN
10 PUT /api/users/{id}/roles UsersController.cs:142 Admin role at class privilege transition review pending account roles UsersControllerTests NOT RUN
+28
View File
@@ -0,0 +1,28 @@
# MFA recovery and factor replacement proposal
9 November 2026. Source review at `de3d5031c8208bc3841a528ae100ec2984d68501`. Owner approval is required before changes to authentication, recovery or support permissions.
## Existing behaviour
- `TwoFactorController` issues ten hashed, one-time-use recovery codes and accepts a code after the pending sign-in credential. Code generation uses 40 random bits. The current consume path reads an unused row and then saves it. Its concurrent single-use guarantee needs a relational test.
- Setup, disabling MFA and replacing recovery codes check the current password in an authenticated session. The existing setup action can replace an enrolled TOTP factor without proving possession of that factor. The disable action clears the secret and codes. These are source findings. Account takeover and concurrent replay were not tested.
- `UsersController` is Admin-only and supports password-reset delivery. It has no admin MFA reset action. Legacy Microsoft relinking is a separate identity-binding flow and grants no MFA or mailbox recovery authority.
NIST SP 800-63B-4 describes saved codes with at least 64 random bits, one-way storage, throttling, invalidation on use and notifications. Its AAL2 recovery guidance combines independent proof or repeated identity proofing. OWASP recommends reauthentication with an enrolled factor for factor changes and treating replacement as high risk. Use this guidance to design the Jobjakt flow and verify it separately. [NIST account recovery](https://pages.nist.gov/800-63-4/sp800-63b.html#account-recovery), [OWASP MFA factor changes](https://cheatsheetseries.owasp.org/cheatsheets/Multifactor_Authentication_Cheat_Sheet.html#changing-mfa-factors)
## Proposed decision 1. Strengthen user-controlled recovery
1. Require recent account reauthentication plus an existing factor for removal, replacement or code regeneration when MFA is already enrolled. Keep first-time enrollment distinct. A local password or an active session alone cannot replace the factor.
2. Generate new saved codes with at least 64 random bits. Store only a one-way hash, show plaintext only to the reauthenticated account owner and rate-limit attempts by account and source. Accept currently valid legacy codes during a documented transition so users retain their recovery path.
3. Consume the selected code atomically with the pending challenge. One concurrent attempt may succeed. A successful code-based recovery opens a short-lived, single-use flow bound to the account and purpose. The owner proves the replacement authenticator before it replaces the old one.
4. Send an independent account notification. Revoke prior sessions and trusted devices on completed recovery. Rotate consumed or replaced recovery material. Bind the result to the owner and preserve existing OAuth and mailbox boundaries.
Acceptance requires local and applicable provider sign-in compatibility, first enrollment, ordinary TOTP, legacy and new codes, races across two challenges, wrong-owner attempts, expiry, rate limits, restart and retry behaviour. A replacement failure must leave the previously valid factor in place. No usable recovery material may appear in logs, admin responses, audit notes or URLs.
## Proposed decision 2. Escalation when the user has lost every factor
Leave escalation disabled until the owner approves a verified identity policy and the separate design. Prefer an already enrolled alternative factor. For manual escalation, support can open a request with a short reason and owner ID. A separate authorised reviewer must verify identity using the approved evidence and recently step up with their own enrolled factor. Email access by itself is insufficient. Apply two-person approval for administrator or high-risk accounts and a documented delay and cancellation window. The request expires, and the user receives independent notifications at initiation, approval and completion.
Approval can grant only an expiring, single-use re-enrolment opportunity to the verified owner. It grants no login token to staff, no access to private content and no disclosure of any old or new factor. The final binding and revocation are owner-scoped, audited and atomic. Record actor, target, reason, request ID and disposition without private evidence in ordinary logs. Cap requests and verify denial, cancellation, expiry, replay, malicious staff, last-admin protections and rollback before enabling it.
The owner must choose the identity evidence, recovery delay, roles and escalation policy before implementation. No admin-assisted recovery or private support-access route is approved by this proposal.
+13
View File
@@ -0,0 +1,13 @@
# Root policy approval and resolver verification
3 November2026. Owner explicitly answered **"Approve policy creation and publication"** to the exact41-line SECURITY.md proposal and target in this chat. Approval includes validating the chain and committing/publishing on the security review branch; it does not approve a scan, charge, active testing or production action.
Target was reread and remained absent; HEAD was88bcd6d91c6823ffc6bbc6a6de176ab7769e729f and worktree was clean before creation. Applied text matches the approved candidate exactly after normal newline decoding. Policy LF SHA256: `4ec5365bdc1f84410b596b893463572b035f03d1260f51af722d1c32c8fa3eb3`.
Installed define-security-policy workflow: policy inventory and root/nearest-path guidance reviewed; both existing docs-only policies resolved inside the repository, regular files below1MiB. API had no inherited policy before the change. After creation the actual resolver exited0 for JobTrackerApi and job-tracker-ui with the new root policy, and for docs/cv-import with root plus its existing nearer policy. No inherited policy overwritten. Diff whitespace check passed. No application tests run for this Markdown-only change.
Exact proposal retained through Codex Security managed standalone artifact storage: `artifacts/root-policy-proposal.md`, artifact SHA256 `5d00af57cfa55de2792218916fd9cf646b470d821db6e8afee5c59c76dba116b`, at `C:\Users\Connor\.codex\state\plugins\codex-security\scans\Job-tracker\artifacts-ad2afb630ff68e10e32f349b87541ffb5778071babb98965362c085936a3f385\artifacts\root-policy-proposal.md`. Persistent readback succeeded. This is a standalone document collection, not a scan ID or result.
No new finding exclusion, risk acceptance or severity downgrade. Canonical-profile, tenant, revision, parser/file, AI-consent, provider and recovery invariants remain review requirements, not assertions that all controls already pass. No secret values or private user content read/exported. Source-security and final integrated acceptance remain open.
Next: reconcile the intended immutable integration revision before a baseline scan, finish capability/cost/scope preflight and record actual results. The older offline audit branch is not a final scan of A3 UI changes. Deep allowance and isolated staging remain distinct protected decisions; final integrated scanning follows relevant application changes.