# CVE-2026-50289 — systeminformation `networkInterfaces()` command injection (Linux)

## Summary

On Linux, `sebhildebrandt/systeminformation`'s public `networkInterfaces()` API
reaches `lib/network.js` → `getLinuxDHCPNics()` →
`checkLinuxDCHPInterfaces('/etc/network/interfaces')`. That helper reads the
Debian/Ubuntu network-interface file, follows `source` directives recursively,
and in versions `<5.31.7` interpolates the parsed source path **unquoted** into a
shell command executed via `child_process.execSync()`:

```js
function checkLinuxDCHPInterfaces(file) {
  let result = [];
  try {
    const cmd = `cat ${file} 2> /dev/null | grep 'iface\\|source'`;
    const lines = execSync(cmd, util.execOptsLinux).toString().split('\n');
    ...
    if (line.toLowerCase().includes('source')) {
      const file = line.split(' ')[1];
      result = result.concat(checkLinuxDCHPInterfaces(file));   // recursive, unquoted
    }
  ...
```

Because `${file}` is treated as shell text rather than a filename argument,
shell metacharacters inside a `source` target execute arbitrary commands as the
Node.js process user. The injected value originates from internal file content
(a `source` line in a file reachable through `/etc/network/interfaces` source
recursion, e.g. a file under `/etc/network/interfaces.d/*`), not from a direct
API argument.

## Impact

- **Package/component:** `systeminformation` npm package, `lib/network.js`,
  `checkLinuxDCHPInterfaces()` (private helper reached by the public
  `networkInterfaces()` API and public aggregate callers
  `getStaticData()`/`getAllData()`).
- **Affected versions:** `<5.31.7` (verified on 5.31.6 / commit `da1cbf5`).
- **Risk level:** High. Arbitrary command execution as the Node.js process user
  whenever an attacker can write or influence a file reached through the
  `/etc/network/interfaces` `source` recursion on a Debian/Ubuntu-style Linux
  host that calls the public network API.

## Impact Parity

- **Disclosed/claimed maximum impact:** Arbitrary command execution (code
  execution) via the public `networkInterfaces()` API on Linux.
- **Reproduced impact from this run:** Arbitrary command execution confirmed —
  a harmless marker oracle file was created by the injected `touch` command only
  when the vulnerable 5.31.6 build processed the crafted `source` directive,
  and the call resolved normally (`NETWORK_INTERFACES_RESOLVED`,
  `marker_exists=true`).
- **Parity:** `full`. The proof demonstrates attacker-controlled shell command
  execution (creation of an attacker-chosen file) through the public API on the
  vulnerable version, with the fixed version negative control showing no
  execution while the same benign source chain still parses.
- **Not demonstrated:** A destructive payload was intentionally not used
  (safety constraint); only a harmless marker oracle was employed.

## Root Cause

`checkLinuxDCHPInterfaces(file)` builds a shell command with an unquoted JS
template-string interpolation of `file`:

```js
const cmd = `cat ${file} 2> /dev/null | grep 'iface\\|source'`;
const lines = execSync(cmd, util.execOptsLinux).toString().split('\n');
```

`file` is initially `/etc/network/interfaces` but, on each `source` directive
line, the code extracts `line.split(' ')[1]` and recurses into
`checkLinuxDCHPInterfaces(file)` with that raw, attacker-influenced value. Since
the value is spliced into a shell string (not passed as an argument to
`execFileSync`/`readFileSync`), any shell metacharacters in the source target are
interpreted by `/bin/sh`. A `source` line such as:

```
source /tmp/pruva_inj_nonexist;touch<TAB>/tmp/pruva_cve_2026_50289_marker
```

is split by JS on the single space into `file =
"/tmp/pruva_inj_nonexist;touch\t/tmp/pruva_cve_2026_50289_marker"` (the literal
TAB survives because `split(' ')` only splits on the space character). The shell
then parses `cat /tmp/pruva_inj_nonexist;touch\t/tmp/marker 2> /dev/null | grep
...` as two commands separated by `;`, executing `touch /tmp/marker` (TAB is
shell whitespace), creating the marker file.

**Fix commit:** `bbfddde48672d0ee124fefdb3cb4442fd9dd4f03`
(https://github.com/sebhildebrandt/systeminformation/commit/bbfddde48672d0ee124fefdb3cb4442fd9dd4f03)
replaced `execSync('cat ... | grep ...')` with `fs.readFileSync(file, { encoding:
'utf8' })` plus an in-JS `.filter((l) => /iface|source/.test(l))`, eliminating the
shell entirely while preserving source-directive recursion.

## Reproduction Steps

1. **Script:** `bundle/repro/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`, parent of the fix)
     and the fixed release `5.31.7` (`bbfddde`).
   - Crafts a controlled `/etc/network/interfaces` containing a **benign**
     `source` directive (functional control pointing to a real DHCP iface config)
     and a **malicious** `source` directive whose target embeds `;touch<TAB>`
     followed by a harmless marker path under `/tmp`.
   - Invokes **only** the public `si.networkInterfaces(true)` API for each build
     (no private helper is called directly).
   - Observes a harmless marker oracle file: created on 5.31.6, absent on 5.31.7.
   - Restores the original `/etc/network/interfaces`.
3. **Expected evidence:**
   - `logs/vulnerable_5_31_6_api.log` → `NETWORK_INTERFACES_RESOLVED`,
     `marker_exists=true`.
   - `logs/fixed_5_31_7_api.log` → `NETWORK_INTERFACES_RESOLVED`,
     `marker_exists=false`.
   - `logs/reproduction_steps.log` → `VERDICT: CONFIRMED`.
   - `repro/runtime_manifest.json` → `target_path_reached=true`,
     `confirmed=true`.

## Evidence

- **Log files:**
  - `bundle/logs/reproduction_steps.log` — full run transcript.
  - `bundle/logs/vulnerable_5_31_6_api.log` — vulnerable API output.
  - `bundle/logs/fixed_5_31_7_api.log` — fixed API output (negative control).
  - `bundle/logs/interfaces.backup` — original `/etc/network/interfaces` backup.
- **Key excerpts:**

  Vulnerable (5.31.6):
  ```
  NETWORK_INTERFACES_RESOLVED
  marker_exists=true
  iface_count=2
  ```

  Fixed (5.31.7, same crafted source chain):
  ```
  NETWORK_INTERFACES_RESOLVED
  marker_exists=false
  iface_count=2
  ```

  Summary:
  ```
  vulnerable 5.31.6 marker created : true
  fixed     5.31.7 marker created : false
  VERDICT: CONFIRMED - command injection via public networkInterfaces() on 5.31.6; absent on fixed 5.31.7
  ```

- **Environment:** Linux, Node v24.18.0, `systeminformation` built from source at
  commits `da1cbf5` (v5.31.6) and `bbfddde` (5.31.7). No sanitizers; the marker
  oracle is a real filesystem side effect of an attacker-controlled shell
  command.

## Recommendations / Next Steps

- **Upgrade:** Update to `systeminformation >= 5.31.7`. The fix removes the shell
  entirely by using `fs.readFileSync`.
- **Defense in depth:** For any remaining `execSync`/`exec` call sites in the
  package that interpolate file-system-derived values, switch to
  `execFileSync`/`readFileSync` with arguments passed as array elements, or
  validate paths against an allow-list.
- **Testing:** Add a regression test that places a `source` directive with shell
  metacharacters in a controlled interfaces file and asserts no shell side
  effect occurs, while a benign `source` chain still parses.

## Additional Notes

- **Idempotency:** The script was run twice consecutively; both runs produced
  `confirmed=true` with identical evidence (`vulnerable marker=true`,
  `fixed marker=false`). The script backs up and restores the original
  `/etc/network/interfaces` on every run.
- **Safety:** Only a harmless marker oracle (`touch` of an empty file under
  `/tmp`) was used; no destructive payload. The private
  `checkLinuxDCHPInterfaces()` helper was never invoked directly — only the
  public `networkInterfaces()` API was called.
- **Scope:** The claim surface is `library_api` (`function_call`), matching the
  accepted claim contract. The proof exercises the real public API end-to-end on
  a real Linux runtime with the real package source; the fixed-version negative
  control confirms the patch blocks the injection while preserving benign
  `source` parsing.
