Skip to content

chore(engine): close path-traversal class on run-artifact endpoints#5

Open
23min wants to merge 1 commit intomainfrom
chore/engine-run-path-traversal
Open

chore(engine): close path-traversal class on run-artifact endpoints#5
23min wants to merge 1 commit intomainfrom
chore/engine-run-path-traversal

Conversation

@23min
Copy link
Copy Markdown
Owner

@23min 23min commented Apr 20, 2026

Summary

Closes the full class of Path.Combine(artifactsRoot, runId) vulnerabilities flagged by ultrareview Finding 1 (work/gaps.md 2026-04-20). Originally reported against a single endpoint; this PR applies a shared GetSafeRunDirectory helper to all 9 sites across API + TimeMachine so the whole class is closed in one pass.

New helper FlowTime.Contracts.Storage.RunPathResolver.GetSafeRunDirectory(artifactsDirectory, runId):

  • Rejects null/whitespace inputs, path separators, ./..
  • Canonicalizes via Path.GetFullPath + trailing-separator-aware StartsWith root check
  • Located in FlowTime.Contracts so both API and TimeMachine consume the same helper

HTTP-layer convenience Program.TryResolveRunDirectory(artifactsDirectory, runId, out runPath) returns IResult? (null on success, 404 on invalid/missing). Collapses the endpoint try/catch/exists pattern from ~10 lines per site to ~4.

Sites closed

  • /v1/runs/{runId}/index, /series/{seriesId}, POST /export, GET /export/{format} (API Program.cs)
  • /v1/runs/{runId}/state, /state_window (Services/StateQueryService.cs)
  • /v1/runs/{runId}/graph (Services/GraphService.cs)
  • /v1/runs/{runId}/metrics (Services/MetricsService.cs)
  • /v1/runs/{runId} (Endpoints/RunOrchestrationEndpoints.cs)
  • TimeMachine/Orchestration/RunOrchestrationService.cs — run-reuse check and the explicit-runId Directory.Move write path (prevents user-supplied RunId from relocating the just-written run outside outputRoot)
  • TimeMachine/TelemetryBundleBuilder.cs — explicit run directory delete/write path

Tests

  • RunPathResolverTests — 26 unit tests covering valid paths, invalid input, traversal attempts (.., ../other, ../../etc/passwd, absolute paths), separator rejection, sibling-prefix false-positive guard (root vs root-sibling), trailing-separator branch, non-existence allowed
  • TryResolveRunDirectoryTests — 8 tests on the HTTP-layer helper

Full solution: 1736 passed / 9 skipped / 0 failed.

Test plan

  • dotnet build FlowTime.sln clean
  • dotnet test FlowTime.sln passes
  • Existing run-read endpoints still return 404 for nonexistent runs (regression guarded)
  • Smoke check on a live API instance with a traversal runId if desired

🤖 Generated with Claude Code

Ultrareview finding 1 (work/gaps.md 2026-04-20) called for a
GetRunDirectorySafe helper applied to the whole class of
Path.Combine(artifactsRoot, runId) call sites, not a single
endpoint. Initial patch covered 5 API sites; this extends the
fix to all 9 sites across API + TimeMachine.

New helper:
- src/FlowTime.Contracts/Storage/RunPathResolver.cs — static
  GetSafeRunDirectory(artifactsDirectory, runId). Validates
  non-null/non-whitespace, rejects path separators and "."/"..",
  canonicalizes via Path.GetFullPath, enforces candidate strictly
  under root via trailing-separator-aware StartsWith check.
  Throws ArgumentException on any violation.
  Located in Contracts so both API and TimeMachine consume it.

HTTP-layer helper:
- Program.TryResolveRunDirectory(artifactsDirectory, runId,
  out runPath) → IResult? — returns 404 IResult on invalid or
  missing runId, null on success. Collapses the endpoint
  try/catch/exists pattern from ~10 lines per site to ~4.

Sites patched:
- API/Program.cs — /runs/{runId}/index, /series, POST /export,
  GET /export/{format}
- API/Services/StateQueryService.cs — state, state_window
- API/Services/GraphService.cs — /runs/{runId}/graph
- API/Services/MetricsService.cs — /runs/{runId}/metrics
- API/Endpoints/RunOrchestrationEndpoints.cs — /runs/{runId}
- TimeMachine/Orchestration/RunOrchestrationService.cs — run-
  reuse check + explicit-runId Directory.Move write path (this
  one prevents a write-side traversal: without validation a
  user-supplied RunId could relocate the just-written run
  outside outputRoot)
- TimeMachine/TelemetryBundleBuilder.cs — explicit run directory
  delete/write path

