Skip to content

fix(files): bound YAML expansion and buffered reads on the file-serve path - #7319

Merged
icecrasher321 merged 5 commits into
stagingfrom
fix/file-serve-resource-limits
Sep 1, 2026
Merged

fix(files): bound YAML expansion and buffered reads on the file-serve path#7319
icecrasher321 merged 5 commits into
stagingfrom
fix/file-serve-resource-limits

Conversation

@icecrasher321

Copy link
Copy Markdown
Collaborator

Summary

Two unbounded resource paths, both reachable from the unauthenticated public share routes.

YAML alias expansion in the page compiler. loadYaml on sim: fence payloads had no ceiling on what the parsed value expands to. Aliases are shared references, so load is cheap and the DAG is small — the cost lands in whatever walks that DAG as a tree. columns: &c [x ×N] plus a row list of aliases to it renders N² cells through marked.parseInline for ~13N source bytes: 25 KB of source measured at 3.6s CPU, 38 MB of HTML and ~123 MB RSS, growing quadratically from there.

  • Extracted the file parser's expansion guard into a shared primitive that takes caller-supplied limits and an optional shared budget. assertYamlWithinLimits is now a thin wrapper over it; its limits and messages are unchanged.
  • The compiler charges the frontmatter and every fence to one per-compile budget, so splitting a payload across blocks buys no extra rendering, and an exhausted budget short-circuits before the next parse.
  • Both a node cap and a byte cap are needed — an aliased 64 KB string is only ~2,000 nodes but 320 MB of rendering.
  • An over-budget payload takes the existing skip-plus-diagnostic path, so the authoring agent is told which block was dropped and why.

Buffered reads on the serve paths. The Files-module serve path already capped reads at MAX_BUFFERED_TRANSFER_BYTES via fetchWorkspaceFileBuffer; five sibling paths serving the same objects did not. Workspace objects are admitted at 5 GB, so a share link was the one way to make an anonymous request hold gigabytes resident in the shared process.

  • Capped: the public share content route, the cloud proxy and public-asset proxy, and both local-file reads (a bare readFile had no size check at all).
  • downloadCopilotFile now takes a required maxBytes, matching fetchWorkspaceFileBuffer, so the next caller has to name a ceiling.
  • The public inline route reads the shared document only to scan it for embedded image references and then discards it — decoding to UTF-16 on top of the buffer — so it gets a much tighter ceiling than a file the route actually serves. A document too large to scan fails the referenced-by-doc gate closed and never reads the image.
  • Re-check after the page render on both routes: renderSimPageDocumentWithAssets inlines up to 32 MB of images, so a source under the ceiling can resolve to a document over it.
  • PayloadSizeLimitError now answers 413 instead of 500 and logs at info — it is a client-side answer, not an on-call page. This also fixes the 500 the already-capped workspace path returned.

100 MB is the ceiling this codebase already converged on for buffered work and the one the Files-module serve path already enforced; this brings the rest of the serve paths onto the same line rather than introducing a new policy.

Type of Change

  • Bug fix

Testing

  • New page-compile-limits.test.ts (8 tests) covering the alias bomb, the byte-vs-node distinction, the shared budget, the frontmatter path, and no-regression on ordinary pages. Verified non-vacuous: with the limits raised to MAX_SAFE_INTEGER, 5 of 8 fail.
  • New coverage on the serve and both public share routes for the ceilings and the 413. Also verified non-vacuous by reverting each guard.
  • Measured before/after on the alias bomb: 25 KB source 3585ms → 6ms; a 254 KB source that projected to minutes now completes in 22ms; a realistic 200-row table plus 40 steps still renders in 4ms.
  • bun run lint, bunx turbo run type-check, bun run check:audits (40/40), check:block-registry, docs-manifest:check, check:migrations all pass. 1,265 tests pass across app/api/files, lib/workspace-files, lib/file-parsers, lib/uploads.

Follow-up not in this PR

The cap is correct for paths that must transform (page compile, doc artifact resolution, HEIC derivative) — those need the whole buffer. Plain passthrough downloads do not and should stream; downloadFileStream already exists for S3/Blob/GCS/local. That removes the ceiling for passthrough entirely, and needs a streaming response helper plus restructuring resolveServableBytes to take a lazy reader so the transform decision stays in one place. Worth its own PR.

Checklist

  • Code follows project style guidelines
  • Self-reviewed my changes
  • Tests added/updated and passing
  • No new warnings introduced
  • I confirm that I have read and agree to the terms outlined in the Contributor License Agreement (CLA)

