← All articles

CVE-2026-19200: When Velociraptor verify() Changed the Live Repository

A validation helper loaded analyst-controlled YAML into Velociraptor's shared artifact repository, allowing trusted built-ins to be replaced in memory.

CVE
CVE-2026-19200
Product
Rapid7 Velociraptor
Affected versions
Linux and Windows releases before 0.77.2
Fixed versions
0.77.2
Status
Patched
8.9/ 10

CVSS 3.1 Base score

High

Rapid7 (CNA) score
Scoring details

Published vector

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

CVSS is owned by FIRST and used by permission.

The short version

The verify() VQL function in Velociraptor 0.77.1 looked like a validation helper, but it loaded supplied artifact YAML into the live global repository. A stock analyst could replace a trusted built-in artifact in memory despite lacking every artifact-writer permission.

The issue was fixed before Velociraptor 0.77.2 and is tracked as CVE-2026-19200.

At a glance

Field Detail
Tested version Velociraptor 0.77.1, commit 3137c7f71
Fixed version 0.77.2
Vulnerability class Missing authorization / code injection, CWE-862 and CWE-94
Vendor affected range Linux and Windows releases before 0.77.2
Required access Stock analyst with ANY_QUERY
Demonstrated impact Volatile replacement of trusted built-in artifacts
Researcher estimate High
Credits Yuval Miller and Leon Kayaliev

Why a validator should not mutate production state

Validation functions are normally safe to expose to less-trusted users because their output is diagnostic: valid, invalid, and a list of errors. That safety assumption fails when validation operates on the same mutable object used by the running application.

Here, an analyst could submit a definition using the name of a shipped server artifact. A later server-side invocation compiling that trusted name on the same frontend could receive the attacker’s definition.

How I approached the test

The interesting property was the verb: verify implies inspection, not a state change. I used a shipped artifact name and a harmless marker query so the test could answer two separate questions: did validation modify the repository, and would an ordinary artifact lookup see the modification?

Restart behavior was part of the evidence. The shipped definition returned after the frontend restarted, which located the change in process memory and prevented me from overstating it as a persistent datastore overwrite. That also explained why separate frontend processes were not automatically proven affected.

Reproduction

The test account had the stock analyst role and lacked ARTIFACT_WRITER, SERVER_ARTIFACT_WRITER, and administrative permissions.

From an ordinary notebook, it invoked verify() with a replacement for a built-in artifact:

SELECT verify(artifact='''
name: Server.Information.Users
description: PWNED_BY_ANALYST_VIA_VERIFY
type: SERVER
sources:
  - query: SELECT "ATTACKER_CONTROLLED" AS x FROM scope()
''') AS Result
FROM scope()

The repository was then inspected and the trusted name invoked:

SELECT name, description
FROM artifact_definitions(names='Server.Information.Users')

SELECT * FROM Artifact.Server.Information.Users()

On 0.77.1, the description changed to the supplied marker and invocation returned ATTACKER_CONTROLLED. Restarting the frontend restored the shipped definition, proving that the mutation affected the live in-memory repository rather than the persistent datastore.

As a control, the same analyst could not overwrite the artifact through the normal artifact-writing APIs.

Root cause

vql/golang/verify.go obtained the shared repository and called repository.LoadYaml() directly without an authorization check. With the default disable_override=false, the function also supplied:

ArtifactIsBuiltIn: !arg.DisableOverride

The untrusted definition was therefore treated as built-in and bypassed the usual protection against replacing built-in artifacts. The function named GetGlobalRepository() returned exactly what its name promised: shared live state, not a disposable validation context.

The vulnerable object choice

The important operation is in vql/golang/verify.go. VerifyFunction.Call() asked the scope for the global repository, then passed submitted YAML directly to that repository’s LoadYaml() method. It also set ArtifactIsBuiltIn to the inverse of DisableOverride. With the default argument, attacker-controlled YAML was loaded into the live repository and labeled built-in.

In simplified form:

repository := GetGlobalRepository(scope)
repository.LoadYaml(untrustedDefinition, ArtifactIsBuiltIn: true)

The security bug is the repository object selected for validation. verify() did not need to execute the replacement itself. A later compilation asked the same mutable repository for a trusted name and received the analyst’s definition. The normal writer-API denial is therefore a meaningful control: the protected mutation was unavailable through the intended interface but reachable as a validation side effect.

Impact boundaries

The replacement was volatile and limited to the affected frontend process. It did not change flows that had already been compiled, and invoking the poisoned artifact directly still used the invoker’s ACL.

The demonstrated target, Server.Information.Users, is a SERVER artifact. The privilege boundary was crossed when a more privileged server-side invocation later compiled the poisoned trusted name. The experiment did not demonstrate an endpoint collection consuming this particular replacement, and it did not immediately grant COLLECT_CLIENT, EXECVE, or SuperUser.

Detection notes

Look for analyst execution of verify() followed by unexpected changes to built-in artifact descriptions or source. Because the mutation was in memory, disk-backed integrity checks alone may remain clean. Comparing definitions returned by each frontend against the shipped repository is more useful.

An unexplained definition that disappears after a frontend restart is a strong indicator of this class of mutation. Also review high-privileged flows compiled shortly after an analyst validation call, especially when the artifact name is built-in and the executed source differs from the version on disk.

Remediation

Upgrade to Velociraptor 0.77.2 or later. Validation should occur in a disposable repository that cannot affect live definitions. Any shared repository mutation must require the appropriate artifact-writer permission, and untrusted input must never be marked as built-in.

The vendor fix in b64d9154 changes the object used by verify(): it validates against a cached repository copy or repository.Copy() rather than the global object. A regression test must therefore check more than the return value. Valid attacker-supplied YAML must leave the live artifact definition unchanged after verification.

Disclosure timeline

Date Event
2026-08-05 Vulnerability reported to Rapid7
2026-08-24 Rapid7 announced CVE-2026-19200 and closed ticket 140161

References