Skip to content

fix(path): reject Windows reserved basenames in sanitizeUntrustedFileName - #67

Open
SebTardif wants to merge 1 commit into
openclaw:mainfrom
SebTardif:fix/windows-reserved-basenames
Open

fix(path): reject Windows reserved basenames in sanitizeUntrustedFileName#67
SebTardif wants to merge 1 commit into
openclaw:mainfrom
SebTardif:fix/windows-reserved-basenames

Conversation

@SebTardif

Copy link
Copy Markdown

What Problem This Solves

Fixes an issue where consumers using sanitizeUntrustedFileName() (via staging, external output, and sibling-temp helpers) would accept Windows reserved device basenames such as CON, NUL.txt, or COM1 from untrusted input. On Windows those names open devices rather than ordinary files, which breaks portable name sanitization already used for C0/C1 and Windows-invalid characters on every host.

Why This Change Was Made

Reuse the reserved-name set and normalization already used by the device-path read guards (isWindowsReservedDeviceBaseName) inside sanitizeUntrustedFileName(). Reserved basenames (including extensions and trailing spaces/dots) return the caller-supplied fallback. Non-goals: changing path resolution or read guards; only the untrusted basename sanitizer.

User Impact

Callers that pass reserved basenames now receive the same fallback they already get for empty, ., or .. names. Valid names such as console.txt and null.pdf are unchanged. No public API signature changes.

Evidence

Red / green

  • Red: pnpm exec vitest run test/filename.test.ts failed with expected 'CON' to be 'fallback.bin' before the production change.
  • Green: same file, 4/4 tests pass after the fix; full pnpm check (598 passed, 22 skipped).

Runtime sample (built dist/)

"CON" -> fallback.bin
"nul.txt" -> fallback.bin
"COM1" -> fallback.bin
"LPT9.doc" -> fallback.bin
"aux " -> fallback.bin
"console.txt" -> console.txt
"report.pdf" -> report.pdf

Checklist

  • Tests added or updated when behavior changed
  • Security and compatibility impact considered
  • CHANGELOG.md updated when release-relevant
  • No credentials, private paths, private hosts, or sensitive contents included

Related

  • Same-repo reserved-name read guards: #22
  • Hardened staging / portable basename sanitization: #62
  • Node.js path reserved-name edge cases: nodejs/node#64266

Real behavior proof

  • Behavior or issue addressed: Untrusted basenames that are Windows reserved devices must not pass through sanitizeUntrustedFileName; they must fall back.

  • Real environment tested: macOS arm64, Node from repo toolchain, @openclaw/fs-safe branch fix/windows-reserved-basenames after pnpm build.

  • Exact steps or command run after this patch:

    pnpm install --frozen-lockfile
    pnpm exec vitest run test/filename.test.ts
    pnpm build
    node --input-type=module -e "import { sanitizeUntrustedFileName } from './dist/filename.js';
    for (const c of ['CON','nul.txt','console.txt']) console.log(c, '->', sanitizeUntrustedFileName(c, 'fallback.bin'));"
    pnpm check
  • Evidence after fix: reserved cases map to fallback.bin; non-reserved console.txt kept; full check green.

  • Observed result after fix: sanitizeUntrustedFileName('CON', 'fallback.bin') === 'fallback.bin' and siblings; staging helpers inherit the safer basename policy.

  • What was not tested: Live Windows NTFS create of reserved names (behavior is documented OS semantics; this PR blocks the name before open).

…Name

Reuse the device-path reserved-name set so untrusted names like CON,
NUL.txt, and COM1 fall back instead of staging as Windows devices.

Signed-off-by: Sebastien Tardif <sebtardif@ncf.ca>
@SebTardif
SebTardif requested a review from a team as a code owner July 27, 2026 15:32
@clawsweeper clawsweeper Bot added proof: sufficient Contributor real behavior proof is sufficient. rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR. P2 Normal priority bug or improvement with limited blast radius. labels Jul 27, 2026
@clawsweeper

clawsweeper Bot commented Jul 27, 2026

Copy link
Copy Markdown

Codex review: needs maintainer review before merge. Reviewed July 30, 2026, 9:37 AM ET / 13:37 UTC.

ClawSweeper review

What this changes

The PR makes sanitizeUntrustedFileName() return the caller fallback for Windows reserved device basenames, while preserving ordinary names such as console.txt.

Merge readiness

⚠️ Ready for maintainer review - 3 items remain

Keep this PR open for maintainer review. The focused hardening reuses the existing Windows reserved-device normalization and has convincing reported runtime proof, but GitHub currently marks the branch dirty against newer main; its real merge result must be refreshed before landing.

Priority: P2
Reviewed head: c47b4e59718f1eacd2b26645a3445d02c80b372d