… path

Two unbounded resource paths reachable from the anonymous public share routes.

YAML alias expansion: the page compiler parsed sim: fence payloads with no
ceiling on what the parsed value expands to. Aliases are shared references, so
`columns: &c [...]` plus a row list of aliases to it renders N^2 cells from ~13N
source bytes — 25 KB of source cost 3.6s of CPU and 38 MB of HTML per request.
The expansion guard already in the file parser is now a shared primitive taking
caller-supplied limits, and the compiler charges every fence and the frontmatter
to one per-compile budget so splitting across blocks buys no extra rendering.

Buffered reads: the Files-module serve path already capped reads at
MAX_BUFFERED_TRANSFER_BYTES via fetchWorkspaceFileBuffer, but five sibling paths
serving the same objects did not, including both unauthenticated share routes.
A workspace object is admitted at 5 GB, so a share link was the one way to make
an anonymous request hold gigabytes resident in the shared process.
@vercel

vercel Bot commented Aug 31, 2026

Copy link
Copy Markdown

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

1 Skipped Deployment
Project Deployment Actions Updated
docs Skipped Skipped Sep 1, 2026 1:16am UTC

Request Review

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

All reported issues were addressed across 13 files

Heads up: you’re close to your included review allowance. Set a flex budget so reviews don’t pause.

Re-trigger cubic

Comment thread apps/sim/app/api/files/utils.ts Outdated
Comment thread apps/sim/app/api/files/public/[token]/content/route.ts
Comment thread apps/sim/lib/file-parsers/yaml-limits.ts Outdated
Comment thread apps/sim/lib/file-parsers/yaml-limits.ts Outdated
Comment thread apps/sim/app/api/files/serve/[...path]/route.ts Outdated
Comment thread apps/sim/lib/workspace-files/page-compile.ts
Comment thread apps/sim/lib/file-parsers/yaml-limits.ts Outdated
@greptile-apps

greptile-apps Bot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR bounds YAML expansion during page compilation and buffered file reads across authenticated and public serving paths, including the compiled-artifact lifecycle.

  • Applies shared node and serialized-size budgets across page frontmatter and YAML fences.
  • Caps cloud and local buffered reads and maps size-limit failures to HTTP 413.
  • Bounds compiled-artifact reads at the serving routes’ 100 MB ceiling and validates final transformed responses.
  • Adds regression coverage for expansion attacks, oversized reads, transformed outputs, and compiled artifacts.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains.

Important Files Changed

Filename Overview
apps/sim/lib/copilot/tools/server/files/doc-compiled-store.ts Caps compiled-artifact downloads at the widest consumer ceiling and preserves size-limit errors so serving routes return 413.
apps/sim/app/api/files/public/[token]/content/route.ts Bounds the anonymous source read and checks the final bytes after document, page, and image transformations converge.
apps/sim/app/api/files/serve/[...path]/route.ts Applies bounded reads across local and cloud paths and validates transformed output before responding.
apps/sim/lib/file-parsers/yaml-limits.ts Introduces bounded iterative measurement of expanded YAML nodes, depth, keys, and serialized size.
apps/sim/lib/workspace-files/page-compile.ts Shares one YAML expansion budget across frontmatter and every supported fence in a page compilation.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart LR
  R[File request] --> S[Bounded source read]
  S --> T{Transform required?}
  T -- No --> F[Final size assertion]
  T -- Page --> P[Bounded YAML compilation]
  T -- Document --> A[Bounded artifact read]
  T -- Image --> I[Derivative generation]
  P --> F
  A --> F
  I --> F
  F --> O[Serve response]
  F -- Over limit --> E[HTTP 413]
Loading

Reviews (5): Last reviewed commit: "fix(files): bound the shared artifact re..." | Re-trigger Greptile

Comment thread apps/sim/app/api/files/public/[token]/content/route.ts
… guard's own walk

Review round 1.

Bounding the source read did not bound the response: every branch that replaces
the source — a compiled document artifact fetched separately, a page with its
images inlined, a transcoded image derivative — could turn a source under the
ceiling into a response over it, on the anonymous share route included. The check
now sits where those branches converge rather than on the page branch alone.

The local read enforced its ceiling on a size measured before the read, so it
described the file only as of the stat. It now reads through the same bounded
stream reader the S3/Blob/GCS downloads use.

