{"id":"GHSA-7648-jmwf-vw7j","summary":"systeminformation has prototype pollution in `networkStats()` via the interface name","details":"## Summary\n\n`networkStats()` keeps a per interface cache for computing `rx_sec` / `tx_sec` between calls.\nThe cache is a plain JavaScript object and is indexed with the interface name the caller\nsupplied. Passing the name `__proto__` makes every cache write land on `Object.prototype`\ninstead of the cache, polluting every object in the Node.js process.\n\nAn application is affected if it passes untrusted input as the `ifaces` argument of\n`si.networkStats()` and runs on FreeBSD, OpenBSD or NetBSD.\n\n## Details\n\n### The vulnerable code\n\n`lib/network.js` declares the cache as a plain object:\n\n```js\nconst _network = {};\n```\n\n`calcNetworkSpeed()` writes into it, keyed by the interface name:\n\n```js\n} else {\n  if (!_network[iface]) {\n    _network[iface] = {};\n  }\n  _network[iface].rx_bytes  = rx_bytes;\n  _network[iface].tx_bytes  = tx_bytes;\n  _network[iface].rx_sec    = null;\n  _network[iface].tx_sec    = null;\n  _network[iface].ms        = Date.now();\n  _network[iface].last_ms   = 0;\n  _network[iface].operstate = operstate;\n}\n```\n\nWith `iface === '__proto__'` this misbehaves twice:\n\n1. `_network['__proto__']` does not read a missing own property — it invokes the inherited\n   `__proto__` getter and returns `Object.prototype`, which is truthy. The\n   `if (!_network[iface])` guard therefore never creates the `{}` placeholder.\n2. Each following assignment writes onto `Object.prototype`.\n\nAfter one call, `rx_bytes`, `tx_bytes`, `rx_sec`, `tx_sec`, `ms`, `last_ms` and `operstate`\nare visible on **every** object in the process. A second call takes the other branch\n(`_network['__proto__'].ms` is now truthy) and keeps them updated.\n\n### Why the existing sanitizing does not help\n\nThe interface name does pass through `util.sanitizeString(iface, true)` before it is used as\nthe cache key. That function is a **shell metacharacter** filter, built to stop command\ninjection in the `netstat` / `ifconfig` invocations — a different threat. Property names made\nof letters and underscores are legitimate interface names and pass through untouched:\n\n```\n\"a;id\"        -\u003e \"aid\"\n\"a$(id)\"      -\u003e \"aid\"\n\"a|id\"        -\u003e \"aid\"\n\"a`id`\"       -\u003e \"aid\"\n\"__proto__\"   -\u003e \"__proto__\"     \u003c-- unchanged\n\"constructor\" -\u003e \"constructor\"   \u003c-- unchanged\n```\n\nThe sanitizer is doing its job; the gap is that no layer treats the value as an *object key*.\n\n### Reachability\n\nThe cache write is guarded incidentally on three of four platform branches. These guards exist\nfor unrelated reasons and are not a deliberate defence:\n\n| Platform | Code path | Reachable with an arbitrary name? |\n| --- | --- | --- |\n| Linux / Android | `fs.existsSync('/sys/class/net/' + ifaceSanitized)` | **No** — a non-existent interface returns early |\n| macOS | requires `netstat` to emit a data row beyond the header | **No** — verified on macOS 27, `Object.prototype` stays clean |\n| Windows | `if (rx_bytes && tx_bytes)` — requires a real adapter match | **No** |\n| **FreeBSD / OpenBSD / NetBSD** | `if (!error) { …; result = calcNetworkSpeed(ifaceSanitized, …) }` | **Yes** — the call sits outside the parsing loop, so any successful `netstat` exit reaches it, and `netstat -ibndI \u003cunknown\u003e` prints only its header and exits `0` |\n\nThe important part is not that three platforms happen to be safe, but that nothing in the\ndesign prevents the write. Any future platform branch without such a guard reopens the issue,\nwhich is why the fix is applied at the cache rather than at the call sites.\n\n### Age\n\nThe pattern predates the v5 line — `git log -S \"_network[iface] = {}\"` traces it back to a\nrefactoring before `v3.42.5`. The v3 and v4 lines are end of life; the advisory covers v5.\n\n## Impact\n\nThe attacker controls which attributes appear on `Object.prototype` but not their values —\nthose come from the interface statistics (`rx_bytes`, `ms`, `operstate` …). The attribute\n*names* are fixed by the library, so this is not an arbitrary-key pollution primitive.\n\nThe consequences are therefore application specific and arise from name collisions:\n\n- Any application code that tests `if (obj.operstate)`, `if (obj.ms)` or similar on an object\n  that should not have those properties now takes the wrong branch.\n- `for…in` loops over plain objects gain seven extra enumerable keys, which can corrupt\n  serialisation, configuration merging or validation logic.\n- Libraries that use `{}` as a lookup map and check membership with `in` or truthiness can\n  return values for keys that were never inserted.\n\nNo path to remote code execution is known, and the values cannot be chosen by the attacker.\n\n## Proof of concept\n\n### The underlying primitive (any platform)\n\n```js\nconst { calcNetworkSpeed } = require('systeminformation/lib/network.js'); // internal\nconst cache = {};                       // the declaration used by the library\ncalcNetworkSpeed('__proto__', 1, 2, 'up', 0, 0, 0, 0);\n\nconsole.log({}.rx_bytes);               // 1\nconsole.log({}.operstate);              // 'up'\nconsole.log(Object.prototype.hasOwnProperty('rx_bytes')); // true\n```\n\n### End to end through the public API (FreeBSD / OpenBSD / NetBSD)\n\n```js\nconst si = require('systeminformation');\n\n(async () =\u003e {\n  console.log('before:', {}.operstate);   // undefined\n  await si.networkStats('__proto__');     // an attacker supplied interface name\n  console.log('after :', {}.operstate);   // 'up'  -\u003e Object.prototype is polluted\n})();\n```\n\nOn Linux, macOS and Windows the same call returns an empty result set and leaves\n`Object.prototype` untouched, for the reasons in the *Reachability* table.\n\n## Patch\n\n`lib/network.js` — give the cache a null prototype, so `__proto__` becomes an ordinary own\nproperty instead of a setter into the prototype chain:\n\n```diff\n-const _network = {};\n+const _network = Object.create(null);\n```\n\nThis is the smallest change that fixes the class of problem rather than one reachable path:\n\n- It is independent of the platform branches, so no guard has to be added or maintained.\n- It keeps behaviour identical for every legitimate interface name — `Object.keys()`,\n  member reads and writes all work unchanged on a null prototype object.\n- It matches the pattern already used elsewhere in the codebase\n  (`lib/filesystem.js:114-115`, `lib/graphics.js:1060`, `lib/graphics.js:1087`).\n- `Object.create` is ES5.1, well below the declared `engines.node \u003e= 10.0.0`.\n\nAfter the patch the hostile name is stored as a normal cache entry:\n\n```js\nconst cache = Object.create(null);\n// ... cache['__proto__'] holds the stats object, Object.prototype is untouched\n```\n\n## Workarounds\n\nFor users who cannot upgrade immediately, either of these is sufficient:\n\n1. **Validate the interface name before calling** — accept only names returned by\n   `si.networkInterfaces()`, or reject the names `__proto__`, `constructor` and `prototype`.\n2. **Never pass untrusted input** as the `ifaces` argument. Passing `'*'` or omitting the\n   argument is unaffected, because those names come from the system rather than the caller.\n\nFreezing `Object.prototype` (`Object.freeze(Object.prototype)`) also prevents the write, but it\nhas wide-reaching side effects on other dependencies and is not recommended as a general\nmitigation.\n\n## Affected versions\n\nAll `5.x` releases up to and including `5.33.12`. Earlier major lines contain the same pattern\nbut are end of life.\n\n### What was verified, and what was not\n\nVerified on this machine (macOS 27, Node 24):\n\n- the pollution primitive, by calling the cache write with `iface = '__proto__'`;\n- that `util.sanitizeString('__proto__', true)` returns the string unchanged, while shell\n  metacharacters are stripped;\n- that unpatched v5 on macOS does **not** pollute through the public API;\n- that `netstat -bdnI \u003cunknown-interface\u003e` prints only its header and exits `0`;\n- that the patch stores the hostile key as an own property and leaves `Object.prototype` clean.\n\nNot verified, because no such host was available:\n\n- end to end exploitation on FreeBSD, OpenBSD or NetBSD. That conclusion is derived from\n  reading the BSD branch of `networkStatsSingle()` — where the `calcNetworkSpeed()` call sits\n  outside the row parsing loop and is reached whenever `netstat` exits successfully — combined\n  with the observed `netstat` exit behaviour above. Worth confirming on a BSD host before\n  publishing, since the affected-platform list depends on it.","aliases":["CVE-2026-116674"],"modified":"2026-10-28T21:30:05.739266214Z","published":"2026-10-28T21:13:32Z","database_specific":{"nvd_published_at":"2026-10-27T17:16:59Z","cwe_ids":["CWE-1321"],"severity":"MODERATE","github_reviewed":true,"github_reviewed_at":"2026-10-28T21:13:32Z"},"references":[{"type":"WEB","url":"https://github.com/sebhildebrandt/systeminformation/security/advisories/GHSA-7648-jmwf-vw7j"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-116674"},{"type":"WEB","url":"https://github.com/sebhildebrandt/systeminformation/commit/6ff25a1ebc0b1fdc7a8aa3507e46f22007da2969"},{"type":"PACKAGE","url":"https://github.com/sebhildebrandt/systeminformation"},{"type":"WEB","url":"https://github.com/sebhildebrandt/systeminformation/releases/tag/v5.33.13"}],"affected":[{"package":{"name":"systeminformation","ecosystem":"npm","purl":"pkg:npm/systeminformation"},"ranges":[{"type":"SEMVER","events":[{"introduced":"0"},{"fixed":"5.33.13"}]}],"database_specific":{"source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/10/GHSA-7648-jmwf-vw7j/GHSA-7648-jmwf-vw7j.json"}}],"schema_version":"1.9.2","severity":[{"type":"CVSS_V3","score":"CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:H/A:L"}]}