Tricking osquery into exposing arbitrary files to unprivileged users (CVE-2026-46388)

󰃭 2026-10-28

osquery is usually deployed with more access to a machine than the user running an ordinary process. That is what makes file carving useful: an administrator can ask osquery to collect files and send the resulting archive to a remote endpoint.

It also means the intermediate files created during a carve need to be private, and the files being carved need to stay within the intended scope. While looking at this flow in osquery 5.23.0, I found a chain involving attacker-controlled paths and a readable staging directory. An unprivileged local user could place a symbolic link in a directory selected by a carve query. osquery followed the link while collecting files, then staged the collected contents in a temporary directory that other local users could read while the upload was in progress. The issue was fixed in osquery 5.23.1 and assigned CVE-2026-46388.

How file carving works

A carve takes one or more files selected by a query, stores their contents in a temporary archive, then uploads that archive in blocks. The upload service is the receiver in this process. It accepts enrollment and upload requests, tells the client how large each block should be, receives the blocks, and acknowledges when the transfer is complete.

The important detail is that collection and upload happen over time. The temporary archive exists on disk while the upload is in progress. Removing it at the end does not protect its contents during that window; filesystem permissions must protect it while it exists.

Two parts of the file carve path

The first problem was path traversal through symbolic links. The file selection was based on a directory writable by an unprivileged user, but the carver did not reject symlinks found beneath that directory. A link could point outside the intended collection path, including to a file the osquery process could read but the user could not.

The second problem was where osquery staged the carve. The carver creates a working directory under the system temporary directory. Before the fix, it used a regular directory creation call:

carveDir_ = fs::temp_directory_path() / fs::path(kCarvePathPrefix + carveGuid_);
auto ret = fs::create_directory(carveDir_);

That did not explicitly restrict access to the account running osquery. The resulting directory had mode 0755 (drwxr-xr-x), so another local user could traverse it and read the staged files while the carve was running. The data was temporary, but it was not private. I reproduced the behavior on Linux and macOS with osquery 5.23.0; the temporary location is /tmp on Linux and commonly /private/tmp on macOS.

Together, these behaviors turn a carve of an attacker-controlled directory into a local arbitrary file read: the symlink makes osquery collect an out-of-scope file, and the readable staging directory lets the unprivileged user recover its contents before cleanup. Sensitive targets can include /etc/shadow, private SSH keys, certificates, or environment files containing credentials. The same symlink behavior can also target very large files such as /proc/kcore, potentially consuming substantial disk space and affecting osquery or the host. The attacker still needs local access, a matching carve to run, and time to read the staging data; this is not a remote attack against the upload server.

Reproducing the upload flow

I enabled carving and configured a TLS distributed plugin pointed at a small HTTPS Flask server. A scheduled query selected files under an attacker-writable directory:

{
  "schedule": {
    "bad_carve": {
      "query": "select * from carves where path like '/home/bob/%%' and carve=1;",
      "interval": 30
    }
  }
}

Here, bob can write to /home/bob/, while osquery runs with the privileges needed to read protected system files. The query asks osquery to carve files in that directory; it does not ask for /etc/shadow directly. Bob places a link there, for example:

ln -s /etc/shadow /home/bob/foo

When the scheduled carve runs, osquery follows foo. To keep the upload in progress long enough to inspect the staging directory, we can place a large file in the selected tree:

dd if=/dev/zero of=/home/bob/randomfile bs=1M count=1024

The 1 GiB file is a timing aid for this testing, not a prerequisite for the underlying permission flaw. A watcher can spot the per-carve directory as it appears:

watch -n 0.5 'ls -la /tmp'
drwxr-xr-x  3 root root 4096 May  9 16:27 osquery_carve_221790b8-f13d-41df-827a-5ca5366ec0b3

It appears as a root-owned drwxr-xr-x directory with the prefix osquery_carve_. The mirrored path beneath it preserved the selected file path, so the symlink target’s contents could be read from the staging tree while the carve was active. The race is that the attacker must read the file before osquery finishes the carve and deletes the temporary data.

cat /tmp/osquery_carve_221790b8-f13d-41df-827a-5ca5366ec0b3/home/bob/foo

root:$y$j9T$695mozQzcEvN.Z4EMeno6.$uVuA2Qg2g4Qc3P2hOIJAF3wwFEYwH7trNV3koPWBGt5:20583:0:99999:7:::
daemon:*:20305:0:99999:7:::
bin:*:20305:0:99999:7:::
sys:*:20305:0:99999:7:::
bob:$y$j9T$lpmMIQFwKrwe/FDTtxCbD1$b/PDmCS38U2l0PT/QL2odljiTM6.HFXBAhd/1ge29s4:20480:0:99999:7:::

The test upload server and its osquery test configuration is available here . osquery TLS configuration needs a certificate the test client trusts; the test flags point the enrollment and carve endpoints at this local server and permit the test certificate.

The fix

The patch for CVE-2026-46388 creates the carve directory with owner-only permissions at creation time. On POSIX, the implementation uses mkdir with owner read, write, and execute permissions (0700) and checks the result. Creating it privately in the first operation avoids a gap where the directory exists with broader access and is only locked down afterward.

auto s = platformCreatePrivateDir(carveDir_);
if (!s.ok()) {
  return s;
}

References: