{"repro_id":"REPRO-2026-00100","version":5,"title":"systeminformation: Command Injection via locate Output","repro_type":"security","status":"published","severity":"high","description":"Inside the `versions()` function, when detecting the PostgreSQL version on Linux, the code does this:\n\n```javascript\n// lib/osinfo.js — lines 770-776\n\nexec('locate bin/postgres', (error, stdout) => {\n  if (!error) {\n    const postgresqlBin = stdout.toString().split('\\n').sort();\n    if (postgresqlBin.length) {\n      exec(postgresqlBin[postgresqlBin.length - 1] + ' -V', (error, stdout) => {\n        // parses version string...\n      });\n    }\n  }\n});\n```\n\nHere's what happens step by step:\n\n1. It runs `locate bin/postgres` to search the filesystem for PostgreSQL binaries\n2. It splits the output by newline and sorts the results alphabetically\n3. It takes the **last element** (highest alphabetically)\n4. It concatenates that path directly into a new `exec()` call with `+ ' -V'`\n\n**No `sanitizeShellString()`. No path validation. No `execFile()`. Raw string concatenation into `exec()`.**\n\nThe `locate` command reads from a system-wide database (`plocate.db` or `mlocate.db`) that indexes all filenames on the system. If any indexed filename contains shell metacharacters — specifically semicolons — those characters will be interpreted by the shell when passed to `exec()`.\n\n---","root_cause":"# Root Cause Analysis Report\n## GHSA-5vv4-hvf7-2h46: Command Injection via Unsanitized `locate` Output in `versions()`\n\n## Summary\n\nThe `systeminformation` npm package (versions <= 5.30.7) contains a command injection vulnerability in the `versions()` function when detecting PostgreSQL version on Linux. The vulnerable code executes `locate bin/postgres` and passes the unsanitized output directly to `exec()` by concatenating the retrieved path with ` + ' -V'`. An attacker can create a file with a malicious path containing shell metacharacters (such as semicolons) that, when indexed by the locate database and sorted, becomes the selected path. This allows arbitrary command execution when any application using the library calls `si.versions('postgresql')`.\n\n## Impact\n\n- **Package:** systeminformation (npm)\n- **Affected Versions:** <= 5.30.7\n- **Fixed Version:** 5.31.0\n- **CVE:** CVE-2026-26318\n- **CVSS Score:** 8.8 (HIGH)\n- **CWE:** CWE-78 (OS Command Injection)\n- **Platform:** Linux only (the vulnerable code path is within an `if (_linux)` block)\n\n### Risk Level and Consequences\n- **Severity:** HIGH\n- **Attack Vector:** Local (requires ability to create files on the filesystem)\n- **Privileges Required:** Low (any user who can create files that get indexed by `updatedb`)\n- **Impact:** Arbitrary command execution with the privileges of the Node.js process\n\n### Attack Scenarios\n1. **Shared Hosting/Multi-Tenant:** A low-privileged user creates malicious files; a monitoring agent using `systeminformation` executes the injected commands\n2. **CI/CD Pipeline Poisoning:** Malicious build steps create crafted filenames; pipeline reporting triggers command execution\n3. **Container Escape:** Compromised containers create malicious files on shared volumes; host monitoring agents execute injected commands\n\n## Root Cause\n\n### Technical Analysis\n\nThe vulnerability exists in `lib/osinfo.js` at lines 770-776:\n\n```javascript\nexec('locate bin/postgres', (error, stdout) => {\n  if (!error) {\n    const postgresqlBin = stdout.toString().split('\\n').sort();\n    if (postgresqlBin.length) {\n      exec(postgresqlBin[postgresqlBin.length - 1] + ' -V', (error, stdout) => {\n        // ...\n      });\n    }\n  }\n});\n```\n\n**Why this is vulnerable:**\n\n1. **Unsanitized external input:** The `locate` command reads from a system-wide database (`plocate.db` or `mlocate.db`) that indexes all filenames. Attackers can create files with shell metacharacters in their paths.\n\n2. **Shell interpretation via `exec()`:** Node.js's `exec()` function passes commands to `/bin/sh -c`, which interprets shell metacharacters. A path like `/var/tmp/x;touch /tmp/FILE;/bin/postgres` becomes three separate commands when concatenated with ` + ' -V'`:\n   - `/var/tmp/x` (fails silently)\n   - `touch /tmp/FILE` (attacker's command executes)\n   - `/bin/postgres -V` (runs normally)\n\n3. **Alphabetical sort selection:** The code sorts paths and takes the last element. Since `/var/` sorts alphabetically after `/usr/`, a malicious path in `/var/tmp/` naturally becomes the selected one.\n\n4. **Missing validation:** No `sanitizeShellString()`, no path validation, no use of `execFile()` (which doesn't spawn a shell).\n\n### Suggested Fix\n\nReplace `exec()` with `execFile()` for the PostgreSQL binary version check:\n\n```javascript\nconst { exec, execFile } = require('child_process');\n\nexec('locate bin/postgres', (error, stdout) => {\n  if (!error) {\n    const postgresqlBin = stdout.toString().split('\\n')\n      .filter(p => p.trim().length > 0)\n      .sort();\n    if (postgresqlBin.length) {\n      // Use execFile instead of exec - does not spawn a shell\n      execFile(postgresqlBin[postgresqlBin.length - 1], ['-V'], (error, stdout) => {\n        // ... parse version\n      });\n    }\n  }\n});\n```\n\nAdditionally, validate paths against a safe pattern:\n\n```javascript\nconst safePath = /^[a-zA-Z0-9/_.-]+$/;\nconst postgresqlBin = stdout.toString().split('\\n')\n  .filter(p => safePath.test(p.trim()))\n  .sort();\n```\n\n## Reproduction Steps\n\nThe reproduction script is located at `repro/reproduction_steps.sh`.\n\n### What the Script Does\n\n1. **Environment Setup:** Installs `plocate` if not present (required for the vulnerable code path)\n2. **Clone Vulnerable Code:** Clones systeminformation v5.30.7 (the last vulnerable version)\n3. **Create Malicious Path:** Creates a file at `/var/tmp/x;touch /tmp/SI_RCE_PROOF;/bin/postgres` containing shell metacharacters\n4. **Update Locate Database:** Runs `updatedb` to index the malicious file\n5. **Verify Database:** Confirms the malicious path appears in `locate bin/postgres` output\n6. **Execute Exploit:** Runs a Node.js script that mimics the vulnerable code behavior:\n   - Executes `locate bin/postgres`\n   - Splits and sorts the output\n   - Selects the last path (alphabetically highest)\n   - Executes `selectedPath + ' -V'` via `exec()`\n7. **Verify Injection:** Checks if `/tmp/SI_RCE_PROOF` was created, proving command injection\n\n### Expected Evidence\n\nSuccessful reproduction creates the file `/tmp/SI_RCE_PROOF`, which was touched via the injected `touch /tmp/SI_RCE_PROOF` command embedded in the malicious path.\n\n**Successful output includes:**\n```\n!!! WARNING: Selected path contains semicolon - will cause command injection !!!\nExecuting vulnerable command: /var/tmp/x;touch /tmp/SI_RCE_PROOF;/bin/postgres -V\n*** COMMAND INJECTION SUCCESSFUL - Proof file exists! ***\n*** VULNERABILITY CONFIRMED ***\n```\n\n## Evidence\n\n### Log Files\n\nAll logs are stored in `$ROOT/logs/`:\n\n- `logs/clone.log` - Git clone output\n- `logs/updatedb.log` - Database update output (if applicable)\n- `logs/locate_output.log` - Output from `locate bin/postgres`\n- `logs/direct_test_output.log` - Main test execution output\n- `logs/direct_test.js` - The test script that mimics vulnerable code\n\n### Key Evidence Excerpts\n\nFrom `logs/direct_test_output.log`:\n```\nFound paths: [\n  '/usr/lib/postgresql/16/bin/postgres',\n  '/var/tmp/x;touch /tmp/SI_RCE_PROOF;/bin/postgres'\n]\nSelected path (last after sort): /var/tmp/x;touch /tmp/SI_RCE_PROOF;/bin/postgres\n\n!!! WARNING: Selected path contains semicolon - will cause command injection !!!\n\nExecuting vulnerable command: /var/tmp/x;touch /tmp/SI_RCE_PROOF;/bin/postgres -V\n\nCommand executed. Checking for proof file...\n\n*** COMMAND INJECTION SUCCESSFUL - Proof file exists! ***\n*** VULNERABILITY CONFIRMED ***\n```\n\nProof file creation:\n```\n-rw-r--r-- 1 root root 0 Feb 19 20:54 /tmp/SI_RCE_PROOF\n```\n\n### Vulnerable Code Location\n\n`repos/systeminformation/lib/osinfo.js` lines 770-776:\n```javascript\nexec('locate bin/postgres', (error, stdout) => {\n  if (!error) {\n    const postgresqlBin = stdout.toString().split('\\n').sort();\n    if (postgresqlBin.length) {\n      exec(postgresqlBin[postgresqlBin.length - 1] + ' -V', (error, stdout) => {\n```\n\n## Recommendations / Next Steps\n\n### Immediate Actions\n\n1. **Upgrade to v5.31.0 or later** - The vulnerability is fixed in version 5.31.0\n2. **Audit existing deployments** - Check if `systeminformation` is used in production applications\n3. **Monitor for exploitation** - Check for suspicious files in the locate database\n\n### Code Review\n\nReview all uses of `exec()` in the codebase to ensure:\n- No unsanitized external input is passed to `exec()`\n- `execFile()` is used when shell interpretation is not needed\n- Input validation is performed on paths from external sources\n\n### Testing Recommendations\n\n1. Add security regression tests that attempt command injection via various inputs\n2. Implement SAST scanning to detect dangerous patterns like `exec(variable + string)`\n3. Consider using a shell-escape library for any dynamic command construction\n\n## Additional Notes\n\n### Idempotency Confirmation\n\nThe reproduction script has been tested multiple times and produces consistent results:\n- Each run cleans up previous test artifacts\n- The malicious file is recreated each time\n- The locate database is updated each run\n- The vulnerability is successfully demonstrated on every execution\n\n### Edge Cases and Limitations\n\n1. **Locate database timing:** In production, the `updatedb` command runs on a daily schedule. The malicious file may not be indexed immediately after creation.\n\n2. **Sort order dependency:** The exploit relies on the malicious path sorting alphabetically after legitimate paths (`/var/` > `/usr/`). Paths starting with characters after 'v' in the alphabet would also work.\n\n3. **PostgreSQL presence:** The vulnerable code path only executes if `locate bin/postgres` returns results. Systems without PostgreSQL installed would not trigger this specific code path (though the pattern may exist elsewhere).\n\n4. **Root requirements:** Updating the locate database typically requires root privileges or the `plocate` group membership. However, the database is updated automatically by system timers, so attacker-created files will eventually be indexed.\n\n### Related Files\n\n- Vulnerable source: `repos/systeminformation/lib/osinfo.js`\n- Reproduction script: `repro/reproduction_steps.sh`\n- Test scripts: `logs/direct_test.js`, `logs/test_injection.js`\n- Package metadata: `repos/systeminformation/package.json`\n","ghsa_id":"GHSA-5vv4-hvf7-2h46","cve_id":"CVE-2026-26318","package":{"name":"systeminformation","ecosystem":"npm","affected_versions":"<= 5.30.7","fixed_version":"5.31.0"},"reproduced_at":"2026-02-19T21:13:58.512463+00:00","duration_secs":1124.0735611915588,"tool_calls":110,"turns":77,"handoffs":2,"total_cost_usd":0.4044941999999999,"agent_costs":{"repro":0.1371922,"support":0.022483999999999997,"vuln_variant":0.24481799999999995},"cost_breakdown":{"repro":{"accounts/fireworks/models/kimi-k2p5":0.1371922},"support":{"accounts/fireworks/models/kimi-k2p5":0.022483999999999997},"vuln_variant":{"accounts/fireworks/models/kimi-k2p5":0.24481799999999995}},"quality":{"idempotent_verified":false,"community_verifications":0},"published_at":"2026-02-19T21:14:00.639352+00:00","retracted":false,"artifacts":[{"path":"repro/rca_report.md","filename":"rca_report.md","size":9131,"category":"analysis"},{"path":"repro/reproduction_steps.sh","filename":"reproduction_steps.sh","size":11077,"category":"reproduction_script"},{"path":"bundle/ticket.md","filename":"ticket.md","size":9100,"category":"ticket"},{"path":"bundle/source.json","filename":"source.json","size":11511,"category":"other"},{"path":"bundle/ticket.json","filename":"ticket.json","size":21320,"category":"other"},{"path":"logs/direct_test_output.log","filename":"direct_test_output.log","size":548,"category":"log"},{"path":"logs/test_injection.js","filename":"test_injection.js","size":1850,"category":"other"},{"path":"logs/test_vuln_behavior.js","filename":"test_vuln_behavior.js","size":1738,"category":"other"},{"path":"logs/variant_vuln_clone.log","filename":"variant_vuln_clone.log","size":681,"category":"log"},{"path":"logs/test3_regex_patterns.log","filename":"test3_regex_patterns.log","size":1067,"category":"log"},{"path":"logs/updatedb.log","filename":"updatedb.log","size":0,"category":"log"},{"path":"logs/test_fixed_behavior.js","filename":"test_fixed_behavior.js","size":2160,"category":"other"},{"path":"logs/clone.log","filename":"clone.log","size":676,"category":"log"},{"path":"logs/locate_output.log","filename":"locate_output.log","size":85,"category":"log"},{"path":"logs/direct_test.js","filename":"direct_test.js","size":2848,"category":"other"},{"path":"logs/test1_vuln_behavior.log","filename":"test1_vuln_behavior.log","size":470,"category":"log"},{"path":"logs/test_output.log","filename":"test_output.log","size":1056,"category":"log"},{"path":"logs/variant_fixed_clone.log","filename":"variant_fixed_clone.log","size":682,"category":"log"},{"path":"logs/test_regex_patterns.js","filename":"test_regex_patterns.js","size":1964,"category":"other"},{"path":"logs/test2_fixed_behavior.log","filename":"test2_fixed_behavior.log","size":478,"category":"log"}]}