Skip to content

fix(world-local): prevent path traversal via request-supplied IDs#1829

Merged
TooTallNate merged 5 commits intomainfrom
fix/world-local-path-traversal
Apr 30, 2026
Merged

fix(world-local): prevent path traversal via request-supplied IDs#1829
TooTallNate merged 5 commits intomainfrom
fix/world-local-path-traversal

Conversation

@TooTallNate
Copy link
Copy Markdown
Member

Summary

  • Request-supplied identifiers (runId, eventId, stepId, hookId, correlationId, stream names, and tags) flowed straight into path.join() inside @workflow/world-local, so a client could send a payload like {"runId":"../../../package"} to /.well-known/workflow/v1/flow and read or write files outside the workflow data directory.
  • Added a centralized assertSafeEntityId helper in packages/world-local/src/fs.ts that rejects IDs which are empty, start with ., or contain /, \, or NUL bytes. This is permissive enough to accept every existing valid ID shape (ULIDs, composite keys like wrun_X-step_Y, base64url stream namespaces, tags like vitest-0) while blocking real traversal vectors.
  • Applied the check at each storage-layer entry point that composes IDs into filesystem paths: fs.taggedPath / readJSONWithFallback / paginatedFileSystemQuery, the runs / steps / events / hooks storage methods, and the streamer.

Test plan

  • pnpm --filter @workflow/world-local test — 266 → 328 passing (62 new unit + integration tests covering the exact payloads from the report: ../../../package, ../runs/wrun_…, backslash variants, NUL bytes, .locks, etc.)
  • pnpm --filter @workflow/core test — 591 passing

Request-supplied identifiers (runId, eventId, stepId, hookId, correlationId,
stream names, and tags) flowed directly into path.join() calls, allowing a
client to send values like '../../../package' and cause the backend to read
or write files outside the workflow data directory.

Add a centralized validator (assertSafeEntityId) that rejects IDs which are
empty, start with '.', or contain path separators or NUL bytes. Apply it at
each storage-layer entry point that composes IDs into filesystem paths:
fs.taggedPath / readJSONWithFallback / paginatedFileSystemQuery, the runs /
steps / events / hooks storage methods, and the streamer.
Copilot AI review requested due to automatic review settings April 22, 2026 21:21
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Apr 22, 2026

🦋 Changeset detected

Latest commit: 8d3898a

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 19 packages
Name Type
@workflow/world-local Patch
@workflow/cli Patch
@workflow/core Patch
@workflow/vitest Patch
@workflow/world-postgres Patch
workflow Patch
@workflow/world-testing Patch
@workflow/builders Patch
@workflow/next Patch
@workflow/nitro Patch
@workflow/web-shared Patch
@workflow/web Patch
@workflow/ai Patch
@workflow/astro Patch
@workflow/nest Patch
@workflow/rollup Patch
@workflow/sveltekit Patch
@workflow/vite Patch
@workflow/nuxt Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@vercel
Copy link
Copy Markdown
Contributor

vercel Bot commented Apr 22, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
example-nextjs-workflow-turbopack Ready Ready Preview, Comment Apr 30, 2026 8:43am
example-nextjs-workflow-webpack Ready Ready Preview, Comment Apr 30, 2026 8:43am
example-workflow Ready Ready Preview, Comment Apr 30, 2026 8:43am
workbench-astro-workflow Ready Ready Preview, Comment Apr 30, 2026 8:43am
workbench-express-workflow Ready Ready Preview, Comment Apr 30, 2026 8:43am
workbench-fastify-workflow Ready Ready Preview, Comment Apr 30, 2026 8:43am
workbench-hono-workflow Ready Ready Preview, Comment Apr 30, 2026 8:43am
workbench-nitro-workflow Ready Ready Preview, Comment Apr 30, 2026 8:43am
workbench-nuxt-workflow Ready Ready Preview, Comment Apr 30, 2026 8:43am
workbench-sveltekit-workflow Ready Ready Preview, Comment Apr 30, 2026 8:43am
workbench-vite-workflow Ready Ready Preview, Comment Apr 30, 2026 8:43am
workflow-docs Ready Ready Preview, Comment, Open in v0 Apr 30, 2026 8:43am
workflow-swc-playground Ready Ready Preview, Comment Apr 30, 2026 8:43am
workflow-web Ready Ready Preview, Comment Apr 30, 2026 8:43am

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 22, 2026