Review scores

Measure Result What it means
Overall readiness 🐚 platinum hermit (4/6) The patch is focused and well-evidenced, with the remaining merge-readiness issue being the dirty branch against newer main rather than a demonstrated defect.
Proof confidence 🦞 diamond lobster (5/6) Sufficient (terminal): The PR body includes post-fix built-package terminal output for reserved and non-reserved names, alongside focused red/green test evidence; no private details are exposed in the supplied transcript.
Patch quality 🐚 platinum hermit (4/6) No actionable review findings were identified.

Verification

Check Result Evidence
Real behavior Verified Sufficient (terminal): The PR body includes post-fix built-package terminal output for reserved and non-reserved names, alongside focused red/green test evidence; no private details are exposed in the supplied transcript.
Evidence reviewed 6 items Focused implementation: The PR adds an exported reserved-device basename predicate and calls it from sanitizeUntrustedFileName() before returning a safe leaf name, making reserved names use the existing fallback path.
Regression coverage: The PR adds cases for CON, NUL with an extension, COM/LPT names, trailing-space aliases, and non-reserved lookalikes.
Existing feature provenance: The shared reserved-name behavior originates from the merged unsafe-device-read hardening, so this PR extends an established rule to filename sanitization rather than defining a parallel rule.
Findings None None.
Security None None.

How this fits together

sanitizeUntrustedFileName() turns attacker-controlled leaf names into portable filenames for staging and external-output helpers. The resulting name is then used by guarded filesystem operations, so rejecting Windows device aliases prevents those helpers from receiving names such as NUL.txt that Windows treats specially.

flowchart LR
  A[Untrusted filename] --> B[Basename extraction]
  B --> C[Portable filename sanitizer]
  C --> D{Windows reserved device name?}
  D -->|Yes| E[Caller fallback filename]
  D -->|No| F[Sanitized portable filename]
  E --> G[Staging and output helpers]
  F --> G[Staging and output helpers]
Loading

Before merge

  • Resolve merge risk (P2) - Merging changes the behavior of callers that currently pass Windows reserved basenames: they will receive their configured fallback instead of the original basename, which is intentional but should be validated on the rebased merge result.
  • Resolve merge risk (P1) - GitHub marks the branch dirty, so conflict resolution could alter the sanitizer, test, or changelog changes unless the focused behavior is rerun afterward.
  • Complete next step (P2) - A maintainer should refresh the dirty branch against current main and review the resulting compatibility behavior; there is no current line-level defect requiring an automated repair.
Agent review details

Security

None.

Review metrics

Metric Value Why it matters
Patch scope 4 files affected; 30 additions, 0 deletions The branch is narrowly scoped to the shared predicate, sanitizer call site, regression test, and release note.
Reserved-name coverage 6 reserved aliases and 2 ordinary lookalikes The tests cover extensions and trailing-space normalization while guarding against prefix/suffix false positives.

Merge-risk options

Maintainer options:

  1. Refresh the branch and validate the merge result (recommended)
    Rebase onto current main, resolve conflicts while retaining the existing shared normalization rule, then rerun the focused filename test and package checks before merge.
  2. Defer the hardening
    Pause this PR if maintainers do not want the intentional fallback change for existing callers that currently submit Windows device aliases.
Copy recommended automerge instruction
@clawsweeper automerge

Special instructions:
Rebase onto current main, preserve reserved-device fallback behavior, and rerun the focused filename test plus pnpm check on the refreshed head.

Technical review

Best possible solution:

Rebase onto current main, preserve the single shared reserved-device rule, and land only after the resulting merge commit proves reserved aliases fall back while ordinary lookalikes remain unchanged.

Do we have a high-confidence way to reproduce the issue?

No high-confidence current-main reproduction was performed in this read-only review. The PR supplies a focused pre-fix failing assertion and post-fix built-package runtime output, making the reported path source-reproducible with medium confidence.

Is this the best way to solve the issue?

Yes, conditionally. Reusing the established reserved-device normalization is the narrowest maintainable fix, but the branch must be rebased and its actual merge result revalidated before it is accepted.

AGENTS.md: found and applied where relevant.

Codex review notes: model internal, reasoning high; reviewed against ab933820c089.

Labels

Label justifications:

  • P2: This is a bounded portability and safety correction in a public filename-sanitization helper, with limited but real impact on callers using reserved Windows device aliases.
  • merge-risk: 🚨 compatibility: Existing callers that pass names such as CON or NUL.txt will intentionally receive their fallback after upgrade, so the rebased merge result needs compatibility confirmation.
  • rating: 🐚 platinum hermit: Overall readiness is 🐚 platinum hermit; proof is 🦞 diamond lobster and patch quality is 🐚 platinum hermit.
  • status: 👀 ready for maintainer look: ClawSweeper has no concrete contributor-facing blocker left for this PR. Sufficient (terminal): The PR body includes post-fix built-package terminal output for reserved and non-reserved names, alongside focused red/green test evidence; no private details are exposed in the supplied transcript.
  • proof: sufficient: Contributor real behavior proof is sufficient. The PR body includes post-fix built-package terminal output for reserved and non-reserved names, alongside focused red/green test evidence; no private details are exposed in the supplied transcript.

Evidence

What I checked:

  • Focused implementation: The PR adds an exported reserved-device basename predicate and calls it from sanitizeUntrustedFileName() before returning a safe leaf name, making reserved names use the existing fallback path. (src/filename.ts:29, c47b4e59718f)
  • Regression coverage: The PR adds cases for CON, NUL with an extension, COM/LPT names, trailing-space aliases, and non-reserved lookalikes. (test/filename.test.ts:22, c47b4e59718f)
  • Existing feature provenance: The shared reserved-name behavior originates from the merged unsafe-device-read hardening, so this PR extends an established rule to filename sanitization rather than defining a parallel rule. (src/device-path.ts:104, 5bf301406851)
  • Adjacent consumer provenance: The merged staging/output work already relies on sanitized fallback filenames, making this sanitizer gap relevant to those helper paths. (src/filename.ts:1, 66e59e0ce95e)
  • Merge state: GitHub reports the PR as dirty against the newer main branch, so the supplied branch diff is not yet the merge result maintainers would ship. (ab933820c089)
  • Real behavior proof: The PR body reports a built-package runtime run where reserved aliases return fallback.bin, valid names remain unchanged, and the focused test plus pnpm check pass. (c47b4e59718f)

Likely related people:

  • amknight: Authored the merged unsafe-device-read work that established the Windows reserved-name set and normalization this PR exports for reuse. (role: introduced shared reserved-device behavior; confidence: high; commits: 5bf301406851; files: src/device-path.ts)
  • steipete: Authored the merged staging and external-output hardening that consumes portable fallback filenames through this sanitizer path. (role: recent adjacent contributor; confidence: medium; commits: 66e59e0ce95e; files: src/filename.ts)

Rank-up moves

Optional improvements that raise the rating; they are not merge blockers.

  • Rebase or otherwise refresh the actual merge result against current main and rerun the focused filename test plus pnpm check.

Rating scale

Score Internal tier Crab rank Meaning
6/6 S 🦀 challenger crab Exceptional readiness
5/6 A 🦞 diamond lobster Very strong readiness
4/6 B 🐚 platinum hermit Good normal PR; ordinary maintainer review
3/6 C 🦐 gold shrimp Useful, but confidence is limited
2/6 D 🦪 silver shellfish Proof or implementation needs work
1/6 F 🧂 unranked krab Not merge-ready
N/A NA 🌊 off-meta tidepool Rating does not apply

Overall follows the weaker of proof and patch quality.
Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics.

Workflow

  • ClawSweeper keeps one durable marker-backed review comment per issue or PR.
  • Re-runs edit this comment so the latest verdict, findings, and automation markers stay together instead of adding duplicate bot comments.
  • A fresh review can be triggered by eligible @clawsweeper re-review comments, exact-item GitHub events, scheduled/background review runs, or manual workflow dispatch.
  • PR/issue authors and users with repository write access can comment @clawsweeper re-review or @clawsweeper re-run on an open PR or issue to request a fresh review only.
  • Maintainers can also comment @clawsweeper review to request a fresh review only.
  • Fresh-review commands do not start repair, autofix, rebase, CI repair, or automerge.
  • Maintainer-only repair and merge flows require explicit commands such as @clawsweeper autofix, @clawsweeper automerge, @clawsweeper fix ci, or @clawsweeper address review.
  • Maintainers can comment @clawsweeper explain to ask for more context, or @clawsweeper stop to stop active automation.

History

Review history (2 earlier review cycles)
  • reviewed 2026-07-27T15:36:27.515Z sha c47b4e5 :: needs maintainer review before merge. :: none
  • reviewed 2026-07-30T11:51:50.961Z sha c47b4e5 :: needs maintainer review before merge. :: none

@clawsweeper clawsweeper Bot added the merge-risk: 🚨 compatibility 🚨 Merging this PR could break existing users, config, migrations, defaults, or upgrades. label Jul 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

merge-risk: 🚨 compatibility 🚨 Merging this PR could break existing users, config, migrations, defaults, or upgrades. P2 Normal priority bug or improvement with limited blast radius. proof: sufficient Contributor real behavior proof is sufficient. rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant