{"repro_id":"REPRO-2026-00291","version":6,"title":"systeminformation networkInterfaces() Linux source-directive command injection","repro_type":"security","status":"published","severity":"high","description":"On Linux, sebhildebrandt/systeminformation’s public networkInterfaces() API reaches lib/network.js and its DHCP interface discovery path. The vulnerable path reads /etc/network/interfaces, follows Debian/Ubuntu-style source directives, and in versions <5.31.7 interpolates a path parsed from file content unquoted into a shell command executed via execSync(). Because the parsed source path is treated as shell text rather than an argument, shell metacharacters in a source target can execute arbitrary commands as the Node.js process user.","root_cause":"# CVE-2026-50289 — systeminformation `networkInterfaces()` command injection (Linux)\n\n## Summary\n\nOn Linux, `sebhildebrandt/systeminformation`'s public `networkInterfaces()` API\nreaches `lib/network.js` → `getLinuxDHCPNics()` →\n`checkLinuxDCHPInterfaces('/etc/network/interfaces')`. That helper reads the\nDebian/Ubuntu network-interface file, follows `source` directives recursively,\nand in versions `<5.31.7` interpolates the parsed source path **unquoted** into a\nshell command executed via `child_process.execSync()`:\n\n```js\nfunction checkLinuxDCHPInterfaces(file) {\n  let result = [];\n  try {\n    const cmd = `cat ${file} 2> /dev/null | grep 'iface\\\\|source'`;\n    const lines = execSync(cmd, util.execOptsLinux).toString().split('\\n');\n    ...\n    if (line.toLowerCase().includes('source')) {\n      const file = line.split(' ')[1];\n      result = result.concat(checkLinuxDCHPInterfaces(file));   // recursive, unquoted\n    }\n  ...\n```\n\nBecause `${file}` is treated as shell text rather than a filename argument,\nshell metacharacters inside a `source` target execute arbitrary commands as the\nNode.js process user. The injected value originates from internal file content\n(a `source` line in a file reachable through `/etc/network/interfaces` source\nrecursion, e.g. a file under `/etc/network/interfaces.d/*`), not from a direct\nAPI argument.\n\n## Impact\n\n- **Package/component:** `systeminformation` npm package, `lib/network.js`,\n  `checkLinuxDCHPInterfaces()` (private helper reached by the public\n  `networkInterfaces()` API and public aggregate callers\n  `getStaticData()`/`getAllData()`).\n- **Affected versions:** `<5.31.7` (verified on 5.31.6 / commit `da1cbf5`).\n- **Risk level:** High. Arbitrary command execution as the Node.js process user\n  whenever an attacker can write or influence a file reached through the\n  `/etc/network/interfaces` `source` recursion on a Debian/Ubuntu-style Linux\n  host that calls the public network API.\n\n## Impact Parity\n\n- **Disclosed/claimed maximum impact:** Arbitrary command execution (code\n  execution) via the public `networkInterfaces()` API on Linux.\n- **Reproduced impact from this run:** Arbitrary command execution confirmed —\n  a harmless marker oracle file was created by the injected `touch` command only\n  when the vulnerable 5.31.6 build processed the crafted `source` directive,\n  and the call resolved normally (`NETWORK_INTERFACES_RESOLVED`,\n  `marker_exists=true`).\n- **Parity:** `full`. The proof demonstrates attacker-controlled shell command\n  execution (creation of an attacker-chosen file) through the public API on the\n  vulnerable version, with the fixed version negative control showing no\n  execution while the same benign source chain still parses.\n- **Not demonstrated:** A destructive payload was intentionally not used\n  (safety constraint); only a harmless marker oracle was employed.\n\n## Root Cause\n\n`checkLinuxDCHPInterfaces(file)` builds a shell command with an unquoted JS\ntemplate-string interpolation of `file`:\n\n```js\nconst cmd = `cat ${file} 2> /dev/null | grep 'iface\\\\|source'`;\nconst lines = execSync(cmd, util.execOptsLinux).toString().split('\\n');\n```\n\n`file` is initially `/etc/network/interfaces` but, on each `source` directive\nline, the code extracts `line.split(' ')[1]` and recurses into\n`checkLinuxDCHPInterfaces(file)` with that raw, attacker-influenced value. Since\nthe value is spliced into a shell string (not passed as an argument to\n`execFileSync`/`readFileSync`), any shell metacharacters in the source target are\ninterpreted by `/bin/sh`. A `source` line such as:\n\n```\nsource /tmp/pruva_inj_nonexist;touch<TAB>/tmp/pruva_cve_2026_50289_marker\n```\n\nis split by JS on the single space into `file =\n\"/tmp/pruva_inj_nonexist;touch\\t/tmp/pruva_cve_2026_50289_marker\"` (the literal\nTAB survives because `split(' ')` only splits on the space character). The shell\nthen parses `cat /tmp/pruva_inj_nonexist;touch\\t/tmp/marker 2> /dev/null | grep\n...` as two commands separated by `;`, executing `touch /tmp/marker` (TAB is\nshell whitespace), creating the marker file.\n\n**Fix commit:** `bbfddde48672d0ee124fefdb3cb4442fd9dd4f03`\n(https://github.com/sebhildebrandt/systeminformation/commit/bbfddde48672d0ee124fefdb3cb4442fd9dd4f03)\nreplaced `execSync('cat ... | grep ...')` with `fs.readFileSync(file, { encoding:\n'utf8' })` plus an in-JS `.filter((l) => /iface|source/.test(l))`, eliminating the\nshell entirely while preserving source-directive recursion.\n\n## Reproduction Steps\n\n1. **Script:** `bundle/repro/reproduction_steps.sh` (self-contained, idempotent).\n2. **What it does:**\n   - Reuses the prepared project-cache repo (or clones\n     `sebhildebrandt/systeminformation` when no cache exists).\n   - Checks out the vulnerable release `v5.31.6` (`da1cbf5`, parent of the fix)\n     and the fixed release `5.31.7` (`bbfddde`).\n   - Crafts a controlled `/etc/network/interfaces` containing a **benign**\n     `source` directive (functional control pointing to a real DHCP iface config)\n     and a **malicious** `source` directive whose target embeds `;touch<TAB>`\n     followed by a harmless marker path under `/tmp`.\n   - Invokes **only** the public `si.networkInterfaces(true)` API for each build\n     (no private helper is called directly).\n   - Observes a harmless marker oracle file: created on 5.31.6, absent on 5.31.7.\n   - Restores the original `/etc/network/interfaces`.\n3. **Expected evidence:**\n   - `logs/vulnerable_5_31_6_api.log` → `NETWORK_INTERFACES_RESOLVED`,\n     `marker_exists=true`.\n   - `logs/fixed_5_31_7_api.log` → `NETWORK_INTERFACES_RESOLVED`,\n     `marker_exists=false`.\n   - `logs/reproduction_steps.log` → `VERDICT: CONFIRMED`.\n   - `repro/runtime_manifest.json` → `target_path_reached=true`,\n     `confirmed=true`.\n\n## Evidence\n\n- **Log files:**\n  - `bundle/logs/reproduction_steps.log` — full run transcript.\n  - `bundle/logs/vulnerable_5_31_6_api.log` — vulnerable API output.\n  - `bundle/logs/fixed_5_31_7_api.log` — fixed API output (negative control).\n  - `bundle/logs/interfaces.backup` — original `/etc/network/interfaces` backup.\n- **Key excerpts:**\n\n  Vulnerable (5.31.6):\n  ```\n  NETWORK_INTERFACES_RESOLVED\n  marker_exists=true\n  iface_count=2\n  ```\n\n  Fixed (5.31.7, same crafted source chain):\n  ```\n  NETWORK_INTERFACES_RESOLVED\n  marker_exists=false\n  iface_count=2\n  ```\n\n  Summary:\n  ```\n  vulnerable 5.31.6 marker created : true\n  fixed     5.31.7 marker created : false\n  VERDICT: CONFIRMED - command injection via public networkInterfaces() on 5.31.6; absent on fixed 5.31.7\n  ```\n\n- **Environment:** Linux, Node v24.18.0, `systeminformation` built from source at\n  commits `da1cbf5` (v5.31.6) and `bbfddde` (5.31.7). No sanitizers; the marker\n  oracle is a real filesystem side effect of an attacker-controlled shell\n  command.\n\n## Recommendations / Next Steps\n\n- **Upgrade:** Update to `systeminformation >= 5.31.7`. The fix removes the shell\n  entirely by using `fs.readFileSync`.\n- **Defense in depth:** For any remaining `execSync`/`exec` call sites in the\n  package that interpolate file-system-derived values, switch to\n  `execFileSync`/`readFileSync` with arguments passed as array elements, or\n  validate paths against an allow-list.\n- **Testing:** Add a regression test that places a `source` directive with shell\n  metacharacters in a controlled interfaces file and asserts no shell side\n  effect occurs, while a benign `source` chain still parses.\n\n## Additional Notes\n\n- **Idempotency:** The script was run twice consecutively; both runs produced\n  `confirmed=true` with identical evidence (`vulnerable marker=true`,\n  `fixed marker=false`). The script backs up and restores the original\n  `/etc/network/interfaces` on every run.\n- **Safety:** Only a harmless marker oracle (`touch` of an empty file under\n  `/tmp`) was used; no destructive payload. The private\n  `checkLinuxDCHPInterfaces()` helper was never invoked directly — only the\n  public `networkInterfaces()` API was called.\n- **Scope:** The claim surface is `library_api` (`function_call`), matching the\n  accepted claim contract. The proof exercises the real public API end-to-end on\n  a real Linux runtime with the real package source; the fixed-version negative\n  control confirms the patch blocks the injection while preserving benign\n  `source` parsing.\n","cve_id":"CVE-2026-50289","cwe_id":"CWE-78 (OS Command Injection)","source_url":"https://github.com/sebhildebrandt/systeminformation","package":{"name":"sebhildebrandt/systeminformation","ecosystem":"github","affected_versions":"<= 5.31.6","fixed_version":"5.31.7","tested_patched":"5.31.7"},"reproduced_at":"2026-07-16T11:51:20.182159+00:00","duration_secs":884.0,"tool_calls":126,"handoffs":2,"total_cost_usd":1.262901,"agent_costs":{"claim_matcher":0.014786,"judge":0.201843,"learning_policy":0.012173,"repro":0.328509,"support":0.042661,"vuln_variant":0.662929},"cost_breakdown":{"claim_matcher":{"gpt-5.4-mini-2026-03-17":0.014786},"judge":{"gpt-5.4-mini":0.168948,"gpt-5.4-mini-2026-03-17":0.032895},"learning_policy":{"gpt-5.4-mini-2026-03-17":0.012173},"repro":{"accounts/fireworks/routers/glm-5p2-fast":0.328509},"support":{"accounts/fireworks/routers/glm-5p2-fast":0.042661},"vuln_variant":{"accounts/fireworks/routers/glm-5p2-fast":0.662929}},"quality":{"confidence":"high","idempotent_verified":false,"community_verifications":0},"evidence":{"workflow":{"profile":"known_vulnerability","schema_version":1,"stages":["support","claim_contract","repro","judge","vuln_variant"]}},"environment":{"sandbox_image":"ghcr.io/n3mes1s/pruva-sandbox@sha256:8096b2518d6022e13d68f885c3b8ded6b4fe607098b1a1ccbfb99abc004d1dc1"},"published_at":"2026-07-16T11:51:20.840937+00:00","retracted":false,"artifacts":[{"path":"bundle/repro/reproduction_steps.sh","filename":"reproduction_steps.sh","size":8613,"category":"reproduction_script"},{"path":"bundle/repro/rca_report.md","filename":"rca_report.md","size":8307,"category":"analysis"},{"path":"bundle/vuln_variant/reproduction_steps.sh","filename":"reproduction_steps.sh","size":12544,"category":"reproduction_script"},{"path":"bundle/vuln_variant/rca_report.md","filename":"rca_report.md","size":11537,"category":"analysis"},{"path":"bundle/vuln_variant/source_identity.json","filename":"source_identity.json","size":1428,"category":"other"},{"path":"bundle/vuln_variant/root_cause_equivalence.json","filename":"root_cause_equivalence.json","size":1891,"category":"other"},{"path":"bundle/logs/reproduction_steps.log","filename":"reproduction_steps.log","size":1858,"category":"log"},{"path":"bundle/repro/validation_verdict.json","filename":"validation_verdict.json","size":988,"category":"other"},{"path":"bundle/repro/runtime_manifest.json","filename":"runtime_manifest.json","size":797,"category":"other"},{"path":"bundle/logs/vulnerable_5_31_6_api.log","filename":"vulnerable_5_31_6_api.log","size":61,"category":"log"},{"path":"bundle/logs/fixed_5_31_7_api.log","filename":"fixed_5_31_7_api.log","size":62,"category":"log"},{"path":"bundle/logs/interfaces.backup","filename":"interfaces.backup","size":75,"category":"other"},{"path":"bundle/vuln_variant/validation_verdict.json","filename":"validation_verdict.json","size":2597,"category":"other"},{"path":"bundle/vuln_variant/patch_analysis.md","filename":"patch_analysis.md","size":5040,"category":"documentation"},{"path":"bundle/vuln_variant/variant_manifest.json","filename":"variant_manifest.json","size":3483,"category":"other"},{"path":"bundle/vuln_variant/runtime_manifest.json","filename":"runtime_manifest.json","size":2285,"category":"other"},{"path":"bundle/logs/C_vuln_5_31_6_api.log","filename":"C_vuln_5_31_6_api.log","size":74,"category":"log"},{"path":"bundle/logs/A_vuln_5_31_6_api.log","filename":"A_vuln_5_31_6_api.log","size":75,"category":"log"},{"path":"bundle/logs/A_fixed_5_31_7_api.log","filename":"A_fixed_5_31_7_api.log","size":75,"category":"log"},{"path":"bundle/logs/B_fixed_5_31_7_api.log","filename":"B_fixed_5_31_7_api.log","size":144,"category":"log"},{"path":"bundle/logs/B_latest_5_31_17_api.log","filename":"B_latest_5_31_17_api.log","size":144,"category":"log"}]}