Tests:
- RunPathResolverTests.cs — 26 unit tests: valid, invalid input,
  traversal attempts (.., ../other, ../../etc/passwd, absolute
  paths), separator rejection, sibling-prefix false-positive
  guard (root vs root-sibling), trailing-separator branch,
  non-existence allowed.
- TryResolveRunDirectoryTests.cs — 8 tests on the HTTP helper:
  existing dir → null, nonexistent → IResult, invalid runId →
  IResult + empty path, invalid artifactsDirectory → IResult.

An earlier iteration added RunPathTraversalEndpointTests that
sent .. as a URL segment; ASP.NET's routing normalises those
before parameter binding so the tests would have passed even
without the fix. Deleted. The unit tests on the helper are the
real security assurance; endpoint wiring is a single helper
call at each site.

Also:
- .gitignore: add .claude/worktrees/
- RunPathResolver: static readonly char[] for path separators
  (avoid per-call allocation)

Full solution: 1736 passed / 9 skipped / 0 failed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
23min added a commit that referenced this pull request May 1, 2026
…actions (m-E21-07 smoke-test polish)

Real-bytes manual smoke-test surfaced several UX gaps and one correctness bug
in the validation surface; this bundle ships the bug fix plus the chrome
adjustments needed for the surface to feel coherent against live data.

Bug fix (edge identity):
- Backend BuildEdgeWarnings keys edgeWarnings by analyser edge id (e.g.
  'source_to_target' for the lag YAML), but the topology rendering, panel
  edge rows, and workbench-edge-card lookup all expect the workbench's
  '${from}→${to}' convention. Mocked Playwright spec #5 used hand-rolled
  from→to keys so the gap stayed hidden.
- validation.setResponse(response, edges?) now accepts optional graph edge
  metadata; when provided the store rewrites raw edgeWarnings keys to the
  workbench convention before deriving rows + severity-max maps. Strips
  ':port' suffixes from graph endpoints (mirrors StateQueryService.
  ExtractNodeReference) so SourceNode:out → TargetNode:in resolves to the
  dag-map's data-edge-from='SourceNode' selector.
- Spec #2 (real-bytes) tightened to assert the edge indicator renders.

Topology indicator visuals:
- New triangleIndicatorPoints helper (vitest-covered, 3 cases) — upward-
  pointing equilateral triangle around (cx, cy) with circumradius r.
- Edge indicator is now a polygon triangle (r=6) at the path midpoint;
  reads sharply at small sizes against thin edge strokes. Edge stroke
  unmodified — composes with --ft-viz-amber selected-edge highlight.
- Node indicator stays a circle but enlarges to r=3 (matching the
  workbench-card severity dot's 6 px diameter) so the two surfaces feel
  consistent.

Severity row tinting (light mode):
- New --ft-warn-bg / --ft-err-bg / --ft-info-bg chrome tokens (very
  low-saturation hue washes in light mode; transparent in dark mode where
  the dot + border carry the signal without a heavy fill).
- New severityRowBackground helper (vitest-covered, 5 cases) — symmetric
  with severityChromeToken.
- Validation panel rows tinted by severity. Pinned warning-flagged cards
  (node + edge) tinted by severity — bg-card swapped via class:bg-card.

Workbench card interactions:
- Selected card border now turquoise (--ft-highlight) — symmetric with the
  validation-panel matching-row border treatment.
- Edge card gained a 'selected' prop; wired at the call site to
  'last-pinned wins' so the cross-link convention from validation panel
  and the visual selection convention stay aligned.
- Cards are now click-selectable: onSelect prop on both cards (role=button,
  tabindex=0, Enter/Space keyboard handler); close button stops propagation.
  Node card onSelect → setSelectedCell; edge card onSelect →
  bringEdgeToFront. Same selection convention as topology-node click and
  validation-row click.
- Sparkline moved above the metrics grid for visual primacy in the card.

Card list animations:
- animate:flip (220 ms) on each card wrapper so reorders / new pins sweep
  into place; in:fade (160 ms) for new-card entry, out:fade (120 ms) for
  close. Existing keys (pin.id, from→to) are FLIP's prerequisite.

Manual-verification recipe:
- New src/FlowTime.API/FlowTime.API.validation.http carries the lag-
  trigger YAML for VS Code REST Client manual smoke-testing. Recommended
  workflow: restart the API in test-runs mode (FLOWTIME_DATA_DIR=...
  /test-runs) so the verification run lands in data/test-runs/ rather
  than data/runs/.

ui-vitest 879 → 897 (+18 across topology-indicators, validation-helpers,
validation.svelte test suites). svelte-check baseline unchanged
(413 errors / 2 warnings). Tracking doc Decisions section updated with the
bug + the chrome adjustments captured during smoke-testing.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant