Skip to content

fix: improve Blueprint asset handling#1713

Open
jstarpl wants to merge 7 commits into
Sofie-Automation:mainfrom
nrkno:contrib/fix/improveBlueprintAssetHandling
Open

fix: improve Blueprint asset handling#1713
jstarpl wants to merge 7 commits into
Sofie-Automation:mainfrom
nrkno:contrib/fix/improveBlueprintAssetHandling

Conversation

@jstarpl
Copy link
Copy Markdown
Contributor

@jstarpl jstarpl commented Apr 13, 2026

About the Contributor

Type of Contribution

This is a:

Bug fix

Current Behavior

Blueprint assets can be stored in invalid locations, it's possible to use directory traversal attacks to read and write files using the Blueprint API, upload/download failures result in obscure errors. Blueprint asset URLs contain double slashes.

New Behavior

The simplest error (Not found) is correctly reported, directory traversal is not possible beyond Blueprint asset directory.

Testing

  • I have added one or more unit tests for this PR
  • I have updated the relevant unit tests
  • No unit test changes are needed for this PR

Affected areas

This PR affects Blueprint asset API

Time Frame

We intend to finish the development on this feature in two weeks time.

Other Information

Status

  • PR is ready to be reviewed.
  • The functionality has been tested by the author.
  • Relevant unit tests has been added / updated.
  • Relevant documentation (code comments, system documentation) has been added / updated.

@jstarpl jstarpl added the Contribution from NRK Contributions sponsored by NRK (nrk.no) label Apr 13, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 13, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Blueprint asset storage is restructured into storePath/assets/, with path-traversal protection added to both upload and retrieval. retrieveBlueprintAsset converts from synchronous to async with promise-based error handling. HTTP routes and client API calls are updated to accommodate the new storage path and async contract.

Changes

Blueprint Asset Storage and Security

Layer / File(s) Summary
Storage contract, path resolution, and traversal guards
meteor/server/api/blueprints/api.ts, meteor/server/api/blueprints/__tests__/api.test.ts
uploadBlueprintAsset and retrieveBlueprintAsset now resolve paths under storePath/assets/, validate that resolved paths remain within assetsDir to prevent traversal, and create/read files from the nested structure. retrieveBlueprintAsset converts to async, returning Promise<ReadStream> that resolves on stream open and rejects on error. Tests verify base64 decode-and-write behavior and reject traversal attempts.
HTTP route integration and error mapping
meteor/server/api/blueprints/http.ts, meteor/server/api/blueprints/__tests__/http.test.ts
Asset POST route (/assets) calls the updated uploadBlueprintAsset for each upload. Asset GET route (/assets/*fileId) awaits the now-async retrieveBlueprintAsset and maps errors to status codes: ENOENT404, traversal-message errors → 400, others → 500. Extension allowlisting and cache headers are preserved. Tests cover malformed payloads, successful uploads, partial failures, and error cases on retrieval.
Client-side asset URL construction
packages/webui/src/client/lib/Components/BlueprintAssetIcon.tsx, packages/webui/src/client/ui/PreviewPopUp/PreviewPopUpContext.tsx
Asset URLs are constructed via createPrivateApiPath('blueprints/assets/' + src) instead of manual string concatenation, aligning with the new storage path structure. Changes apply to blueprint images, split backgrounds, and transition previews.

Sequence Diagram

sequenceDiagram
    participant Client
    participant HttpRoute as HTTP Route<br/>(Server)
    participant CoreApi as Asset API<br/>(Server)
    participant Fs as Filesystem

    Client->>HttpRoute: GET /api/blueprints/assets/file123.png
    HttpRoute->>CoreApi: retrieveBlueprintAsset('file123.png')
    activate CoreApi
    Note over CoreApi: Resolve path under<br/>storePath/assets/
    Note over CoreApi: Validate no traversal
    CoreApi->>Fs: open(storePath/assets/file123.png)
    Fs-->>CoreApi: ReadStream (on 'open')
    deactivate CoreApi
    CoreApi-->>HttpRoute: Promise<ReadStream>
    HttpRoute->>HttpRoute: Set Content-Type, Cache headers
    HttpRoute-->>Client: Stream body + 200

    Client->>HttpRoute: POST /api/blueprints/assets
    HttpRoute->>CoreApi: uploadBlueprintAsset(fileId, base64Data)
    activate CoreApi
    Note over CoreApi: Resolve path under<br/>storePath/assets/
    Note over CoreApi: Validate no traversal
    CoreApi->>Fs: mkdir -p dirname(assetPath)
    CoreApi->>Fs: writeFile(assetPath, decodedBytes)
    deactivate CoreApi
    HttpRoute-->>Client: 200 (or 400/500 on error)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 10.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: improving Blueprint asset handling by fixing security and error reporting issues.
Description check ✅ Passed The description is directly related to the changeset, detailing the bug being fixed (directory traversal, invalid asset storage, obscure errors, double slashes) and the improvements made.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 13, 2026

Codecov Report

❌ Patch coverage is 72.22222% with 10 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
meteor/server/api/blueprints/api.ts 52.38% 10 Missing ⚠️

📢 Thoughts on this report? Let us know!

@jstarpl jstarpl force-pushed the contrib/fix/improveBlueprintAssetHandling branch from 8792b58 to 6b956dc Compare April 14, 2026 08:42
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@meteor/server/api/blueprints/api.ts`:
- Around line 134-138: The path traversal check in api.ts using assetsDir and
assetPath can be bypassed via sibling directory names; update the validation in
the asset lookup code to resolve real filesystem paths (use fs.realpath or
fs.realpathSync on assetsDir and assetPath), then compute
path.relative(assetsDirReal, assetPathReal) and reject the request if the
relative path starts with '..' or is equal to '..' (and also reject if
path.isAbsolute(relative) is true); ensure you reference the existing assetsDir,
assetPath and fileId variables and perform this stronger check before serving
the file.
- Around line 115-119: The current path traversal check using
assetPath.startsWith(assetsDir) is bypassable; update the containment check for
assetsDir/assetPath (where assetsDir, assetPath, fileId are used) to a robust
sibling-safe test — for example compute path.relative(assetsDir, assetPath) and
reject if it begins with '..' or equals ''/'. Ensure you normalize/resolved both
paths first (using path.resolve) and use a path separator-aware comparison (or
ensure the relative result is not outside the asset directory) before allowing
access.

In `@meteor/server/api/blueprints/http.ts`:
- Around line 203-210: The catch block in the HTTP handler currently returns 501
for retrieval failures; change the logic to return a proper status: detect
path-traversal errors thrown by retrieveBlueprintAsset (e instanceof Error &&
/path[\s-]*traversal/i.test(e.message) or another identifying message) and set
ctx.statusCode = 403 (and log via logger.warn including the error), treat
missing file (e.code === 'ENOENT') as 404 as existing, and for any other
unexpected errors set ctx.statusCode = 500 and log the full error with
logger.error; update the logger messages in this block to include the error
details and use the symbols retrieveBlueprintAsset, logger.warn/logger.error,
and ctx.statusCode to find the code.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: de827cc1-924c-47b8-bff5-fad590cbe226

📥 Commits

Reviewing files that changed from the base of the PR and between aace1e5 and 6b956dc.

📒 Files selected for processing (4)
  • meteor/server/api/blueprints/api.ts
  • meteor/server/api/blueprints/http.ts
  • packages/webui/src/client/lib/Components/BlueprintAssetIcon.tsx
  • packages/webui/src/client/ui/PreviewPopUp/PreviewPopUpContext.tsx

Comment thread meteor/server/api/blueprints/api.ts Outdated
Comment thread meteor/server/api/blueprints/api.ts Outdated
Comment thread meteor/server/api/blueprints/http.ts
@jstarpl jstarpl force-pushed the contrib/fix/improveBlueprintAssetHandling branch from 6b956dc to 87ca6b1 Compare April 14, 2026 10:06
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

♻️ Duplicate comments (1)
meteor/server/api/blueprints/http.ts (1)

203-213: ⚠️ Potential issue | 🟠 Major

Use correct status codes for traversal and unexpected failures.

Line 212 returning 501 is semantically wrong here; retrieval runtime failures should be 500, and traversal attempts (Line 207-209) should be 403. Also, unexpected failures should log with logger.error to preserve incident visibility.

Suggested patch
 		} catch (e) {
 			if (e instanceof Error && 'code' in e && e.code === 'ENOENT') {
 				logger.warn('Blueprint asset not found: ' + e)
 				ctx.statusCode = 404
 			} else if (e instanceof Error && e.message.includes('outside of asset storage path')) {
 				logger.warn('Blueprint asset path traversal attempt: ' + e)
-				ctx.statusCode = 400
+				ctx.statusCode = 403
 			} else {
-				logger.warn('Blueprint asset retrieval failed: ' + e)
-				ctx.statusCode = 501
+				logger.error('Blueprint asset retrieval failed: ' + e)
+				ctx.statusCode = 500
 			}
 		}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@meteor/server/api/blueprints/http.ts` around lines 203 - 213, In the catch
block in meteor/server/api/blueprints/http.ts that handles blueprint asset
retrieval (the try/catch around asset reads), change the response codes and
logging as follows: keep the ENOENT branch returning 404 with logger.warn,
change the "outside of asset storage path" branch (path traversal) to set
ctx.statusCode = 403 and use logger.warn, and change the final fallback branch
to set ctx.statusCode = 500 and call logger.error instead of logger.warn so
unexpected runtime failures are logged as errors.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@meteor/server/api/blueprints/http.ts`:
- Around line 203-213: In the catch block in
meteor/server/api/blueprints/http.ts that handles blueprint asset retrieval (the
try/catch around asset reads), change the response codes and logging as follows:
keep the ENOENT branch returning 404 with logger.warn, change the "outside of
asset storage path" branch (path traversal) to set ctx.statusCode = 403 and use
logger.warn, and change the final fallback branch to set ctx.statusCode = 500
and call logger.error instead of logger.warn so unexpected runtime failures are
logged as errors.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: eb595e1b-a934-4ee6-846b-18ba69f039d7

📥 Commits

Reviewing files that changed from the base of the PR and between 6b956dc and 87ca6b1.

📒 Files selected for processing (4)
  • meteor/server/api/blueprints/api.ts
  • meteor/server/api/blueprints/http.ts
  • packages/webui/src/client/lib/Components/BlueprintAssetIcon.tsx
  • packages/webui/src/client/ui/PreviewPopUp/PreviewPopUpContext.tsx
✅ Files skipped from review due to trivial changes (1)
  • packages/webui/src/client/ui/PreviewPopUp/PreviewPopUpContext.tsx
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/webui/src/client/lib/Components/BlueprintAssetIcon.tsx
  • meteor/server/api/blueprints/api.ts

@jstarpl jstarpl force-pushed the contrib/fix/improveBlueprintAssetHandling branch from 87ca6b1 to 620ad0a Compare April 14, 2026 10:23
@jstarpl jstarpl force-pushed the contrib/fix/improveBlueprintAssetHandling branch from 08aeebd to e9300e5 Compare April 14, 2026 11:22
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

♻️ Duplicate comments (1)
meteor/server/api/blueprints/api.ts (1)

115-119: ⚠️ Potential issue | 🟠 Major

startsWith() still doesn't stop symlink escapes.

This only constrains the lexical path. If a symlink already exists anywhere under assets/, a fileId like linked-dir/file.png still passes the prefix check while writeFile/createReadStream follow the symlink outside the asset root. Please canonicalize with realpath and compare with path.relative(...) against the canonical asset directory before reading or writing.

Also applies to: 134-138

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@meteor/server/api/blueprints/api.ts` around lines 115 - 119, The current
prefix check using assetsDir and assetPath can be bypassed by symlinks; replace
the lexical check in the asset lookup/write paths (references: assetsDir,
assetPath, fileId) with a canonicalization-based check: resolve the real paths
using fs.promises.realpath (or fs.realpathSync) for both the assets directory
and the target asset path, then compute path.relative(realAssetsDir,
realAssetPath) and throw if the result starts with '..' or is equal to '..' (or
if the relative path is absolute), ensuring you perform this canonical check in
both places that currently use startsWith (the asset read path and the asset
write path).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@meteor/server/api/blueprints/api.ts`:
- Around line 115-119: The current prefix check using assetsDir and assetPath
can be bypassed by symlinks; replace the lexical check in the asset lookup/write
paths (references: assetsDir, assetPath, fileId) with a canonicalization-based
check: resolve the real paths using fs.promises.realpath (or fs.realpathSync)
for both the assets directory and the target asset path, then compute
path.relative(realAssetsDir, realAssetPath) and throw if the result starts with
'..' or is equal to '..' (or if the relative path is absolute), ensuring you
perform this canonical check in both places that currently use startsWith (the
asset read path and the asset write path).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 0d245aff-507f-4ff0-85ba-b6f9cbfab1fd

📥 Commits

Reviewing files that changed from the base of the PR and between 87ca6b1 and 08aeebd.

📒 Files selected for processing (6)
  • meteor/server/api/blueprints/__tests__/api.test.ts
  • meteor/server/api/blueprints/__tests__/http.test.ts
  • meteor/server/api/blueprints/api.ts
  • meteor/server/api/blueprints/http.ts
  • packages/webui/src/client/lib/Components/BlueprintAssetIcon.tsx
  • packages/webui/src/client/ui/PreviewPopUp/PreviewPopUpContext.tsx
✅ Files skipped from review due to trivial changes (1)
  • packages/webui/src/client/lib/Components/BlueprintAssetIcon.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/webui/src/client/ui/PreviewPopUp/PreviewPopUpContext.tsx

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@meteor/server/api/blueprints/http.ts`:
- Around line 203-213: The try/catch can't catch async stream errors from
retrieveBlueprintAsset (which calls createReadStream), so change the response
flow to wait for the stream to open and handle its 'error' event before
assigning to ctx.body: call retrieveBlueprintAsset to get the ReadStream, attach
a one-time 'error' handler that maps ENOENT to ctx.statusCode=404,
path-traversal messages to 400 and other errors to 500, and attach a one-time
'open' handler (or use stream.promises/stream.finished) to only set ctx.body =
stream once the stream is ready; also remove reliance on synchronous exceptions
in the try/catch around retrieveBlueprintAsset and ensure any stream errors
clean up the stream and don't leak unhandled exceptions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e6cc3915-3575-4d7b-9807-fb610fdf3b54

📥 Commits

Reviewing files that changed from the base of the PR and between 08aeebd and 3884ef9.

📒 Files selected for processing (5)
  • meteor/server/api/blueprints/__tests__/api.test.ts
  • meteor/server/api/blueprints/__tests__/http.test.ts
  • meteor/server/api/blueprints/api.ts
  • meteor/server/api/blueprints/http.ts
  • packages/webui/src/client/lib/Components/BlueprintAssetIcon.tsx
🚧 Files skipped from review as they are similar to previous changes (4)
  • packages/webui/src/client/lib/Components/BlueprintAssetIcon.tsx
  • meteor/server/api/blueprints/api.ts
  • meteor/server/api/blueprints/tests/http.test.ts
  • meteor/server/api/blueprints/tests/api.test.ts

Comment thread meteor/server/api/blueprints/http.ts
@jstarpl jstarpl requested a review from nytamin May 14, 2026 12:39
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@meteor/server/api/blueprints/http.ts`:
- Around line 187-188: The current file extension check on variable filePath in
the HTTP blueprint accepts a trailing dot; update the if condition that uses
filePath.match(...) so it requires a full extension (e.g. use a regex that
enforces a dot followed by one of the allowed extensions and anchors the end,
and make it case-insensitive). Locate the check around the image-only gate (the
filePath variable and the if that currently contains /\.(png|svg|gif)?$/) and
replace the pattern with a strict pattern that does not allow a bare trailing
dot (for example, use a non-capturing group like (?:png|svg|gif) with
end-of-string anchor and /i flag).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 71831444-9c78-4125-8994-5e3ba16d1e7f

📥 Commits

Reviewing files that changed from the base of the PR and between 3884ef9 and b526634.

📒 Files selected for processing (2)
  • meteor/server/api/blueprints/api.ts
  • meteor/server/api/blueprints/http.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • meteor/server/api/blueprints/api.ts

Comment on lines +187 to 188
const filePath = ctx.params.fileId
if (filePath.match(/\.(png|svg|gif)?$/)) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Require an actual allowed extension here.

/\.(png|svg|gif)?$/ still accepts a bare trailing dot, so /assets/foo. passes the image-only gate. Tighten this to require one of the allowed extensions.

Suggested fix
-	if (filePath.match(/\.(png|svg|gif)?$/)) {
+	if (/\.(png|svg|gif)$/.test(filePath)) {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const filePath = ctx.params.fileId
if (filePath.match(/\.(png|svg|gif)?$/)) {
const filePath = ctx.params.fileId
if (/\.(png|svg|gif)$/.test(filePath)) {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@meteor/server/api/blueprints/http.ts` around lines 187 - 188, The current
file extension check on variable filePath in the HTTP blueprint accepts a
trailing dot; update the if condition that uses filePath.match(...) so it
requires a full extension (e.g. use a regex that enforces a dot followed by one
of the allowed extensions and anchors the end, and make it case-insensitive).
Locate the check around the image-only gate (the filePath variable and the if
that currently contains /\.(png|svg|gif)?$/) and replace the pattern with a
strict pattern that does not allow a bare trailing dot (for example, use a
non-capturing group like (?:png|svg|gif) with end-of-string anchor and /i flag).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Contribution from NRK Contributions sponsored by NRK (nrk.no)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants