# CVE-2026-50289 — Variant / Bypass Analysis (systeminformation `networkInterfaces()`)

## Summary

No distinct variant or bypass was found. The fix shipped in 5.31.7
(commit `bbfddde`) is **complete and surgical**: it replaced the only
attacker-controlled-file→shell sink in the `networkInterfaces()` DHCP path —
`execSync(`cat ${file} 2> /dev/null | grep 'iface\\|source'`)` inside
`checkLinuxDCHPInterfaces()` — with `fs.readFileSync(file, { encoding: 'utf8' })`
plus an in-JS `.filter((l) => /iface|source/.test(l))`. Because `readFileSync`
invokes no shell, shell metacharacters in a `source`-directive target are inert
on the fixed and latest (5.31.17) releases. The only sibling data path an
attacker can influence via `/etc/network/interfaces.d/*` — the `iface`-directive
interface name — never reaches a shell command (it flows only to
`result.push(parts[1])` and later `DHCPNics.indexOf(iface)`), so it is not a
variant of the same root cause. A bounded three-candidate search was executed
and produced a negative result.

## Fix Coverage / Assumptions

**What the fix changes** (`lib/network.js`, `checkLinuxDCHPInterfaces(file)`):

```diff
-    const cmd = `cat ${file} 2> /dev/null | grep 'iface\\|source'`;
-    const lines = execSync(cmd, util.execOptsLinux).toString().split('\n');
+    const content = readFileSync(file, { encoding: 'utf8' });
+    const lines = content.split('\n').filter((l) => /iface|source/.test(l));
```

plus `const readFileSync = require('fs').readFileSync;` added to the imports.

**Invariant the fix relies on:** the source-directive recursion
(`result = result.concat(checkLinuxDCHPInterfaces(file))` where `file =
line.split(' ')[1]`) now reads the next file with `readFileSync` instead of a
shell `cat`. There is no shell anywhere in the recursion, so no interpolation of
file-derived content into a command string can occur.

**Code paths explicitly covered:** every call that reaches
`checkLinuxDCHPInterfaces('/etc/network/interfaces')` — i.e. the public
`networkInterfaces()` API and the public aggregate callers `getStaticData()` /
`getAllData()` that invoke it on Linux via `getLinuxDHCPNics()`.

**What the fix does NOT cover (examined, ruled out):**
- The `iface`-directive interface name (`parts[1]`) is parsed from the same
  attacker-writable files but is only pushed onto the DHCP nic list and later
  compared with `DHCPNics.indexOf(iface)` — it is never interpolated into a
  shell command, so it is not an uncovered variant sink.
- Other `execSync`/`exec` sites in `lib/network.js` (e.g. the
  `cat /sys/class/net/${ifaceSanitized}/...` block at lines ~943-967) consume
  interface names from `os.networkInterfaces()` (system-controlled) and pass
  them through `util.sanitizeString()`; they do not consume attacker-writable
  file content and do not share this CVE's trust boundary.

## Variant / Alternate Trigger

Three materially distinct candidate angles were tested (see
`bundle/vuln_variant/reproduction_steps.sh`):

- **Candidate A — `iface`-directive NAME injection (sibling data path).**
  Entry point: public `si.networkInterfaces(true)`. A controlled
  `/etc/network/interfaces` contains an `iface pruvaA;touch<TAB><marker> inet dhcp`
  line, exercising the other directive type an attacker can write into the
  source-recursion tree. Tested on vulnerable 5.31.6 **and** fixed 5.31.7.
  Result: marker **not created** on either version — the parsed interface name
  never reaches `execSync`. Not a variant.

- **Candidate B — `source`-directive BYPASS on fixed/latest.**
  Entry point: public `si.networkInterfaces(true)`. A controlled
  `/etc/network/interfaces` contains `source` targets with three different
  shell-injection styles — `;touch<TAB>`, `$(touch<TAB>)`, and
  `` `touch<TAB>` `` — each pointing at its own harmless marker oracle. Tested
  on fixed 5.31.7 **and** latest 5.31.17. Result: **none** of the three markers
  were created on either version — `readFileSync` invokes no shell, so all
  metacharacters are inert. No bypass.

- **Candidate C — baseline sanity (original `;touch<TAB>` source injection).**
  Re-run on vulnerable 5.31.6 to prove the harness and sink still fire. Result:
  marker **created** (`C_vuln marker=true`), confirming the test rig is sound
  and the sink is reachable on the vulnerable version only.

Exact entry point for all candidates: the **public** `si.networkInterfaces()`
library API (no private helper is called directly). Code path:
`networkInterfaces()` → `getLinuxDHCPNics()` →
`checkLinuxDCHPInterfaces('/etc/network/interfaces')` → recursive
`source`-directive traversal (`lib/network.js` lines ~549-566).

## Impact

- **Package/component:** `systeminformation`, `lib/network.js`,
  `checkLinuxDCHPInterfaces()` (private helper reached by the public
  `networkInterfaces()` API and aggregate callers).
- **Affected versions (as tested):** vulnerable on `v5.31.6`
  (`da1cbf5`); fixed on `5.31.7` (`bbfddde`); also verified
  fixed on latest `v5.31.17` (`9e79988`).
- **Risk level:** the original CVE is High (arbitrary command execution as the
  Node.js process user). No additional variant/bypass risk was identified — the
  fix closes the sink completely.

## Impact Parity

- **Disclosed/claimed maximum impact (parent CVE):** arbitrary command
  execution as the Node.js process user via shell metacharacters in a
  `source`-directive target reachable through `/etc/network/interfaces`
  source recursion.
- **Reproduced impact from this variant run:** no variant/bypass reproduced.
  The baseline (Candidate C) reproduced the original command injection on
  5.31.6 only; all candidate variants were inert on the fixed and latest
  releases.
- **Parity:** `none` — no distinct variant impact was demonstrated because no
  distinct variant exists.
- **Not demonstrated:** N/A (negative result; no new code-execution path was
  found).

## Root Cause

The original root cause was **unquoted interpolation of an
attacker-controlled-from-file path into a shell command**:
`checkLinuxDCHPInterfaces()` built `cat ${file} | grep 'iface|source'` and
ran it via `execSync`, where `${file}` came from `line.split(' ')[1]` of a
`source` directive inside a file reachable through `/etc/network/interfaces`
source recursion (e.g. `/etc/network/interfaces.d/*`). Shell metacharacters in
that token were interpreted by `/bin/sh`.

The fix (commit
[`bbfddde`](https://github.com/sebhildebrandt/systeminformation/commit/bbfddde48672d0ee124fefdb3cb4442fd9dd4f03))
eliminates the shell from this path entirely by reading the file with
`fs.readFileSync` and filtering lines in JavaScript. The source-directive
recursion is preserved, but it now recurses through `readFileSync`, which
cannot execute commands. There is no remaining code path in the
`networkInterfaces()` DHCP discovery flow that interpolates file-derived
content into a shell command, so the same underlying bug cannot be reached from
a different entry point or via different metacharacter encodings. The
`iface`-directive name — the only other attacker-influenceable token parsed
from the same files — was confirmed by test not to reach any shell sink.

## Reproduction Steps

1. **Script:** `bundle/vuln_variant/reproduction_steps.sh` (self-contained,
   idempotent).
2. **What it does:**
   - Reuses the prepared project-cache repo (or clones
     `sebhildebrandt/systeminformation` when no cache exists).
   - Checks out the vulnerable release `v5.31.6` (`da1cbf5`), the fixed
     release `5.31.7` (`bbfddde`), and the latest release `v5.31.17`.
   - For each candidate, crafts a controlled `/etc/network/interfaces` (plus a
     benign DHCP config under `/etc/network/interfaces.d/`), invokes **only**
     the public `si.networkInterfaces(true)` API, and observes distinct
     harmless marker oracles under `/tmp`.
   - Restores `/etc/network/interfaces` and restores the repo to `bbfddde`
     (the state the repro stage left it in).
3. **Expected evidence:**
   - `C_vuln` marker = **true** (baseline sink fires on vulnerable).
   - `A_vuln` / `A_fixed` markers = **false** (iface-name path does not reach
     a shell).
   - `B_fixed` / `B_latest` markers (all three styles) = **false** (no bypass
     on fixed or latest).

## Evidence

- `bundle/logs/vuln_variant_reproduction.log` — full annotated run log.
- `bundle/logs/C_vuln_5_31_6_api.log` — baseline API output on 5.31.6
  (`marker=true`).
- `bundle/logs/A_vuln_5_31_6_api.log`,
  `bundle/logs/A_fixed_5_31_7_api.log` — iface-name candidate (`marker=false`).
- `bundle/logs/B_fixed_5_31_7_api.log`,
  `bundle/logs/B_latest_5_31_17_api.log` — source-bypass candidate
  (`marker=false` for `;`, `$()`, and backtick styles).
- `bundle/logs/vuln_variant_interfaces.backup` — original
  `/etc/network/interfaces` backup.

Key excerpt (second idempotent run):
```
C_vuln:/tmp/pruva_variant_markerC=true
A_vuln:/tmp/pruva_variant_markerA=false
A_fixed:/tmp/pruva_variant_markerA=false
B_fixed: ...markerB1=false ...markerB2=false ...markerB3=false
B_latest: ...markerB1=false ...markerB2=false ...markerB3=false
VERDICT: NO VARIANT/BYPASS - fix covers the source-directive sink; iface-name
data path does not reach a shell
```

Environment: Linux; Node.js; `systeminformation` built from source at commits
`da1cbf5` (v5.31.6), `bbfddde` (5.31.7), and `9e79988` (v5.31.17).

## Recommendations / Next Steps

- **No gap to close.** The fix is complete for the command-injection root
  cause. The Coding stage does not need to extend the patch for this CVE.
- **Defense-in-depth (optional, already partially present):** the project
  already routes most shell-interpolated interface names through
  `util.sanitizeString()`. Maintainers may consider auditing the remaining
  `execSync`/`exec` template-literal sites in `lib/network.js` and `lib/wifi.js`
  to ensure every interpolated value is either system-controlled or sanitized,
  as a general hardening measure — but none of those constitute a bypass of
  this specific fix.
- **Recursion note:** `checkLinuxDCHPInterfaces` still recurses on
  `source`-directive targets with no depth limit. This is a benign
  resource-use concern (unbounded recursion / file reads), not a command
  injection, and is out of scope for this CVE.

## Additional Notes

- **Idempotency:** the script was run twice; both runs completed cleanly with
  identical results (exit 1, no variant). No crashes.
- **Repo state preserved:** the cache repo was restored to `bbfddde` (the
  commit the repro stage left it on) at the end of every run.
- **Trust boundary / threat model:** the project's `SECURITY.md` treats
  security bugs as in-scope and commits to auditing for similar problems. The
  attacker preconditions (Linux; attacker can write a file reached through
  `/etc/network/interfaces` source recursion) match the parent CVE. The
  variant search stayed within that trust boundary and found no additional
  reachable sink.
- **Negative-result scope:** fewer than 3 *distinct* bypass candidates exist
  because there is exactly one attacker-controlled-file→shell sink in the
  `networkInterfaces()` DHCP flow, and the fix removes the shell from it.
  Candidate A is a genuinely different *data path* (the `iface` directive) that
  was tested and ruled out; Candidate B is a genuinely different *encoding set*
  tested against the fixed code; Candidate C is the baseline control. Further
  attempts would re-exercise the same sink/data path and would not constitute
  materially distinct variants.