📊 Benchmark Results

📈 Comparing against baseline from main branch. Green 🟢 = faster, Red 🔺 = slower.

workflow with no steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
💻 Local 🥇 Nitro 0.044s (+2.1%) 1.005s (~) 0.961s 10 1.00x
💻 Local Express 0.044s (~) 1.005s (~) 0.961s 10 1.00x
🐘 Postgres Express 0.049s (-15.0% 🟢) 1.009s (~) 0.960s 10 1.12x
🐘 Postgres Nitro 0.057s (-40.5% 🟢) 1.010s (-3.2%) 0.953s 10 1.29x
workflow with 1 step

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 1.120s (-2.3%) 2.009s (~) 0.889s 10 1.00x
💻 Local Express 1.126s (~) 2.005s (~) 0.880s 10 1.01x
💻 Local Nitro 1.129s (~) 2.006s (~) 0.876s 10 1.01x
🐘 Postgres Nitro 1.135s (~) 2.009s (~) 0.874s 10 1.01x
workflow with 10 sequential steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 10.691s (-2.5%) 11.018s (~) 0.327s 3 1.00x
🐘 Postgres Nitro 10.886s (~) 11.018s (~) 0.132s 3 1.02x
💻 Local Express 10.937s (~) 11.023s (~) 0.086s 3 1.02x
💻 Local Nitro 10.947s (~) 11.023s (~) 0.076s 3 1.02x
workflow with 25 sequential steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 13.971s (-4.2%) 14.021s (-6.7% 🟢) 0.050s 5 1.00x
🐘 Postgres Nitro 14.494s (-0.7%) 15.019s (~) 0.525s 4 1.04x
💻 Local Nitro 14.982s (-0.5%) 15.280s (-4.7%) 0.298s 4 1.07x
💻 Local Express 14.995s (~) 15.029s (~) 0.034s 4 1.07x
workflow with 50 sequential steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 12.932s (-7.7% 🟢) 13.021s (-10.8% 🟢) 0.089s 7 1.00x
🐘 Postgres Nitro 13.814s (-1.1%) 14.020s (-2.0%) 0.206s 7 1.07x
💻 Local Nitro 16.416s (-2.2%) 17.031s (~) 0.614s 6 1.27x
💻 Local Express 16.689s (+0.5%) 17.030s (~) 0.341s 6 1.29x
Promise.all with 10 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 1.196s (-5.1% 🟢) 2.009s (~) 0.812s 15 1.00x
🐘 Postgres Nitro 1.253s (-1.7%) 2.009s (~) 0.756s 15 1.05x
💻 Local Nitro 1.504s (-7.8% 🟢) 2.006s (-3.3%) 0.502s 15 1.26x
💻 Local Express 1.512s (+1.6%) 2.006s (~) 0.494s 15 1.26x
Promise.all with 25 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 2.278s (-3.5%) 3.009s (~) 0.731s 10 1.00x
🐘 Postgres Nitro 2.338s (-0.6%) 3.010s (~) 0.673s 10 1.03x
💻 Local Nitro 2.828s (-10.0% 🟢) 3.008s (-22.6% 🟢) 0.180s 10 1.24x
💻 Local Express 2.958s (~) 3.208s (-7.1% 🟢) 0.250s 10 1.30x
Promise.all with 50 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 3.371s (-3.3%) 4.009s (~) 0.638s 8 1.00x
🐘 Postgres Nitro 3.455s (-0.7%) 4.010s (~) 0.554s 8 1.03x
💻 Local Nitro 7.405s (-11.3% 🟢) 8.020s (-11.1% 🟢) 0.614s 4 2.20x
💻 Local Express 8.209s (-1.6%) 9.022s (~) 0.814s 4 2.44x
Promise.race with 10 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 1.199s (-4.6%) 2.007s (~) 0.808s 15 1.00x
🐘 Postgres Nitro 1.260s (~) 2.008s (~) 0.749s 15 1.05x
💻 Local Express 1.516s (-19.9% 🟢) 2.006s (-15.2% 🟢) 0.489s 15 1.26x
💻 Local Nitro 1.585s (-15.1% 🟢) 2.006s (-14.3% 🟢) 0.421s 15 1.32x
Promise.race with 25 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 2.269s (-3.1%) 3.009s (~) 0.740s 10 1.00x
🐘 Postgres Nitro 2.315s (-1.0%) 3.009s (~) 0.694s 10 1.02x
💻 Local Nitro 2.903s (-5.3% 🟢) 3.454s (-11.1% 🟢) 0.550s 9 1.28x
💻 Local Express 3.170s (+1.2%) 4.010s (+6.6% 🔺) 0.840s 8 1.40x
Promise.race with 50 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 3.388s (-3.2%) 4.008s (~) 0.620s 8 1.00x
🐘 Postgres Nitro 3.476s (~) 4.009s (~) 0.533s 8 1.03x
💻 Local Nitro 8.071s (-11.7% 🟢) 9.021s (-10.0% 🟢) 0.950s 4 2.38x
💻 Local Express 8.942s (+1.6%) 9.276s (~) 0.335s 4 2.64x
workflow with 10 sequential data payload steps (10KB)

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 0.587s (-30.0% 🟢) 1.006s (-1.7%) 0.419s 60 1.00x
🐘 Postgres Nitro 0.789s (-3.8%) 1.006s (~) 0.216s 60 1.34x
💻 Local Express 0.987s (~) 1.158s (+7.7% 🔺) 0.171s 52 1.68x
💻 Local Nitro 1.011s (+3.1%) 1.627s (+48.7% 🔺) 0.616s 37 1.72x
workflow with 25 sequential data payload steps (10KB)

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 1.406s (-28.9% 🟢) 2.007s (-11.1% 🟢) 0.601s 45 1.00x
🐘 Postgres Nitro 1.886s (-2.2%) 2.030s (-3.4%) 0.144s 45 1.34x
💻 Local Express 3.040s (+0.8%) 3.609s (+0.7%) 0.569s 25 2.16x
💻 Local Nitro 3.048s (~) 3.729s (-0.8%) 0.681s 25 2.17x
workflow with 50 sequential data payload steps (10KB)

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 2.926s (-26.7% 🟢) 3.137s (-28.2% 🟢) 0.210s 39 1.00x
🐘 Postgres Nitro 3.860s (-5.9% 🟢) 4.077s (-11.4% 🟢) 0.218s 30 1.32x
💻 Local Nitro 8.969s (-3.5%) 9.479s (-5.4% 🟢) 0.510s 13 3.06x
💻 Local Express 9.232s (~) 9.942s (-0.8%) 0.710s 13 3.15x
workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 0.226s (-20.0% 🟢) 1.007s (~) 0.781s 60 1.00x
🐘 Postgres Nitro 0.272s (-4.1%) 1.007s (~) 0.735s 60 1.20x
💻 Local Nitro 0.551s (-8.9% 🟢) 1.004s (-1.7%) 0.454s 60 2.44x
💻 Local Express 0.578s (+3.2%) 1.004s (~) 0.426s 60 2.56x
workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 0.354s (-30.5% 🟢) 1.006s (~) 0.651s 90 1.00x
🐘 Postgres Nitro 0.484s (-2.5%) 1.007s (~) 0.523s 90 1.37x
💻 Local Nitro 2.402s (-5.4% 🟢) 3.009s (~) 0.607s 30 6.78x
💻 Local Express 2.564s (+2.0%) 3.009s (~) 0.445s 30 7.24x
workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 0.564s (-31.2% 🟢) 1.006s (-1.1%) 0.443s 120 1.00x
🐘 Postgres Nitro 0.769s (-2.6%) 1.007s (~) 0.238s 120 1.37x
💻 Local Nitro 10.339s (-7.6% 🟢) 11.028s (-5.5% 🟢) 0.689s 11 18.34x
💻 Local Express 11.094s (-0.9%) 11.756s (-1.5%) 0.662s 11 19.68x
Stream Benchmarks (includes TTFB metrics)
workflow with stream

💻 Local Development

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 0.153s (-25.5% 🟢) 1.000s (~) 0.001s (-31.3% 🟢) 1.009s (~) 0.856s 10 1.00x
💻 Local Express 0.201s (+0.7%) 1.004s (~) 0.012s (+2.5%) 1.019s (~) 0.818s 10 1.31x
💻 Local Nitro 0.205s (-3.9%) 1.004s (~) 0.010s (-20.0% 🟢) 1.016s (~) 0.810s 10 1.34x
🐘 Postgres Nitro 0.208s (+1.6%) 0.995s (~) 0.001s (-6.7% 🟢) 1.009s (~) 0.801s 10 1.36x
stream pipeline with 5 transform steps (1MB)

💻 Local Development

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 0.496s (-21.2% 🟢) 1.006s (~) 0.003s (-9.7% 🟢) 1.020s (~) 0.524s 59 1.00x
🐘 Postgres Nitro 0.597s (-4.4%) 1.007s (~) 0.004s (-5.7% 🟢) 1.021s (~) 0.424s 59 1.20x
💻 Local Express 0.752s (-0.6%) 1.012s (-1.6%) 0.010s (+1.5%) 1.024s (-1.6%) 0.271s 59 1.52x
💻 Local Nitro 0.857s (+2.2%) 1.011s (~) 0.009s (-1.4%) 1.115s (~) 0.258s 54 1.73x
10 parallel streams (1MB each)