The expansion guard held one stack frame per pending node, so proving a wide
document too large allocated in proportion to the width it was rejecting. It now
holds one frame per level of nesting and consumes each container through a lazy
generator, bounding its own working set by depth. A double serializes to up to 24
characters, so numbers are charged their serialized length rather than the flat
16-byte allowance they could outgrow.
@icecrasher321

Copy link
Copy Markdown
Collaborator Author

@greptile

The `raw=1` branch returned before the check, so the ceiling held for the
branches that transform bytes but not for the one that returns them unchanged.
Those bytes are already bounded by the read that produced them, so this changes
no behavior — it makes the guarantee hold for everything the resolver returns
rather than for every branch someone remembered to cover.
@icecrasher321

Copy link
Copy Markdown
Collaborator Author

@greptile

@icecrasher321

Copy link
Copy Markdown
Collaborator Author

@cubic review

@cubic-dev-ai

cubic-dev-ai Bot commented Sep 1, 2026

Copy link
Copy Markdown

@cubic review

@icecrasher321 I have started the AI code review. It will take a few minutes to complete.

Comment thread apps/sim/app/api/files/public/[token]/content/route.ts

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

All reported issues were addressed across 14 files

Heads up: you’re close to your included review allowance. Set a flex budget so reviews don’t pause.

Fix all with cubic | Re-trigger cubic

Comment thread apps/sim/app/api/files/serve/[...path]/route.ts
Comment thread apps/sim/app/api/files/public/[token]/content/route.ts
Comment thread apps/sim/app/api/files/public/[token]/content/route.ts
Comment thread apps/sim/lib/file-parsers/yaml-limits.ts
…e response

Review round 2.

The response-level ceiling rejected an oversized compiled artifact only after the
whole thing was resident, which is the allocation it exists to prevent. The
artifact read is bounded at its own funnel instead, and a size breach is rethrown
rather than folded into the "not built yet" null — that answer tells callers to
retry, which an oversized artifact would never stop doing. The cached image
derivative is bounded the same way, but keeps swallowing the breach, because a
miss there re-transcodes from an already-bounded source.

The local storage branch enforced its ceiling with stat-then-read, so it described
the file only as of the stat. It now reads through the same bounded-stream reader
the cloud branches use — this is the anonymous share path on a self-hosted
deployment, and the route helper was already fixed while the storage service it
sits next to was not.

The default js-yaml schema turns a timestamp into a Date, which serializes to a
26-character quoted string; charging it the 16-byte flat allowance let a document
of them exceed the byte cap.
@icecrasher321

Copy link
Copy Markdown
Collaborator Author

@greptile

@icecrasher321

Copy link
Copy Markdown
Collaborator Author

@cubic review

@cubic-dev-ai

cubic-dev-ai Bot commented Sep 1, 2026

Copy link
Copy Markdown

@cubic review

@icecrasher321 I have started the AI code review. It will take a few minutes to complete.

Comment thread apps/sim/lib/copilot/tools/server/files/doc-compiled-store.ts Outdated

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

All reported issues were addressed across 19 files

Heads up: you’re close to your included review allowance. Set a flex budget so reviews don’t pause.

Fix all with cubic | Re-trigger cubic

Comment thread apps/sim/lib/uploads/server/image-derivative.ts Outdated
…iling

Review round 3.

The previous round bounded the compiled-artifact and cached-derivative reads at
the rendered-document ceiling, which is half what the serving routes will return.
That made the bound a policy rather than a backstop: an artifact between the two
figures was refused even though the route accepts a response that size, and a
derivative in that band read as a cache miss on every preview and re-transcoded
the original each time.

Both funnels now bound at the widest ceiling any of their consumers allows. A
consumer that permits less still enforces its own limit on what it got back — the
workspace download path continues to hold artifacts to the rendered-document
ceiling.
@icecrasher321

Copy link
Copy Markdown
Collaborator Author

@greptile

@icecrasher321

Copy link
Copy Markdown
Collaborator Author

@cubic review

@cubic-dev-ai

cubic-dev-ai Bot commented Sep 1, 2026

Copy link
Copy Markdown

@cubic review

@icecrasher321 I have started the AI code review. It will take a few minutes to complete.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

No issues found across 19 files

Confidence score: 5/5

  • Automated review surfaced no issues in the provided summaries.
  • No files require special attention.

Heads up: you’re close to your included review allowance. Set a flex budget so reviews don’t pause.

Re-trigger cubic

@icecrasher321
icecrasher321 merged commit e100c5a into staging Sep 1, 2026
27 checks passed
@waleedlatif1
waleedlatif1 deleted the fix/file-serve-resource-limits branch September 1, 2026 02:01
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