CVE-2026-50289: Verified Repro With Script Download
CVE-2026-50289: systeminformation networkInterfaces Linux source-directive command injection
CVE-2026-50289 is verified against sebhildebrandt/systeminformation · github. Affected versions: <= 5.31.6. Fixed in 5.31.7. Vulnerability class: Command Injection. This high reproduction includes runnable sandbox proof, artifacts, and a plain-text agent view under REPRO-2026-00291.
What Is CVE-2026-50289?
CVE-2026-50289 is a high-severity OS command injection (CWE-78) in the sebhildebrandt/systeminformation npm library on Linux. Calling the public networkInterfaces() API can execute attacker-controlled shell commands as the Node.js process user. Pruva reproduced it (reproduction REPRO-2026-00291).
CVE-2026-50289 Severity & CVSS Score
CVE-2026-50289 is rated high severity, with a CVSS base score of 8.7 out of 10.
High — serious impact or readily exploitable. Prioritize remediation.
Affected sebhildebrandt/systeminformation Versions
sebhildebrandt/systeminformation · github versions <= 5.31.6 are affected.
How to Reproduce CVE-2026-50289
pruva-verify REPRO-2026-00291 curl -O https://pruva.dev/api/v1/reproductions/REPRO-2026-00291/artifacts/bundle/repro/reproduction_steps.sh && chmod +x reproduction_steps.sh && ./reproduction_steps.sh Proof of Reproduction for CVE-2026-50289
- reached the target end-to-end
- full exploit chain demonstrated
- high confidence
- the upstream fix blocks the same trigger
Shell-metacharacter-bearing path inside a Debian/Ubuntu 'source' directive line in a file reachable through /etc/network/interfaces source recursion (e.g. /etc/network/interfaces.d/*)
- public si.networkInterfaces()
- getLinuxDHCPNics()
- checkLinuxDCHPInterfaces('/etc/network/interfaces')
- recursive source traversal
- execSync(`cat ${file} 2> /dev/null | grep 'iface\|source'`) with unquoted ${file}
reproduction_steps.sh Negative variant/bypass search for CVE-2026-50289 (systeminformation networkInterfaces() source-directive command injection). The 5.31.7 fix replaces execSync(`cat ${file} | grep ...`) with fs.readFileSync, removing the shell from the source-directive recursion. No bypass on 5.31.7 or latest v5.31.17; the sibling ifac…
How the agent worked
Root Cause and Exploit Chain for CVE-2026-50289
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():
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.
- Package/component:
systeminformationnpm package,lib/network.js,checkLinuxDCHPInterfaces()(private helper reached by the publicnetworkInterfaces()API and public aggregate callersgetStaticData()/getAllData()). - Affected versions:
<5.31.7(verified on 5.31.6 / commitda1cbf5). - 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/interfacessourcerecursion 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
touchcommand only when the vulnerable 5.31.6 build processed the craftedsourcedirective, 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:
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
- Script:
bundle/repro/reproduction_steps.sh(self-contained, idempotent). - What it does:
- Reuses the prepared project-cache repo (or clones
sebhildebrandt/systeminformationwhen no cache exists). - Checks out the vulnerable release
v5.31.6(da1cbf5, parent of the fix) and the fixed release5.31.7(bbfddde). - Crafts a controlled
/etc/network/interfacescontaining a benignsourcedirective (functional control pointing to a real DHCP iface config) and a malicioussourcedirective 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.
- Reuses the prepared project-cache repo (or clones
- 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/interfacesbackup.
Key excerpts:
Vulnerable (5.31.6):
NETWORK_INTERFACES_RESOLVED marker_exists=true iface_count=2Fixed (5.31.7, same crafted source chain):
NETWORK_INTERFACES_RESOLVED marker_exists=false iface_count=2Summary:
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.7Environment: Linux, Node v24.18.0,
systeminformationbuilt from source at commitsda1cbf5(v5.31.6) andbbfddde(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 usingfs.readFileSync. - Defense in depth: For any remaining
execSync/execcall sites in the package that interpolate file-system-derived values, switch toexecFileSync/readFileSyncwith arguments passed as array elements, or validate paths against an allow-list. - Testing: Add a regression test that places a
sourcedirective with shell metacharacters in a controlled interfaces file and asserts no shell side effect occurs, while a benignsourcechain still parses.
Additional Notes
- Idempotency: The script was run twice consecutively; both runs produced
confirmed=truewith identical evidence (vulnerable marker=true,fixed marker=false). The script backs up and restores the original/etc/network/interfaceson every run. - Safety: Only a harmless marker oracle (
touchof an empty file under/tmp) was used; no destructive payload. The privatecheckLinuxDCHPInterfaces()helper was never invoked directly — only the publicnetworkInterfaces()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 benignsourceparsing.
Variant Analysis & Alternative Triggers for CVE-2026-50289
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)):
- 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 withDHCPNics.indexOf(iface)— it is never interpolated into a shell command, so it is not an uncovered variant sink. - Other
execSync/execsites inlib/network.js(e.g. thecat /sys/class/net/${ifaceSanitized}/...block at lines ~943-967) consume interface names fromos.networkInterfaces()(system-controlled) and pass them throughutil.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: publicsi.networkInterfaces(true). A controlled/etc/network/interfacescontains aniface pruvaA;touch<TAB><marker> inet dhcpline, 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 reachesexecSync. Not a variant.Candidate B —
source-directive BYPASS on fixed/latest. Entry point: publicsi.networkInterfaces(true). A controlled/etc/network/interfacescontainssourcetargets 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 —readFileSyncinvokes 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).
- Package/component:
systeminformation,lib/network.js,checkLinuxDCHPInterfaces()(private helper reached by the publicnetworkInterfaces()API and aggregate callers). - Affected versions (as tested): vulnerable on
v5.31.6(da1cbf5); fixed on5.31.7(bbfddde); also verified fixed on latestv5.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/interfacessource 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)
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
- Script:
bundle/vuln_variant/reproduction_steps.sh(self-contained, idempotent). - What it does:
- Reuses the prepared project-cache repo (or clones
sebhildebrandt/systeminformationwhen no cache exists). - Checks out the vulnerable release
v5.31.6(da1cbf5), the fixed release5.31.7(bbfddde), and the latest releasev5.31.17. - For each candidate, crafts a controlled
/etc/network/interfaces(plus a benign DHCP config under/etc/network/interfaces.d/), invokes only the publicsi.networkInterfaces(true)API, and observes distinct harmless marker oracles under/tmp. - Restores
/etc/network/interfacesand restores the repo tobbfddde(the state the repro stage left it in).
- Reuses the prepared project-cache repo (or clones
- Expected evidence:
C_vulnmarker = true (baseline sink fires on vulnerable).A_vuln/A_fixedmarkers = false (iface-name path does not reach a shell).B_fixed/B_latestmarkers (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=falsefor;,$(), and backtick styles).bundle/logs/vuln_variant_interfaces.backup— original/etc/network/interfacesbackup.
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 remainingexecSync/exectemplate-literal sites inlib/network.jsandlib/wifi.jsto 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:
checkLinuxDCHPInterfacesstill recurses onsource-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.mdtreats 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/interfacessource 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 (theifacedirective) 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.
CVE-2026-50289 Reproduction Transcript
The agent's step-by-step process — every tool call, every handoff, the moment the exploit fired. Phases: support · claim contract · reproduction · judge · variant analysis
Full session Replay every step — scrub the timeline or play it back.
ls -la /data/pruva/project-cache/ebe61b2a-f601-4416-916e-129c2338e243/ 2>/dev/null; echo "---repo---"; ls -la /data/pruva/project-cache/ebe61b2a-f601-4416-916e-129c2338e243/repo/ 2>/dev/null{"cwd":"/data/pruva/runs/871f48bd-5e2c-49d2-b789-559e145055b3","duration_ms":7,"exit_code":2,"stderr":"","stdout":"total 0\ndrwxr-xr-x 1 vscode vscode 24 Jul 16 08:10 .\ndrwxr-xr-x 1 vscode vscode 7388 Jul 16 08:10 ..\ndrwxr-xr-x 1 vscode vscode 0 Jul 16 08:10 repo-mirrors\n---repo---\n","success":false,"timeout_secs":120}Unknown error
cd /data/pruva/project-cache/ebe61b2a-f601-4416-916e-129c2338e243 && git clone --quiet https://github.com/sebhildebrandt/systeminformation.git repo 2>&1 | tail -5; echo "EXIT=$?"; cd repo && git log --oneline -3 bbfddde48672d0ee124fefdb3cb4442fd9dd4f03 2>&1 | headEXIT=0 bbfddde networkInterfaces() fix unsanitized command (linux) da1cbf5 5.31.6 87dbb60 fix: smaller issues
cd /data/pruva/project-cache/ebe61b2a-f601-4416-916e-129c2338e243/repo && git checkout --quiet da1cbf5 2>&1; echo "vuln checkout EXIT=$?"; sed -n '530,640p' lib/network.js{"cwd":"/data/pruva/runs/871f48bd-5e2c-49d2-b789-559e145055b3","duration_ms":14,"exit_code":0,"stderr":"","stdout":"vuln checkout EXIT=0\n}\n\nfunction getLinuxIfaceConnectionName(interfaceName) {\n try {\n const output = execFileSync('nmcli', ['device', 'status'], { ...util.execOptsLinux, stdio: ['ignore', 'pipe', 'ignore'] }).toString();\n const result = util.grep(output, interfaceName);\n\n const resultFormat = result.replace(/\\s+/g, ' ').trim();\n const connectionNameLines = resultFormat.split(' ').slice(3);\n const connectionName = connectionNameLines.join(' ');\n const connectionNameSanitized = util.sanitizeString(connectionName, false);\n\n return connectionNameSanitized !== '--' ? connectionNameSanitized : '';\n } catch {\n return '';\n }\n}\n\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 lines.forEach((line) => {\n const parts = line.replace(/\\s+/g, ' ').trim().split(' ');\n if (parts.length >= 4) {\n if (line.toLowerCase().indexOf(' inet ') >= 0 &&… [truncated]cd /data/pruva/project-cache/ebe61b2a-f601-4416-916e-129c2338e243/repo && git checkout --quiet bbfddde48672d0ee124fefdb3cb4442fd9dd4f03 2>&1; echo "fixed EXIT=$?"; sed -n '549,560p' lib/network.js; echo "---pkg main---"; grep -E '"main"|"name"|"version"' package.jsonfixed EXIT=0
function checkLinuxDCHPInterfaces(file) {
let result = [];
try {
const content = readFileSync(file, { encoding: 'utf8' });
const lines = content.split('\n').filter((l) => /iface|source/.test(l));
lines.forEach((line) => {cd /data/pruva/project-cache/ebe61b2a-f601-4416-916e-129c2338e243/repo && sed -n '549,575p' lib/network.jsfunction checkLinuxDCHPInterfaces(file) {
let result = [];
try {
const content = readFileSync(file, { encoding: 'utf8' });
const lines = content.split('\n').filter((l) => /iface|source/.test(l));
lines.forEach((line) => {
const parts = line.replace(/\s+/g, ' ').trim().split(' ');Artifacts and Evidence for CVE-2026-50289
Scripts, logs, diffs, and output captured during the reproduction.
How to Fix CVE-2026-50289
Upgrade sebhildebrandt/systeminformation · github to 5.31.7 or later.
FAQ: CVE-2026-50289
What has to be true to exploit CVE-2026-50289?
Which versions of systeminformation are affected by CVE-2026-50289, and where is it fixed?
How severe is CVE-2026-50289?
How can I reproduce CVE-2026-50289?
References for CVE-2026-50289
Authoritative sources for CVE-2026-50289 — official vulnerability databases and the upstream advisory. Pruva's reproduction verifies the issue firsthand; these are the primary records to corroborate it.