💻 Local Development

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 0.903s (-6.0% 🟢) 1.109s (-13.2% 🟢) 0.000s (+25.5% 🔺) 1.118s (-14.4% 🟢) 0.215s 55 1.00x
🐘 Postgres Nitro 0.946s (-2.3%) 1.148s (-7.9% 🟢) 0.000s (-53.8% 🟢) 1.161s (-7.7% 🟢) 0.215s 52 1.05x
💻 Local Nitro 1.208s (-1.2%) 2.020s (~) 0.000s (+233.3% 🔺) 2.022s (~) 0.814s 30 1.34x
💻 Local Express 1.245s (+1.6%) 2.023s (~) 0.000s (-10.0% 🟢) 2.024s (~) 0.780s 30 1.38x
fan-out fan-in 10 streams (1MB each)

💻 Local Development

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 1.629s (-8.1% 🟢) 2.103s (-3.4%) 0.000s (+Infinity% 🔺) 2.112s (-3.9%) 0.483s 29 1.00x
🐘 Postgres Nitro 1.778s (-0.7%) 2.102s (-1.8%) 0.000s (-100.0% 🟢) 2.113s (-2.8%) 0.335s 29 1.09x
💻 Local Nitro 3.472s (+2.5%) 4.102s (+1.7%) 0.000s (-25.0% 🟢) 4.104s (+1.7%) 0.632s 15 2.13x
💻 Local Express 3.573s (+3.1%) 4.099s (+1.6%) 0.001s (-16.7% 🟢) 4.101s (+1.6%) 0.528s 15 2.19x

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World 🥇 Fastest Framework Wins
💻 Local Nitro 14/21
🐘 Postgres Express 21/21
Fastest World by Framework

Winner determined by most benchmark wins

Framework 🥇 Fastest World Wins
Express 🐘 Postgres 20/21
Nitro 🐘 Postgres 18/21
Column Definitions
  • Workflow Time: Runtime reported by workflow (completedAt - createdAt) - primary metric
  • TTFB: Time to First Byte - time from workflow start until first stream byte received (stream benchmarks only)
  • Slurp: Time from first byte to complete stream consumption (stream benchmarks only)
  • Wall Time: Total testbench time (trigger workflow + poll for result)
  • Overhead: Testbench overhead (Wall Time - Workflow Time)
  • Samples: Number of benchmark iterations run
  • vs Fastest: How much slower compared to the fastest configuration for this benchmark

Worlds:

  • 💻 Local: In-memory filesystem world (local development)
  • 🐘 Postgres: PostgreSQL database world (local development)
  • ▲ Vercel: Vercel production/preview deployment
  • 🌐 Turso: Community world (local development)
  • 🌐 MongoDB: Community world (local development)
  • 🌐 Redis: Community world (local development)
  • 🌐 Jazz: Community world (local development)

📋 View full workflow run

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 22, 2026

🧪 E2E Test Results

All tests passed

Summary

Passed Failed Skipped Total
✅ ▲ Vercel Production 989 0 67 1056
✅ 💻 Local Development 1066 0 86 1152
✅ 📦 Local Production 1066 0 86 1152
✅ 🐘 Local Postgres 1066 0 86 1152
✅ 🪟 Windows 96 0 0 96
✅ 📋 Other 270 0 18 288
Total 4553 0 343 4896

Details by Category

✅ ▲ Vercel Production
App Passed Failed Skipped
✅ astro 89 0 7
✅ example 89 0 7
✅ express 89 0 7
✅ fastify 89 0 7
✅ hono 89 0 7
✅ nextjs-turbopack 94 0 2
✅ nextjs-webpack 94 0 2
✅ nitro 89 0 7
✅ nuxt 89 0 7
✅ sveltekit 89 0 7
✅ vite 89 0 7
✅ 💻 Local Development
App Passed Failed Skipped
✅ astro-stable 90 0 6
✅ express-stable 90 0 6
✅ fastify-stable 90 0 6
✅ hono-stable 90 0 6
✅ nextjs-turbopack-canary 77 0 19
✅ nextjs-turbopack-stable 96 0 0
✅ nextjs-webpack-canary 77 0 19
✅ nextjs-webpack-stable 96 0 0
✅ nitro-stable 90 0 6
✅ nuxt-stable 90 0 6
✅ sveltekit-stable 90 0 6
✅ vite-stable 90 0 6
✅ 📦 Local Production
App Passed Failed Skipped
✅ astro-stable 90 0 6
✅ express-stable 90 0 6
✅ fastify-stable 90 0 6
✅ hono-stable 90 0 6
✅ nextjs-turbopack-canary 77 0 19
✅ nextjs-turbopack-stable 96 0 0
✅ nextjs-webpack-canary 77 0 19
✅ nextjs-webpack-stable 96 0 0
✅ nitro-stable 90 0 6
✅ nuxt-stable 90 0 6
✅ sveltekit-stable 90 0 6
✅ vite-stable 90 0 6
✅ 🐘 Local Postgres
App Passed Failed Skipped
✅ astro-stable 90 0 6
✅ express-stable 90 0 6
✅ fastify-stable 90 0 6
✅ hono-stable 90 0 6
✅ nextjs-turbopack-canary 77 0 19
✅ nextjs-turbopack-stable 96 0 0
✅ nextjs-webpack-canary 77 0 19
✅ nextjs-webpack-stable 96 0 0
✅ nitro-stable 90 0 6
✅ nuxt-stable 90 0 6
✅ sveltekit-stable 90 0 6
✅ vite-stable 90 0 6
✅ 🪟 Windows
App Passed Failed Skipped
✅ nextjs-turbopack 96 0 0
✅ 📋 Other
App Passed Failed Skipped
✅ e2e-local-dev-nest-stable 90 0 6
✅ e2e-local-postgres-nest-stable 90 0 6
✅ e2e-local-prod-nest-stable 90 0 6

📋 View full workflow run

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR hardens @workflow/world-local’s filesystem-backed storage against path traversal by validating request-supplied identifiers before they’re used in file paths, and adds regression tests to cover common traversal payloads.

Changes:

  • Introduces assertSafeEntityId (and UnsafeEntityIdError) and applies it across filesystem path composition helpers.
  • Adds ID validation at storage entry points (runs/steps/events/hooks) and streamer operations that use IDs in filenames.
  • Expands unit + integration coverage to ensure traversal payloads are rejected; adds a changeset for the patch release.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
packages/world-local/src/fs.ts Adds centralized ID validation + applies it in taggedPath, readJSONWithFallback, and paginatedFileSystemQuery.
packages/world-local/src/streamer.ts Validates runId and streamName before using them as filename prefixes / keys.
packages/world-local/src/storage/runs-storage.ts Validates runId before reading run JSON from disk.
packages/world-local/src/storage/steps-storage.ts Validates runId/stepId before reading steps and before listing by run prefix.
packages/world-local/src/storage/events-storage.ts Validates runId/eventId and request correlationId before composing composite keys and paths.
packages/world-local/src/storage/hooks-storage.ts Validates hookId before reading hook JSON from disk.
packages/world-local/src/fs.test.ts Adds focused tests for assertSafeEntityId and for validation in taggedPath/readJSONWithFallback.
packages/world-local/src/storage.test.ts Adds regression tests ensuring traversal payloads are rejected across storage APIs.
.changeset/world-local-path-traversal.md Declares a patch release for the security fix.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/world-local/src/fs.ts
Comment thread packages/world-local/src/fs.ts
Comment thread packages/world-local/src/fs.ts
Comment thread packages/world-local/src/fs.ts Outdated
Comment thread packages/world-local/src/fs.ts
Copy link
Copy Markdown
Contributor

@pranaygp pranaygp left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Solid, well-scoped fix. Validation is applied at every entry point I could trace, tests cover the exact payloads from the Latacora report, and all 328 @workflow/world-local tests pass locally. A few non-blocking suggestions inline — mostly around defense-in-depth and error-type consistency. Copilot already flagged the docstring inaccuracies so I won't duplicate those.

One additional note not worth a line comment: legacy.ts's handleLegacyEvent uses runId in path.join directly (lines 47 and 73 — unchanged by this PR). It's safe today because the function is only called from events.create after assertSafeEntityId('runId', runId) has already run, but the function is exported and nothing in its signature documents that invariant. A one-line assertSafeEntityId('runId', runId) at the top of handleLegacyEvent would make the guarantee local to the file.

Comment thread packages/world-local/src/fs.ts Outdated
Comment thread packages/world-local/src/fs.ts Outdated
Comment thread packages/world-local/src/fs.ts
Comment thread packages/world-local/src/storage/events-storage.ts
Copy link
Copy Markdown
Contributor

@pranaygp pranaygp left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two follow-ups not already covered in prior review rounds.

Comment thread packages/world-local/src/fs.ts Outdated
Comment thread packages/world-local/src/fs.ts Outdated
- UnsafeEntityIdError now extends WorkflowWorldError for consistency with
  other storage-layer errors and the platform error-to-HTTP mapping.
- Add resolveWithinBase(basedir, ...segments) containment helper and
  apply it at every taggedPath / readJSONWithFallback / .locks path
  construction site in events-storage and legacy, so a forgotten
  assertSafeEntityId at a future call site can't silently regress.
- Truncate attacker-controlled values in the error message.
- Drop unused assertSafeEntityIds helper and the unreachable typeof
  check under the TS signature.
- Fix docstrings on assertSafeEntityId / taggedPath JSDoc example /
  filePrefix validation comment to match what the code actually does.
- handleLegacyEvent now re-asserts runId locally so the invariant is
  documented at the call site instead of implicitly inherited from
  events.create.
@TooTallNate
Copy link
Copy Markdown
Member Author

Thanks for the review. Pushed b33b922 addressing every comment:

  • UnsafeEntityIdError now extends WorkflowWorldError (with name + static is()) so it flows through the platform's normal error mapping instead of surfacing as a generic 500.
  • Added resolveWithinBase(basedir, ...segments) as the belt-and-suspenders containment check, and routed every path.join-with-user-input through it (taggedPath, readJSONWithFallback, all four .locks/** sites in events-storage.ts, and the two sites in legacy.ts).
  • handleLegacyEvent now re-asserts runId locally so the invariant is documented at the call site (per your top-level note) and both its path.join sites use resolveWithinBase.
  • Error message values are truncated (48 chars + ellipsis) via truncateForError to limit attacker feedback.
  • Removed the unused assertSafeEntityIds helper and the unreachable typeof branch.
  • Fixed the docstring / example / filePrefix comment inaccuracies Copilot flagged.

Tests: 335 passing in @workflow/world-local (328 → 335, +7 for resolveWithinBase and the WorkflowWorldError hierarchy), 591 passing in @workflow/core.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 10 out of 10 changed files in this pull request and generated 1 comment.

Comments suppressed due to low confidence (1)

packages/world-local/src/streamer.ts:77

  • listChunkFilesForStream() validates streamName, but tag is also embedded into streamer chunk filenames elsewhere via tagSuffix (and is used here to build tagged extensions). If tag can ever be influenced outside trusted config, a value containing /, .., or \\ could still lead to writes outside the intended chunks directory. Recommend validating tag with the same rules (e.g., assertSafeEntityId('tag', tag)) at streamer creation time or before any chunk path is composed, and/or using resolveWithinBase() when building chunk file paths for defense in depth.
  // Name is used as a filename prefix below; validate it can't escape chunksDir.
  assertSafeEntityId('streamName', name);
  const listPromises: Promise<string[]>[] = [
    listFilesByExtension(chunksDir, '.bin'),
    listFilesByExtension(chunksDir, '.json'),
  ];
  if (tag) {
    listPromises.push(listFilesByExtension(chunksDir, `.${tag}.bin`));
  }

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

): string {
const resolvedBase = path.resolve(basedir);
const joined = path.resolve(basedir, ...segments);
if (joined !== resolvedBase && !joined.startsWith(resolvedBase + path.sep)) {
@TooTallNate TooTallNate enabled auto-merge (squash) April 30, 2026 07:30
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.

5 participants