Skip to content

fix: harden MCP and supply chain security - #6

Merged
aditzel merged 3 commits into
mainfrom
security/harden-mcp-supply-chain
Jun 10, 2026
Merged

fix: harden MCP and supply chain security#6
aditzel merged 3 commits into
mainfrom
security/harden-mcp-supply-chain

Conversation

@aditzel

@aditzel aditzel commented Jun 9, 2026

Copy link
Copy Markdown
Owner

Summary

  • harden MCP image inputs/outputs with local-file opt-in, root confinement, safe filenames, and no-overwrite writes
  • harden output downloads against SSRF/DNS rebinding with public-IP validation, pinned lookups, redirect revalidation, size/type checks, and body timeouts
  • harden custom API endpoint handling to reduce credential exfiltration risk, including HTTPS-by-default and explicit opt-ins for custom/non-Wavespeed bases
  • refresh bun.lock, add Dependabot, pin CI/release toolchains/actions, use frozen installs, and enable npm provenance
  • document MCP safety defaults and add/expand regression coverage

Validation

  • bun run lint
  • bun test — 102 pass, 4 skipped
  • bun run build
  • bun audit --json — 0 advisories
  • npm pack --dry-run --json — no suspicious env/token/config files
  • git diff --check
  • CodeRabbit review returned 0 findings before final minor test/workflow additions; final rerun was rate-limited
  • security/quality subagent revalidation approved with no blockers

Notes

  • This PR also includes the existing README/package/skill documentation updates that were part of the current working tree.
  • Minimal branch protection was enabled on main to block force-pushes and deletions.

Summary by CodeRabbit

  • New Features

    • MCP server support and a public skill for image generation/editing; improved image handling and safer image saving.
  • Bug Fixes

    • Safer endpoint/request ID handling and URL encoding/validation; added "succeeded" job status.
  • Documentation

    • Major README updates for MCP usage, model selection, and CLI; removed obsolete WARP docs.
  • Chores

    • CI/CD/workflow pinning, Dependabot config, and package/release metadata updates.
  • Tests

    • Expanded test coverage for model resolution, image handling, and MCP formatting.

@coderabbitai

coderabbitai Bot commented Jun 9, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@aditzel, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 43 minutes and 51 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 5a79be0a-fd4f-4c23-ba75-4cb1a1891db0

📥 Commits

Reviewing files that changed from the base of the PR and between 9e5bcea and 2f5580c.

📒 Files selected for processing (2)
  • src/utils/images.ts
  • tests/utils/images.test.ts

Walkthrough

Adds MCP-first behavior and Wavespeed-focused security: debug logging, model-path and request-id encoding/validation, API base URL hostname and network checks, robust image I/O (secure downloads, local-file support, per-index save results), MCP tool wiring/output root options, tests, docs, and CI/workflow pinning.

Changes

Wavespeed MCP Platform with Security Hardening

Layer / File(s) Summary
Debug logging infrastructure
src/utils/logging.ts
New utilities for conditional debug output, message truncation, and URL credential redaction replace ad-hoc console.error calls throughout the codebase.
API model path and request ID validation
src/api/types.ts, src/api/client.ts, tests/api/client.test.ts
encodeModelPath and encodeRequestId prevent directory traversal and special-character injection in API paths. JobStatus gains "succeeded" state. HTTP interactions use debug logging for redacted URLs and response status.
API base URL security validation
src/config/models.ts, tests/config/models.test.ts
Enforces HTTPS/HTTP schemes, blocks localhost and private networks by default, rejects credentials and query/fragments, restricts API key usage to Wavespeed hosts unless environment flags allow overrides. Comprehensive test coverage for edge cases (IPv6-mapped loopback, insecure HTTP).
Image utilities with secure downloads and local file support
src/utils/images.ts, tests/utils/images.test.ts
Completely rewritten: base64/data-URI decoding with MIME validation, secure HTTPS downloads with DNS pinning and private-network blocking, local file reading with symlink rejection and path confinement, unique non-clobbering output writes. saveImagesFromOutputs now returns per-index success tracking and detailed failure reasons. Tests use local HTTP server for URL validation.
Image list validation with configurable local file support
src/utils/validation.ts, tests/utils/validation.test.ts
parseImagesList accepts options to enable local files, configure read root directory, and enforce per-file size limits. Normalizes arrays and comma-separated strings (including embedded data URIs), validates URLs/data URIs/base64, and conditionally converts local files to data URIs with path/size enforcement.
MCP output formatting with configurable image saving
src/core/output-formatter.ts, tests/core/output-formatter.test.ts
formatForMCP accepts saveOptions controlling output root and download/read restrictions, passes them to saveImagesFromOutputs, and tracks per-index success. Tests validate index preservation when outputs fail to save.
MCP tools with debug logging and configurable I/O
src/commands/mcp.ts, src/mcp/tools.ts
All image tools use debugLog for lifecycle events. getMcpOutputRoot() and getMcpImageParseOptions() enable WAVESPEED_MCP_OUTPUT_DIR and WAVESPEED_MCP_INPUT_DIR configuration. Image parsing now supports local files; formatForMCP receives outputRoot option. MCP mode flag WAVESPEED_MCP_MODE=1 is set at server startup.
Core operations and polling using debug logging
src/core/operations.ts, src/utils/polling.ts
All async operations and polling lifecycle events switch from console.error to structured debugLog calls for submission, completion, failure, and exception reporting.
Documentation and packaging updates
README.md, skills/wavespeed-image-generation/SKILL.md, package.json, CHANGELOG.md
README updated with MCP-first Quick Start, Features list, new "Agent Skill and MCP Usage" section describing tools and safety rules, custom endpoint safety documentation, and model discovery via wavespeed models. New SKILL.md defines MCP workflows, CLI fallback, and troubleshooting. package.json reflects MCP/Wavespeed positioning, includes skills in published files, adds provenance flag, and defines release script. Changelog notes model selection improvement.
CI/CD hardening and configuration
.github/workflows/release.yml, .github/workflows/test.yml, .github/workflows/junie.yml, .github/dependabot.yml, .releaserc.json, tests/commands/cli.test.ts, .gitignore
Pins GitHub Actions and Bun to specific versions/SHAs, uses bun install --frozen-lockfile, tightens workflow permissions, removes skip-CI conditionals, adds Dependabot config for npm/GitHub Actions, updates release command to bun run release, removes [skip ci] tag, and expands .gitignore for environment and tool-specific configs. CLI tests now include flags for custom/insecure API base URLs.

Possibly Related PRs

  • aditzel/wavespeed-cli#3: Introduces buildSubmitTarget function; this PR hardens it with URL-encoding and validation of model path segments.
  • aditzel/wavespeed-cli#5: Updates shared model-routing and payload-building utilities that this PR extends with enhanced API base URL validation and security controls.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 36.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
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix: harden MCP and supply chain security' clearly and concisely describes the primary changes across the PR, which focus on hardening MCP (Model Context Protocol) and supply chain security through input validation, secure downloads, custom API endpoint handling, and toolchain improvements.
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
  • Commit unit tests in branch security/harden-mcp-supply-chain

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.

@socket-security

socket-security Bot commented Jun 9, 2026

Copy link
Copy Markdown

@socket-security

socket-security Bot commented Jun 9, 2026

Copy link
Copy Markdown

Warning

Review the following alerts detected in dependencies.

According to your organization's Security Policy, it is recommended to resolve "Warn" alerts. Learn more about Socket for GitHub.

Action Severity Alert  (click "▶" to expand/collapse)
Warn High
Obfuscated code: npm npm is 90.0% likely obfuscated

Confidence: 0.90

Location: Package overview

From: ?npm/@semantic-release/npm@13.1.5npm/npm@11.16.0

ℹ Read more on: This package | This alert | What is obfuscated code?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Packages should not obfuscate their code. Consider not using packages with obfuscated code.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/npm@11.16.0. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Warn High
Obfuscated code: npm npm is 90.0% likely obfuscated

Confidence: 0.90

Location: Package overview

From: ?npm/@semantic-release/npm@13.1.5npm/npm@11.16.0

ℹ Read more on: This package | This alert | What is obfuscated code?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Packages should not obfuscate their code. Consider not using packages with obfuscated code.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/npm@11.16.0. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

View full report

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5696cc1928

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/utils/images.ts Outdated
filePath: string,
options: LocalImageReadOptions = {},
): Promise<string> {
const info = await lstat(filePath);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Resolve relative MCP input paths under the configured root

When WAVESPEED_MCP_INPUT_DIR is set and an MCP client supplies a relative image path such as foo.png, this lstat(filePath) still checks the server's current working directory before the later root containment check. As a result, files that exist under the configured input root are reported as missing unless the server happens to be started from that same directory, which breaks the advertised “allow local image reads only under that directory” mode for relative paths. Resolve relative paths against options.rootDir before stat/realpath validation.

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai 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.

Actionable comments posted: 7

🧹 Nitpick comments (2)
tests/utils/images.test.ts (2)

123-131: ⚡ Quick win

Tighten mock-server route matching to avoid false positives.

Line 123 uses substring checks, so /invalid.png would be treated as valid and could hide regressions in failure-path tests.

Proposed diff
-      server = createServer(async (req, res) => {
-        if (req.url?.includes("valid") || req.url?.includes("image")) {
+      server = createServer(async (req, res) => {
+        const pathname = new URL(req.url ?? "/", "http://127.0.0.1").pathname;
+        if (pathname === "/valid.png" || pathname === "/image1.png" || pathname === "/image2.png") {
           const pngData = await readFile(testImagePath);
           res.writeHead(200, { "content-type": "image/png" });
           res.end(pngData);
           return;
         }
🤖 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 `@tests/utils/images.test.ts` around lines 123 - 131, The mock HTTP handler in
tests/utils/images.test.ts is matching routes with substring checks
(req.url?.includes("valid") || req.url?.includes("image")), which causes false
positives like "/invalid.png"; change the route matching to strict checks (e.g.,
exact path comparison or anchored regex) against the known test route(s) that
should return the image (reference the request handler using req.url and the
testImagePath variable) so only the intended URL(s) trigger the 200 PNG response
and all other URLs fall through to the 404 response.

211-213: ⚡ Quick win

Use an explicit absolute outside path for output-root rejection.

Line 212 currently depends on "../outside" resolution relative to process CWD. Making it explicitly absolute keeps this test deterministic across runners.

Proposed diff
     it("should reject output directories outside a configured root", async () => {
       const base64 = await convertFileToBase64(testImagePath);
+      const outsideDir = path.resolve(outputDir, "..", "outside");
       await expect(
-        saveImagesFromOutputs([base64], "../outside", "task", { outputRoot: outputDir }),
+        saveImagesFromOutputs([base64], outsideDir, "task", { outputRoot: outputDir }),
       ).rejects.toThrow("Output directory must stay within configured output root");
     });
🤖 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 `@tests/utils/images.test.ts` around lines 211 - 213, Update the test that
calls saveImagesFromOutputs to pass an explicit absolute "outside" path instead
of the relative "../outside" so the assertion is deterministic across runners;
construct the outside path with Node's path.resolve using the test's outputDir
(e.g. path.resolve(outputDir, '..', 'outside')) and pass that resolved absolute
path to saveImagesFromOutputs while keeping the same expectation about
rejection.
🤖 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 @.github/workflows/release.yml:
- Around line 23-24: Add persist-credentials: false to both actions/checkout
usages to prevent the GITHUB_TOKEN from being automatically persisted to the
workspace; specifically update the checkout step in the test job (the
actions/checkout@... step referenced as the "Checkout code" step) and the
checkout step in the release job (the actions/checkout@... step that runs before
steps requiring write/id-token permissions) by adding the persist-credentials:
false input so the token with contents: write / id-token: write is not left in
the working directory.

In @.github/workflows/test.yml:
- Around line 16-17: The checkout steps using actions/checkout (e.g., the step
with "uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5" and the
second checkout invocation later in the workflow) must include "with:
persist-credentials: false" so the runner does not persist GITHUB_TOKEN into git
config; update both checkout steps to add a "with" block containing
persist-credentials: false.

In `@src/config/models.ts`:
- Around line 126-136: The hostname check in isLocalOrPrivateHost doesn't
canonicalize trailing dots nor checks resolved A/AAAA records; update
isLocalOrPrivateHost to strip and normalize trailing dots (e.g., remove a final
"." after toLowerCase()), then perform a DNS lookup (resolve4/resolve6 or
resolve) for the hostname and run net.isIP + isPrivateIPv4/isPrivateIPv6 against
each resolved address; retain the existing literal checks for "localhost" and
".localhost" but ensure the function returns true if any resolved IP is
private/loopback. Use the same logic where similar checks appear (lines
referenced around 163-168) so both literal and resolved addresses are validated
before allowing the host.

In `@src/utils/images.ts`:
- Around line 186-193: The save helpers (e.g., saveBase64Image) currently force
a .png filename but the decoded image bytes include the real MIME type; update
saveBase64Image to derive the file extension from the detected MIME (use the
MIME returned by decodeBase64Image or otherwise detect it) — map
image/jpeg→.jpg, image/png→.png, image/gif→.gif, image/webp→.webp,
image/bmp→.bmp — and pass a dest filename with that extension into
writeUniqueFile instead of hardcoding .png; apply the same change to the other
save helpers referenced (the save functions around the other blocks noted) so
filenames match the actual image MIME.
- Around line 516-523: The code that checks normalizedContentType (using
getHeader and redactUrl) throws an Error when the response is not an image but
does not consume or destroy res.body, which can leak sockets; before throwing
the Error for non-image content types, ensure you tear down the response body
the same way the non-2xx branch does (e.g., call
res.body.cancel()/destroy()/read and discard the stream) so the socket is
released, then throw the Error including redactUrl(url).

In `@src/utils/logging.ts`:
- Around line 21-41: The fallback path in redactUrl currently only strips query
strings and can leak credentials for malformed URLs; update the catch block of
redactUrl to also detect and redact credentials in the authority portion before
returning (e.g., remove or replace any "username:password@" segment), then
proceed to redact the query string as now; target the redactUrl function to
perform a regex-based or string-based replacement of credentials (look for the
pattern "//...@") prior to slicing/returning so malformed URLs cannot expose
user/password in logs.

In `@src/utils/validation.ts`:
- Around line 49-78: The guard that decides to run the data-URI splitting uses a
lowercase-only substring check (if (s.includes("data:image/"))), so uppercase or
mixed-case data URIs are missed; change that guard to be case-insensitive (e.g.,
use a case-insensitive regex like /data:image\//i.test(s) or compare
s.toLowerCase().includes("data:image/")) so the existing case-insensitive
dataUriPattern and the parsing logic around s.matchAll, items, and
isDataUriImage will correctly detect and split DATA URIs regardless of case.

---

Nitpick comments:
In `@tests/utils/images.test.ts`:
- Around line 123-131: The mock HTTP handler in tests/utils/images.test.ts is
matching routes with substring checks (req.url?.includes("valid") ||
req.url?.includes("image")), which causes false positives like "/invalid.png";
change the route matching to strict checks (e.g., exact path comparison or
anchored regex) against the known test route(s) that should return the image
(reference the request handler using req.url and the testImagePath variable) so
only the intended URL(s) trigger the 200 PNG response and all other URLs fall
through to the 404 response.
- Around line 211-213: Update the test that calls saveImagesFromOutputs to pass
an explicit absolute "outside" path instead of the relative "../outside" so the
assertion is deterministic across runners; construct the outside path with
Node's path.resolve using the test's outputDir (e.g. path.resolve(outputDir,
'..', 'outside')) and pass that resolved absolute path to saveImagesFromOutputs
while keeping the same expectation about rejection.
🪄 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: 4cc94912-e5b1-4485-b3ac-1ff37528eac9

📥 Commits

Reviewing files that changed from the base of the PR and between 21eca7a and 5696cc1.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (29)
  • .github/dependabot.yml
  • .github/workflows/junie.yml
  • .github/workflows/release.yml
  • .github/workflows/test.yml
  • .gitignore
  • .releaserc.json
  • CHANGELOG.md
  • README.md
  • WARP.md
  • package.json
  • skills/wavespeed-image-generation/SKILL.md
  • src/api/client.ts
  • src/api/types.ts
  • src/commands/mcp.ts
  • src/commands/models.ts
  • src/config/models.ts
  • src/core/operations.ts
  • src/core/output-formatter.ts
  • src/mcp/tools.ts
  • src/utils/images.ts
  • src/utils/logging.ts
  • src/utils/polling.ts
  • src/utils/validation.ts
  • tests/api/client.test.ts
  • tests/commands/cli.test.ts
  • tests/config/models.test.ts
  • tests/core/output-formatter.test.ts
  • tests/utils/images.test.ts
  • tests/utils/validation.test.ts
💤 Files with no reviewable changes (1)
  • WARP.md

Comment thread .github/workflows/release.yml
Comment thread .github/workflows/test.yml
Comment thread src/config/models.ts
Comment thread src/utils/images.ts
Comment thread src/utils/images.ts
Comment thread src/utils/logging.ts
Comment thread src/utils/validation.ts Outdated

@coderabbitai coderabbitai 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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/utils/images.ts (2)

657-665: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Validate outputRoot before creating directories.

ensureOutputDir(resolvedOutputDir) on Line 658 can already create directories through a symlinked intermediate component before the realpath check on Lines 659-665 runs. With a pre-existing symlink under the configured root, this mutates the filesystem outside outputRoot and only fails afterwards.

Please move the confinement validation ahead of mkdir by resolving/checking the nearest existing ancestor first, then keep the post-create realpath check as a defense-in-depth revalidation.

🤖 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 `@src/utils/images.ts` around lines 657 - 665, The current flow calls
ensureOutputDir(resolvedOutputDir) before verifying confinement, which can
create directories through symlinks; change the order in the block that uses
resolveOutputDir/resolvedOutputDir so you first validate that the nearest
existing ancestor of resolvedOutputDir is within options.outputRoot by calling
realpath on path.resolve(options.outputRoot) and realpath on the nearest
existing parent (walk up from resolvedOutputDir until fs.existsSync or similar),
use isSubpath(outputRealAncestor, rootRealPath) to enforce the confinement, and
only then call ensureOutputDir(resolvedOutputDir); keep the existing post-create
realpath/isSubpath check as a defense-in-depth revalidation.

566-583: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Reject out-of-root inputs before lstat.

Line 570 touches candidatePath before the rootDir confinement check on Lines 584-589. That means absolute paths or .. escapes can still probe file existence/type/size outside the allowed root via different errors, even though the read is later rejected.

Suggested fix
 async function resolveValidatedLocalImagePath(
   filePath: string,
   options: LocalImageReadOptions = {},
 ): Promise<string> {
+  const root = options.rootDir ? await realpath(options.rootDir) : undefined;
   const candidatePath =
     options.rootDir && !path.isAbsolute(filePath)
       ? path.resolve(options.rootDir, filePath)
       : filePath;
+
+  if (root && !isSubpath(path.resolve(candidatePath), root)) {
+    throw new Error(`Image file must stay within configured input root: ${root}`);
+  }
+
   const info = await lstat(candidatePath);
   if (!info.isFile()) {
     throw new Error(`Image path is not a regular file: ${filePath}`);
   }
@@
   const resolvedPath = await realpath(candidatePath);
-  if (options.rootDir) {
-    const root = await realpath(options.rootDir);
+  if (root) {
     if (!isSubpath(resolvedPath, root)) {
       throw new Error(`Image file must stay within configured input root: ${root}`);
     }
   }
🤖 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 `@src/utils/images.ts` around lines 566 - 583, The code currently calls lstat
on candidatePath before enforcing the rootDir confinement; move or add an early
check so any input outside options.rootDir is rejected before filesystem
probing. Specifically, when options.rootDir is set, compute the absolute
candidatePath (using path.resolve) and then use path.relative(options.rootDir,
candidatePath) (or equivalent) to detect escapes (reject when the relative path
starts with '..' or is absolute) and throw an error if outside the root; only
after that safe containment check call lstat, realpath, etc. Reference symbols:
candidatePath, options.rootDir, path.resolve, path.relative, lstat, realpath.
🤖 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.

Outside diff comments:
In `@src/utils/images.ts`:
- Around line 657-665: The current flow calls ensureOutputDir(resolvedOutputDir)
before verifying confinement, which can create directories through symlinks;
change the order in the block that uses resolveOutputDir/resolvedOutputDir so
you first validate that the nearest existing ancestor of resolvedOutputDir is
within options.outputRoot by calling realpath on
path.resolve(options.outputRoot) and realpath on the nearest existing parent
(walk up from resolvedOutputDir until fs.existsSync or similar), use
isSubpath(outputRealAncestor, rootRealPath) to enforce the confinement, and only
then call ensureOutputDir(resolvedOutputDir); keep the existing post-create
realpath/isSubpath check as a defense-in-depth revalidation.
- Around line 566-583: The code currently calls lstat on candidatePath before
enforcing the rootDir confinement; move or add an early check so any input
outside options.rootDir is rejected before filesystem probing. Specifically,
when options.rootDir is set, compute the absolute candidatePath (using
path.resolve) and then use path.relative(options.rootDir, candidatePath) (or
equivalent) to detect escapes (reject when the relative path starts with '..' or
is absolute) and throw an error if outside the root; only after that safe
containment check call lstat, realpath, etc. Reference symbols: candidatePath,
options.rootDir, path.resolve, path.relative, lstat, realpath.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a66055da-664b-4955-b331-b1d328540072

📥 Commits

Reviewing files that changed from the base of the PR and between 5696cc1 and 9e5bcea.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (10)
  • .github/workflows/release.yml
  • .github/workflows/test.yml
  • package.json
  • src/config/models.ts
  • src/utils/images.ts
  • src/utils/logging.ts
  • src/utils/validation.ts
  • tests/config/models.test.ts
  • tests/utils/images.test.ts
  • tests/utils/validation.test.ts
🚧 Files skipped from review as they are similar to previous changes (9)
  • tests/utils/validation.test.ts
  • tests/utils/images.test.ts
  • package.json
  • tests/config/models.test.ts
  • src/utils/logging.ts
  • .github/workflows/test.yml
  • src/utils/validation.ts
  • src/config/models.ts
  • .github/workflows/release.yml

@aditzel
aditzel merged commit 956a305 into main Jun 10, 2026
5 checks passed
@aditzel
aditzel deleted the security/harden-mcp-supply-chain branch June 10, 2026 02:53
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