Files
ui/_docs/05_security/security_report_cycle4_delta.md
T
Oleksandr Bezdieniezhnykh 873749197a
ci/woodpecker/push/build-arm Pipeline failed
[AZ-512] Cycle 4 Steps 12-15: test-spec sync + docs + sec + perf
Steps 12-15 closure for cycle 4 (AZ-512 admin class inline edit):

- Step 12 (Test-Spec Sync): traceability O9 -> Covered; new FT-P-62
  + FT-N-18 in blackbox-tests.md.
- Step 13 (Update Docs): AdminPage module doc gains the inline-edit
  state slots, four new handlers, PATCH integrations row, expanded
  i18n key list, tests section. architecture.md row 272 now lists
  PATCH /api/admin/classes/{id} with AZ-513 deploy-gate caveat.
- Step 14 (Security Audit): cycle-4 delta report records one new
  LOW finding (F-SAST-CY4-1 lost-update / mid-air-collision on
  PATCH, by design per spec); verdict carries PASS_WITH_WARNINGS;
  bun audit re-run clean.
- Step 15 (Performance Test): NFT-PERF-01 bundle = 291 332 B
  (+757 B / +0.26% vs cycle 3; ~13.89% of 2 MB budget); PASS.

Tests 243 passed / 13 skipped / 0 failed (+12 AZ-512 cases).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-13 04:51:17 +03:00

13 KiB

Security Audit — Cycle 4 Delta Report

Date: 2026-05-13 Mode: Resume / incremental — cycle-2 base (security_report.md + companion artifacts) plus cycle-3 delta (security_report_cycle3_delta.md) are kept verbatim; this report records ONLY the deltas introduced by cycle 4. Cycle: Phase B / Cycle 4 (AZ-512 only — admin/ AZ-513 prerequisite still un-shipped; UI implemented under user-authorized Option B against MSW stubs) Scope of delta: cycle-4 commits only — ef56d9c (AZ-512 reactivation chore), ecacfa8 (AZ-512 implementation batch 16). No infrastructure / CI / nginx / Dockerfile changes; no new dependencies; no new external surface; no new secrets. Verdict (post-cycle-4): PASS_WITH_WARNINGS — unchanged from cycle 3. One new LOW finding documented (F-SAST-CY4-1 — lost-update / mid-air-collision admission on PATCH). All cycle-3 carries remain unchanged.


Verdict change

Verdict component Cycle 3 (2026-05-13 — pre-cycle-4) Cycle 4 (2026-05-13 — post AZ-512) Driver
Overall PASS_WITH_WARNINGS PASS_WITH_WARNINGS No change in severity ceiling
Critical 0 0
High 1 carried (F-SAST-1 — Google Geocode key in mission-planner/ git history; production-bundle exposure NONE) 1 carried (unchanged) User-action gate: key revocation still pending
Medium 7 carried 7 carried (unchanged) No cycle-4 changes to CI / nginx / Dockerfile
Low 3 carried 4 (new: F-SAST-CY4-1) New lost-update admission on PATCH /api/admin/classes/{id}

Cycle 4 scope — exactly what changed and what each change can / cannot affect

File Domain Security-relevant? Why / why not
src/features/admin/AdminPage.tsx Production source Yes — adds a new wire call to PATCH /api/admin/classes/{id} and a new client-side validation path. See finding F-SAST-CY4-1 below + carry analysis. No new credentials, no new external surface, no string interpolation in URL (endpoints.admin.class(id) builder is unchanged from cycle 2).
src/i18n/en.json, src/i18n/ua.json Production source No New translation keys are static strings rendered through React (auto-escaped). No interpolation of untrusted input.
tests/admin_class_edit.test.tsx Test-only No Vitest fixture; never shipped.
tests/msw/handlers/admin.ts Test-only No MSW worker; never shipped. Dockerfile final stage is nginx:alpine serving dist/.
tests/destructive_ux.test.tsx Test-only No Selector-target fix; logic unchanged.
_docs/02_document/**/*.md, _docs/03_implementation/**/*.md Documentation No Documentation only.

No new package added, no version bumped. bun audit re-run 2026-05-13 against ui/ reports "No vulnerabilities found" (bun 1.3.11). The cycle-3 OWASP A06 PASS verdict carries forward.


Resolved findings (cycle 3 → cycle 4)

None. Cycle 4 did not close any prior finding.

Pending user-action items (carried for visibility)
F-SAST-1 — Google Geocode API key in mission-planner/ git history → user-action: revoke at GCP credentials console + externalize via VITE_GOOGLE_GEOCODE_KEY (AZ-499 pattern).
OpenWeatherMap key revocation — recorded in cycle-2 retrospective; the AZ-449 code-side fix shipped but the revocation of the previously committed key is still a pending user action.

These two pending revocations are visible in _docs/06_metrics/retro_2026-05-12.md and the _docs/_process_leftovers/ set; they were not in scope for AZ-512 and remain open.


New cycle-4 findings

F-SAST-CY4-1 — Lost-update / mid-air-collision on PATCH /api/admin/classes/{id} — LOW

Field Value
Severity LOW
Category Insecure Design (OWASP A04) / Software & Data Integrity (OWASP A08)
Location src/features/admin/AdminPage.tsx:57-75 (handleUpdateClass)
Introduced by AZ-512 (commit ecacfa8)
Production exposure The /admin route is gated by Header's ADM permission AND backend authZ on every /api/admin/* call. Surface is restricted to authenticated admins.

Description

handleUpdateClass performs an inline edit with this sequence:

  1. Client-side validation (name.trim() non-empty, maxSizeM > 0).
  2. await api.patch(endpoints.admin.class(editingId), editForm) — sends the complete edit-form body (the documented "Risk 2 mitigation" so partial-merge vs full-replace PATCH semantics are equivalent for the UI).
  3. await api.get(endpoints.annotations.classes()) — refetch list, replace classes, clear editingId.

The intentional full-body PATCH guarantees the UI's view of the row replaces whatever is on the server. There is no concurrency guard (If-Match / ETag / version). If admin A and admin B open the same row simultaneously, the last PATCH wins silently and overwrites the other admin's edit without notification.

This is a deliberate trade-off: the task spec (AZ-512_admin_edit_detection_class.md) explicitly scopes optimistic-locking out, and AZ-513's backend spec mirrors that (no ETag header). The risk class is documented in the task spec's "Risks" section. The audit records it for completeness so future hardening can re-open it.

Impact

  • Two admins editing the same detection class in the same window → second save silently overwrites the first.
  • Audit trail (if any — owned by admin/ service) would show both PATCHes, so attribution survives.
  • Detection-class editing is a low-frequency administrative operation with typically a single active admin, so practical exposure is low.

Production-bundle exposure

Limited to authenticated ADM users, in a low-multi-admin operation domain, with no user-data leak. No exploitable path to data exfiltration or escalation. This is a correctness / data-integrity weakness, not an authN/authZ break.

Remediation (future / out-of-cycle)

  1. When AZ-513 lands the backend, decide whether admin/ will emit an ETag on GET /api/admin/classes/{id} and accept If-Match on PATCH. If yes, the UI side becomes:
    • Capture etag from the row on edit-start.
    • Send If-Match: <etag> header on PATCH.
    • On 412 Precondition Failed, render a "this class was changed by someone else — reload?" inline alert (analogous to today's editError = 'updateFailed').
  2. Cheaper short-term alternative: append a generated version: number to DetectionClass and have the UI assert it on PATCH; backend returns 409 on mismatch.

Track as: open in _docs/05_security/; not blocking. To be promoted to a UI ticket only when AZ-513 lands and the backend's chosen concurrency model is known.


Cross-cutting cycle-4 verification

Static analysis — AZ-512 deltas

  • URL construction: endpoints.admin.class(editingId) is the same builder used by handleDeleteClass (cycle-2 audited path). editingId: number | null is constrained at the type level and is only set from a server-returned DetectionClass.id. No tainted-input → URL path.
  • JSON body: editForm is a plain { name, shortName, color, maxSizeM } object. React form-controlled inputs feed it; no dangerouslySetInnerHTML, no innerHTML, no template injection surface. Backend must still validate length / charset (UI relies on backend per AZ-513 ACs).
  • Error path: the catch block sets a discriminated-union error kind, not the raw thrown message. No information leak from server error responses into the rendered UI.
  • Optimistic refetch: same shape as cycle-2-audited handleAddClass refetch. No new surface.
  • Test-only MSW handler in tests/msw/handlers/admin.ts: not bundled. Vite's bundle-introspect.test.ts (cycle-2 evidence) already enforces tests/ is excluded from dist/.

Verdict: PASS — no new injection, no new secret, no new auth-surface.

Authentication & authorization — AZ-512 deltas

  • Route gating: AZ-512 does not change /admin route gating. Header's hasPermission('ADM') continues to filter the visible nav entry. As cycle-2 noted (F2 / AC-22 carry), a user who deep-links to /admin without ADM still renders the page but every fetch 401/403s. AZ-512 inherits that posture exactly.
  • Per-action authZ: each PATCH/DELETE/POST/GET is authZ'd server-side by admin/. The UI does not perform pre-flight permission checks for the edit affordance specifically. This matches the existing add / delete posture (cycle-2 audited).

Verdict: PASS — no degradation; carries F2 / AC-22 unchanged.

Cryptographic failures, secrets, data exposure — AZ-512 deltas

  • No new secrets introduced. bun audit clean. No new env vars touched.
  • No PII in the PATCH body (detection-class metadata only).
  • No new log output: client.ts has no new logging path; AdminPage.tsx adds no console.*.
  • Error message localization: errors are mapped to i18n keys (admin.classes.updateFailed) — no server-message echo into the UI string.

Verdict: PASS.

OWASP Top 10 — categories whose status would change

None. All ten categories carry forward from the cycle-3 delta verdict unchanged. The new LOW finding F-SAST-CY4-1 maps to A04 (Insecure Design) but the category's status was already PASS (cycle 2) and stays PASS because LOW findings do not flip the category.

# Category Cycle-3 status Cycle-4 status
A01 Broken Access Control PASS_WITH_KNOWN (F2/AC-22 carry) unchanged
A02 Cryptographic Failures PASS_WITH_KNOWN (ADR-008 carry) unchanged
A03 Injection PASS unchanged
A04 Insecure Design PASS unchanged (new LOW F-SAST-CY4-1 is informational only)
A05 Security Misconfiguration FAIL (F-INF-2 carry) unchanged
A06 Vulnerable & Outdated Components PASS unchanged (bun audit re-run clean 2026-05-13)
A07 Identification & Authentication Failures PASS unchanged
A08 Software & Data Integrity Failures FAIL (F-INF-1, F-INF-3, F-INF-4 carry) unchanged
A09 Logging & Monitoring N/A unchanged
A10 SSRF N/A unchanged

Infrastructure / CI / Container — AZ-512 deltas

None. Cycle 4 did not touch Dockerfile, nginx.conf, .woodpecker/build-arm.yml, .env.example, or any container/CI artifact. Carries F-INF-1..5 verbatim.


Cross-workspace dependency note

AZ-512 ships against MSW stubs in tests. The live PATCH /api/admin/classes/{id} endpoint does not exist in production until AZ-513 is implemented and deployed by the admin/ workspace team. Until then:

  • A real admin clicking ✎ + Save in the deployed dev/stage/prod UI will hit a backend 404 (or 405 depending on how admin/ rejects unknown methods).
  • The UI surfaces a generic editError = 'updateFailed' ⇒ "Update failed" inline alert. No information leak.
  • Deploy gate: Step 16 of cycle 4 must NOT promote this build past the boundary where AZ-513 has not yet landed. The _docs/_process_leftovers/2026-05-13_az-512-admin-classes-prereq.md leftover entry remains open until AZ-513 ships + deploys.

This is a process control concern, not a security finding — captured here so the audit history records why a deploy-gate exists for an otherwise-clean cycle.


Updated counts (carries from cycle 3 + cycle-4 net)

Severity Cycle 3 Cycle 4 Net change
Critical 0 0
High 1 (F-SAST-1) 1 (F-SAST-1)
Medium 7 7
Low 3 (F-SAST-4, F-INF-5, F-SAST-CY3-1) 4 (+F-SAST-CY4-1) +1

Self-verification (Phase 5 of security/SKILL.md)

  • All cycle-4 changed files reviewed (6 source/test files + doc files; surface enumerated above).
  • No duplicate findings (F-SAST-CY4-1 is new, not a restatement of F-INF-1..5 or F-SAST-CY3-1).
  • Every finding has remediation guidance (see F-SAST-CY4-1 § Remediation).
  • Verdict matches severity logic (PASS_WITH_WARNINGS = only Medium/Low new findings + carried High is pre-existing).
  • bun audit re-run is clean.
  • No new credentials / secrets in cycle-4 commits (ef56d9c, ecacfa8).
  • Cross-workspace dependency (AZ-513) is recorded as a process / deploy-gate concern, not a security finding.

Recommendations

Immediate (Critical/High)

  • None new from cycle 4. Cycle-2 / cycle-3 carries unchanged: revoke Google Geocode key (F-SAST-1); revoke OpenWeatherMap key (carried).

Short-term (Medium)

  • None new from cycle 4. Cycle-2 carries unchanged: nginx security headers (F-INF-2); bun audit in CI (F-INF-1); Trivy/Grype in CI (F-INF-3); SBOM + image signing (F-INF-4).

Long-term (Low / Hardening)

  • F-SAST-CY4-1 follow-up: when AZ-513 lands, decide on the concurrency model with admin/. If ETag / If-Match: open a UI ticket to thread the header through client.ts and surface 412 as a "reload" alert. If version field: open a UI ticket to assert version on PATCH and surface 409 the same way. Cheap fix once the backend picks a model — until then, it stays LOW.