Actions: `uses: $/…` self-repository references are not resolved to local reusable workflows or composite actions (false positives and downgraded severity) · Issue #22755 · github/codeql · GitHub
You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Actions: uses: $/… self-repository references are not resolved to local reusable workflows or composite actions (false positives and downgraded severity) #22755
The Actions extractor and libraries do not resolve GitHub's self-repository uses: $/… syntax to the local reusable workflow or composite action it references. CodeQL resolves the equivalent ./… reference correctly. Because the call edge is lost, CodeQL analyzes every $/-called reusable workflow and composite action as if it had no caller. That causes two problems:
False positives. A reusable workflow whose only callers run on schedule, workflow_dispatch, push, or release is analyzed with workflow_call as its trigger. That makes it a source for actions/untrusted-checkout/medium, actions/code-injection/medium, and actions/envvar-injection/medium.
Downgraded severity, which masks real findings. A pull_request_target caller that passes github.event.pull_request.title into a $/ reusable workflow or composite action is reported as actions/code-injection/medium rather than actions/code-injection/critical. The same code with ./ is reported as critical.
On a real repository (below), switching the uses: prefix between ./ and $/ with no other change moves CodeQL from 0 results to 8. With $/ now recommended for same-repository references, CodeQL's caller-aware Actions analysis is silently lost for those workflows.
Why projects are adopting $/
$/ is not cosmetic. Several GitHub-owned controls depend on it:
zizmor. zizmor's self-repository audit flags every ./ same-repository reference and auto-fixes it to $/.
CodeQL already accepts $/ in one place: #22155 (for #22464) excludes $/ from actions/unpinned-tag. The call-resolution path in the AST was not updated, so every other Actions query still treats $/ as unresolved.
Root cause
At ce33a8a98cb2b35160a2f89bd464cc2fe04e742b (current main):
DataFlowPrivate.qll#L122: viableCallable joins Uses.getCallee() to getResolvedPath(), which is a file path with no prefix or a ./ prefix.
Ast.qll#L1393-L1395: repoUsesParser() is ([^/]+)/([^/]+)/([^@]+)@(.+) and pathUsesParser() is \./(.+).
ExternalJobImpl.getCallee() (Ast.qll#L1402-L1410): $/.github/workflows/x.yml does not match ./% and has no @, so getCallee() has no result and no call edge is created for job-level reusable workflows.
UsesStepImpl.getCallee() (Ast.qll#L1371-L1375): this returns the literal $/.github/actions/x, which never equals .github/actions/x or ./.github/actions/x, so step-level composite actions are not linked either.
Downstream, ReusableWorkflowImpl.getACaller() and CompositeActionImpl.getACallerStep() are empty. As a result:
JobImpl.getATriggerEvent() falls back to workflow_call;
EventImpl.isPrivileged() takes the not exists(...getACaller()) branch;
the checkoutTriggers() sources in UntrustedCheckoutQuery.qll match on workflow_call;
ControlChecks cannot see caller-side if: guards;
inputs lose interprocedural taint from the caller, so privileged, externally triggerable flows are not recognized as critical.
Minimal reproduction
CodeQL CLI 2.27.1 (bundle codeql-bundle-v2.27.1, codeql/actions-queries 0.6.36, codeql/actions-all 0.6.2). Each scenario is two identical trees that differ only in the uses: prefix (./ or $/).
actions/code-injection/criticalreusable.yml:12 and greet/action.yml:10
actions/code-injection/medium at the same two locations
Code samples or links to source code
Real-world case: microsoft/hve-core#3138 migrates 57 job-level reusable-workflow calls and 34 step-level local actions from ./ to $/ to satisfy the SHA-pinning policy, dependency locking, and zizmor's self-repository audit. The tracking issue is microsoft/hve-core#3112.
Treat $/ as a same-repository path, exactly like ./:
--- a/actions/ql/lib/codeql/actions/ast/internal/Ast.qll+++ b/actions/ql/lib/codeql/actions/ast/internal/Ast.qll
@@ class UsesStepImpl
override string getCallee() {
- if u.getValue().indexOf("@") > 0- then result = u.getValue().prefix(u.getValue().indexOf("@"))- else result = u.getValue()+ if u.getValue().matches("$/%")+ then result = "./" + u.getValue().suffix(2)+ else+ if u.getValue().indexOf("@") > 0+ then result = u.getValue().prefix(u.getValue().indexOf("@"))+ else result = u.getValue()
}
@@
-private string pathUsesParser() { result = "\\./(.+)" }+private string pathUsesParser() { result = "(?:\\.|\\$)/(.+)" }
@@ class ExternalJobImpl
override string getCallee() {
- if u.getValue().matches("./%")+ if u.getValue().matches(["./%", "$/%"])
then result = u.getValue().regexpCapture(pathUsesParser(), 1)
With this patch applied to codeql/actions-all 0.6.2 (including the copy vendored in codeql/actions-queries 0.6.36), the $/ variants match the ./ variants exactly:
Scenario 1: 3 results become 0.
Scenario 2: medium becomes critical.
microsoft/hve-core at 35e26eac1: 8 results become 0.
UsesStepImpl.getVersion() and ExternalJobImpl.getVersion() need no change: $/ references have no @. actions/unpinned-tag already skips $/ (#22155).
Possibly related: #21834 and #22263, which cover external callee resolution; this issue covers same-repository resolution only.
I'm happy to open a PR with the fix, library tests, and a change note if that would help.
Activity
fix(workflows): analyze full tree on PRs and register $/ CodeQL false…
hvitved commentedon Oct 6, 2026
Thank you, that would be much appreciated.
fix(workflows): harden CI supply chain and gate scanners without supp…