fix(notifications): prevent PowerShell string injection on Windows (#1138) - #1216
fix(notifications): prevent PowerShell string injection on Windows (#1138)#1216rishu685 wants to merge 1 commit into
Conversation
nc-review: comments — 1 important, 1 nitFix is correct and resolves the linked issue: the previous 🟠 important · The third test ( ⚪ nit ·
🔴 blocking · 🟠 a reviewer would ask for a change · ⚪ optional Automated code review — correctness, security, design, tests, plus duplicates and scope. A human still decides; this is not a substitute for review and is not exhaustive. The required status checks separately cover lint, formatting, types, unused dependencies, the test suite and the build. This bot never merges. Maintainers can rerun with |
There was a problem hiding this comment.
🟢 Approval recommended
The change removes the identified injection vector by design (static encoded script + encoded env vars) and is covered by focused unit tests for both safety and correctness.
Pull request overview
This PR fixes Windows native notification handling by eliminating PowerShell string interpolation of user-controlled notification content, preventing command evaluation/parsing issues (as described in #1138) while keeping notification behavior intact.
Changes:
- Replaced dynamic PowerShell
-Commandstring building with a static script executed via-EncodedCommand(UTF-16LE Base64). - Passed notification
title/messagesafely via Base64-encoded UTF-8 environment variables decoded inside PowerShell at runtime. - Added AVA unit tests to verify the script remains static (no interpolation) and that tricky/injection-like inputs round-trip correctly.
File summaries
| File | Description |
|---|---|
source/utils/notifications.ts |
Refactors Windows notification execution to use a static, encoded PowerShell command and Base64 env vars instead of interpolated script strings. |
source/utils/notifications.spec.ts |
Adds tests validating non-interpolation, correct decoding/roundtrip for special characters, and safe handling under a forced win32 platform. |
.changeset/fix-windows-notification-powershell-injection.md |
Adds a patch changeset describing the security fix and linking it to #1138. |
Review details
- Files reviewed: 3/3 changed files
- Comments generated: 0
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
|
Hey @rishu685 - please can you address the points in nc-review above? :) |
will-lamerton
left a comment
There was a problem hiding this comment.
Thanks for this - the direction is right and I want to merge it. Passing the user strings out-of-band instead of splicing them into the script is the correct shape. Three things first.
1. The changeset overstates the fix (please reword).
PowerShell single-quoted strings are verbatim: inside '...', backticks, $(...) and $env:X are all literal, and the only escape is '', which the old code applied to every '. There was no break-out, so no command execution. #1138 hedges on this ("may allow ... depending on the evaluation context") but the changeset states it as fact, and that text ships to the public changelog.
Suggested:
Hardened Windows native notifications. Notification title and message are no longer interpolated into the PowerShell script; they are passed out-of-band as environment variables and read by a static script. Fixes rendering and parsing edge cases with backticks, quotes and other special characters. Closes #1138.
2. Stub execFile in the win32 test (the nc-review point).
sendNotification handles win32 platform gracefully spawns a real powershell.exe on a Windows runner and ENOENTs into the swallowing callback everywhere else, so t.notThrows only proves nothing throws synchronously. Please spy on execFile and assert the command that would run is the static -EncodedCommand form. That also closes the real gap: nothing currently verifies sendWindows passes the payload through to execFile, so a regression that reintroduced interpolation there would pass every test in the file.
Related: t.false(decodedScript.includes(title)) cannot fail as written. WINDOWS_NOTIFICATION_ENCODED_COMMAND is a module constant computed at import and returned unmodified, so those assertions hold no matter what the function does with title. Fine to keep as an invariant, but they aren't the safety evidence they read as.
3. Drop the base64 on the env vars.
Env vars are already a data channel, not a code channel - $env:FOO yields a string and assigning it to .BalloonTipTitle never evaluates it. Windows env vars are natively UTF-16, so emoji, newlines and quotes survive without encoding. Removes the decode branch and the else { '' } fallback on both sides:
$notify.BalloonTipTitle = $env:NANOCODER_NOTIFICATION_TITLE
$notify.BalloonTipText = $env:NANOCODER_NOTIFICATION_MESSAGEKeep -EncodedCommand - it sidesteps powershell.exe's -Command quoting quirks and the script is a constant now, so nothing is lost.
Nits, take or leave:
- Spreading
process.envis load-bearing (powershell.exeneedsSystemRoot/PATH) since passingenvdisables implicit inheritance. Worth a comment so nobody trims it to the two keys later. - Stray blank line at the end of
notifications.spec.ts. Biome won't flag it,biome.jsonexcludes**/*.spec.ts.
f1d077f to
6c70105
Compare
6c70105 to
5c90a1f
Compare
Summary
Fixes #1138 by eliminating PowerShell script string interpolation when displaying native notifications on Windows.
Details
sendWindowspreviously interpolated notificationtitleandmessagedirectly into a PowerShell script string passed topowershell -Command.`), quotes, or PowerShell variables could trigger command evaluation or break notification delivery.sendWindowsto use a completely static PowerShell script executed via-EncodedCommand(UTF-16LE Base64) with-NonInteractiveandwindowsHide: true.NANOCODER_NOTIFICATION_TITLEandNANOCODER_NOTIFICATION_MESSAGE), decoded at runtime inside PowerShell via[System.Convert]::FromBase64Stringand[System.Text.Encoding]::UTF8.GetString.Testing
pnpm run test:ava source/utils/notifications.spec.ts(15/15 passed)pnpm run format:checkpnpm run test:lintpnpm run test:types