Skip to content

fix(transfer): sanitize download extension from untrusted ?filename= param (BE-3326)#548

Open
mattmillerai wants to merge 2 commits into
mainfrom
matt/be-3326-sanitize-download-ext
Open

fix(transfer): sanitize download extension from untrusted ?filename= param (BE-3326)#548
mattmillerai wants to merge 2 commits into
mainfrom
matt/be-3326-sanitize-download-ext

Conversation

@mattmillerai

Copy link
Copy Markdown
Collaborator

ELI-5

When comfy download saves a file, it guesses the file extension from the
name the server hands back (a ?filename=... query param). That name is
untrusted — a malicious or compromised server can stuff invisible control /
ANSI escape bytes into it. Those bytes were copied straight into the saved
filename and into the path we print to the terminal, so printing the result
in human mode could let the server inject terminal escape sequences. This PR
scrubs the extension through a small whitelist so only safe characters
survive.

What changed

  • Added _sanitize_ext next to the existing _sanitize_item_name in
    transfer.py. It whitelists the extension to [A-Za-z0-9._-], dropping
    everything else (control bytes, ANSI escapes, etc.).
  • Applied it to ext in both derivation branches of the download loop —
    the local-source branch (local_source.suffix) and the query-param branch
    (Path(remote_name).suffix). The existing or ".png" fallback is kept, so
    an extension that sanitizes to empty still degrades to .png.
  • Added tests pinning: (1) a control-byte filename
    (?filename=out.png\x1b[31mHACK) yields a clean .png-family extension
    with no control bytes in the resulting local name or echoed path —
    covered for both the query-param and local-source branches; (2) the
    existing no-directory-traversal behavior is preserved.

Why this is its own PR (not part of #531)

This naming/ext block is byte-identical to origin/main, and #531 (BE-3273)
is a deliberately pure mechanical extraction whose contract is that helper
bodies are verbatim with unchanged runtime behavior. Sanitizing ext is a
real behavior change, so it belongs in its own small, testable diff against
the pre-existing issue on main. (BE-3326, deferred from a #531 review thread.)

Verification

  • New tests fail without the fix (control-byte extension leaks through) and
    pass with it; the no-traversal test passes both ways (regression guard).
  • Full tests/comfy_cli/command/ suite: 1160 passed, 2 pre-existing skips.
  • ruff check + ruff format --check clean on both changed files.

Judgment call

The sanitizer whitelists rather than truncating. For the attack input
out.png\x1b[31mHACK, the on-disk extension becomes .png31mHACK — clean,
png-family, and free of control bytes — not a bare .png. This follows the
ticket's proposed whitelist approach and avoids a more invasive
"truncate the extension at the first junk byte" behavior change; the
security-relevant property (no control/ANSI bytes reach the filename or the
echoed path) is fully enforced.

Not applicable

Negative-claim / capability-denial falsification: this change adds no
"not supported" / dead-end path — it only sanitizes a string — so that check
does not apply.

…param (BE-3326)

The download extension was taken straight from the untrusted `filename`
query param via `Path(remote_name).suffix` and embedded into the on-disk
name and echoed path. Unlike the `item` token, `ext` was never sanitized,
so a malicious server returning `?filename=out.png` plus control/ANSI
bytes could inject into the terminal when the path is printed in human mode.

Add `_sanitize_ext` next to `_sanitize_item_name` and apply it to `ext` in
both the local-source and query-param branches, whitelisting to a safe
extension charset. Directory traversal remains impossible (Path.suffix
drops path components). Tests pin both the control-byte scrub and the
preserved no-traversal behavior.
@mattmillerai mattmillerai added agent-coded PR authored by the agent-work loop cursor-review Request Cursor bot review labels Jul 17, 2026
@coderabbitai

coderabbitai Bot commented Jul 17, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 49 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

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.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: c8538417-7b02-4ef5-a638-3b3e53e2c8e4

📥 Commits

Reviewing files that changed from the base of the PR and between 7c5b63c and ff17cd9.

📒 Files selected for processing (2)
  • comfy_cli/command/transfer.py
  • tests/comfy_cli/command/test_transfer_download.py
📝 Walkthrough

Walkthrough

Download extension handling now sanitizes local and remote filename-derived suffixes. Tests cover control-byte removal, fallback naming, reported paths, and traversal-shaped filenames.

Changes

Download Extension Safety

Layer / File(s) Summary
Sanitize extensions during download
comfy_cli/command/transfer.py
Adds _sanitize_ext and applies it to extensions derived from local sources and remote filename parameters before the existing .png fallback.
Validate sanitized output paths
tests/comfy_cli/command/test_transfer_download.py
Adds end-to-end tests for control-byte removal, safe output paths, fallback naming, and local-source sanitization.

Suggested reviewers: bigcat88

🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
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.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch matt/be-3326-sanitize-download-ext
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch matt/be-3326-sanitize-download-ext

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

@mattmillerai
mattmillerai marked this pull request as ready for review July 17, 2026 15:35
@dosubot dosubot Bot added the size:S This PR changes 10-29 lines, ignoring generated files. label Jul 17, 2026

@github-actions github-actions 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.

🔍 Cursor Review — Consolidated panel

Triggered by @mattmillerai.

Found 3 finding(s).

Severity Count
🟡 Medium 2
🟢 Low 1

Panel: 6/8 reviewers contributed findings.

Reviewers that did not contribute: kimi-k2.5:adversarial (empty), kimi-k2.5:edge-case (empty)

Comment thread comfy_cli/command/transfer.py Outdated
Comment thread comfy_cli/command/transfer.py
Comment thread tests/comfy_cli/command/test_transfer_download.py Outdated
@coderabbitai
coderabbitai Bot requested a review from bigcat88 July 17, 2026 15:36
Address three cursor-review findings on the download-extension sanitizer:

- Collapse an extensionless suffix (`.💥`, `.日本語`, lone control byte, or
  bare `.`) to "" so the caller's `or ".png"` fallback applies instead of a
  truthy bare-dot result that writes `<id>_000.` — invalid/normalized on
  Windows and desyncing the reported path from the on-disk name.
- Cap the whitelisted extension length (_MAX_EXT_LEN=20) so a hostile
  `?filename=out.<thousands of chars>` can't push local_name past NAME_MAX and
  raise OSError(ENAMETOOLONG) outside the NDJSON error contract.
- Assert the exact sanitized name in the control-byte tests (not a `.png`
  prefix) so a regression leaking control bytes after `.png` is caught; add
  direct unit tests for the empty-collapse and length-cap behaviors.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@dosubot dosubot Bot added size:M This PR changes 30-99 lines, ignoring generated files. and removed size:S This PR changes 10-29 lines, ignoring generated files. labels Jul 17, 2026
@mattmillerai

Copy link
Copy Markdown
Collaborator Author

🤖 The reviews loop filed Linear follow-up ticket(s) for review thread(s) deferred as out of scope for this PR:

@mattmillerai

Copy link
Copy Markdown
Collaborator Author

🤖 Reviews-loop pass: all 3 Cursor findings are resolved (fixed in ff17cd9, present in the current diff) and CodeRabbit passed with no actionable comments. The PR merges cleanly (no conflict).

The two red CI checks are pre-existing, infra/dependency failures unrelated to this PR (which only touches comfy_cli/command/transfer.py):

  • build — 9 failures with ValueError: Comment cannot contain line breaks at comfy_cli/registry/config_parser.py:75, caused by a newer tomlkit (0.15.1) rejecting the multiline .comment(...) call. That file is byte-identical to main. Filed as an agent-ok follow-up.
  • test (Windows)ImportError: cannot import name '__version__' from 'pydantic_core' during Install Dependencies. Already tracked as BE-3327.

Transfer tests pass locally (77 passed, 1 skipped).

@mattmillerai

Copy link
Copy Markdown
Collaborator Author

↑ Correction: the build (tomlkit config_parser.py:75) failure is already tracked and fixed by BE-3290 / #536 (draft) — no separate follow-up needed.

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

Labels

agent-coded PR authored by the agent-work loop cursor-review Request Cursor bot review size:M This PR changes 30-99 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant