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 |