← All articles

CVE-2026-19584: Stored VQL Execution During Velociraptor Notebook Restore

A notebook editor could store Go-template syntax that executed server-side VQL when an administrator later restored a normal backup.

CVE
CVE-2026-19584
Product
Rapid7 Velociraptor
Affected versions
Linux releases before 0.77.2
Fixed versions
0.77.2
Status
Patched
7.7/ 10

CVSS 3.1 Base score

High

Rapid7 (CNA) score
Scoring details

Published vector

CVSS:3.1/AV:N/AC:H/PR:L/UI:R/S:C/C:H/I:H/A:N

CVSS is owned by FIRST and used by permission.

The short version

Velociraptor 0.77.1 embedded stored notebook cell input into a Go template during backup restoration. A stock analyst could plant a template action in a VQL cell; when an administrator later restored a normal backup containing that notebook, the restore renderer evaluated the action with a permissive ACL manager.

The lab demonstrated server-host command execution. This was stored, administrator-triggered escalation-not autonomous analyst RCE. The issue was fixed before 0.77.2 and is tracked as CVE-2026-19584.

At a glance

Field Detail
Tested version Velociraptor 0.77.1, commit 3137c7f71
Fixed version 0.77.2
Vulnerability class Template injection / code injection, CWE-1336 and CWE-94
Vendor affected range Velociraptor on Linux before 0.77.2
Required access Stock analyst with NOTEBOOK_EDITOR
User interaction Administrator restores a backup containing the stored cell
Demonstrated impact Server-host command execution during restore
Researcher estimate High
Credits Yuval Miller and Leon Kayaliev

Reproduction

The analyst lacked EXECVE, all collection permissions, and administrative permissions. A direct execution attempt established the control:

SELECT * FROM execve(argv=[
  '/bin/sh', '-c', 'echo ANALYST_DIRECT > /tmp/CONTROL.txt'])

Velociraptor returned an EXECVE permission error and created no file.

Using the analyst’s normal notebook permissions, I stored this complete input in a VQL cell:

{{ Query "SELECT * FROM execve(argv=['/bin/sh','-c','id > /tmp/RESTORE_PROOF.txt 2>&1; echo RESTORE_RAN_AS_SUPERUSER >> /tmp/RESTORE_PROOF.txt'])" | Table }}

Normal cell evaluation rejected the braces as invalid VQL. Nothing executed, but the input remained stored. An administrator then created a normal backup after the cell existed and restored the notebook provider from that backup:

SELECT * FROM backup(name='restore-test.zip')

SELECT * FROM backup_restore(
  name='restore-test.zip',
  providers='Notebook')

The restore completed and the proof file contained real server-host id output followed by the marker. Backup creation alone did not execute the payload; execution occurred during restore.

The attacker did not create, upload, or replace the ZIP. This matters because the vulnerable object could reach the restore path through Velociraptor’s ordinary backup workflow.

Why the backup sequence matters

There are three different moments in this chain: storing the cell, creating the backup, and restoring it. I tested them separately. Saving the cell did not execute the template action. Creating a backup containing it did not execute the action either. Only restore reached the privileged renderer.

That distinction keeps the impact honest. An analyst could prepare the stored input, but an administrator or automated recovery workflow still had to restore a backup containing it. The ZIP did not need to come from the attacker, which is what made the ordinary backup lifecycle relevant.

Root cause

The notebook restore code created its renderer with:

acl_manager := acl_managers.NullACLManager{}

It then constructed a Go template by concatenating the stored cell input:

data := "Recalculate this cell:\n```vql\n" + cell.Input + "\n```\n"

The reporting template engine exposed Query and Table helpers. Markdown fences did not neutralize {{ ... }} because template parsing occurred before Markdown rendering. The existing sanitizer addressed line continuations, not template actions.

This produced a classic stored injection chain: low-privileged input was saved as data, later reinterpreted as code, and evaluated in a more privileged context.

Why the Markdown fence did not help

The vulnerable function is NotebookBackupProvider.renderCell() in services/notebook/backup.go. It built a restore description by concatenating cell.Input into a Go-template source string, then passed that string to the reporting template engine. The Markdown fence was only output syntax; the template engine parsed {{ ... }} before Markdown rendering began.

The order was:

stored notebook input
    -> concatenated into Go-template source
    -> template actions parsed
    -> Query/Table helpers evaluated
    -> resulting text rendered as Markdown

Escaping backticks, line continuations, or other Markdown characters cannot repair that data/code boundary. The template must remain static and cell input must enter the renderer as a value, never as part of its grammar.

Detection notes

Search stored notebook cell input and notebook backups for Go-template delimiters such as {{ and }}, particularly around Query, Table, or other reporting helpers. Treat this as a triage signal rather than proof; legitimate documentation may contain braces.

Correlate suspicious cells with backup-restore events and server-side VQL or process execution. The restore timestamp is the important execution boundary, while the notebook edit identifies who planted the input and when.

Remediation

Upgrade to Velociraptor 0.77.2 or later. Notebook cell input should be passed to the renderer as inert data rather than concatenated into template source.

The fix in c0c9dd60 binds the cell input to a scope variable named In and places the fixed action {{ Scope "In" }} in the template. The attacker controls the value, not the template grammar. The patch retains NullACLManager; removing execution helpers or replacing that manager with a least-privilege identity would be additional defense in depth, not a description of the vendor fix.

Disclosure timeline

Date Event
2026-08-05 Vulnerability reported to Rapid7
2026-09-10 Rapid7 published CVE-2026-19584 and closed ticket 140166

References