Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions actions/extractor/tools/autobuild-impl.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ $DefaultPathFilters = @(
'include:.github/workflows/*.yaml',
'include:.github/reusable_workflows/**/*.yml',
'include:.github/reusable_workflows/**/*.yaml',
'include:.github/actions/external/mapping.yaml',
'include:.github/workflows/external/mapping.yaml',
'include:**/action.yml',
'include:**/action.yaml'
)
Expand Down
2 changes: 2 additions & 0 deletions actions/extractor/tools/autobuild.sh
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ include:.github/workflows/*.yml
include:.github/workflows/*.yaml
include:.github/reusable_workflows/**/*.yml
include:.github/reusable_workflows/**/*.yaml
include:.github/actions/external/mapping.yaml
include:.github/workflows/external/mapping.yaml
include:**/action.yml
include:**/action.yaml
END
Expand Down
49 changes: 49 additions & 0 deletions actions/ql/lib/codeql/actions/MappingHelper.qll
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/**
* Provides predicates for resolving external action and workflow references
* through SHA-based mapping files.
*/

private import codeql.actions.ast.internal.Yaml

/**
* Holds if the external action mapping file maps `ownerRepo` at `ref` to `sha`.
*
* The mapping file is expected at `.github/actions/external/mapping.yaml` and has
* the following structure:
* ```yaml
* owner/repo:
* ref: sha
* ```
*
* This enables SHA-based directory resolution for external composite actions
* stored in the `.github/actions/external/{owner}/{repo}/{sha}/` directory structure.
*/
predicate externalActionRefMapping(string ownerRepo, string ref, string sha) {
exists(YamlMapping mapping, YamlMapping refMap |
(
mapping.getLocation().getFile().getRelativePath().matches("%/.github/actions/external/mapping.yaml")
or
mapping.getLocation().getFile().getRelativePath() = ".github/actions/external/mapping.yaml"
) and
refMap = mapping.lookup(ownerRepo) and
sha = refMap.lookup(ref).(YamlScalar).getValue()
)
}

/**
* Holds if the external workflow mapping file maps `ownerRepo` at `ref` to `sha`.
*
* The mapping file is expected at `.github/workflows/external/mapping.yaml` and has
* the same structure as the action mapping file.
*/
predicate externalWorkflowRefMapping(string ownerRepo, string ref, string sha) {
exists(YamlMapping mapping, YamlMapping refMap |
(
mapping.getLocation().getFile().getRelativePath().matches("%/.github/workflows/external/mapping.yaml")
or
mapping.getLocation().getFile().getRelativePath() = ".github/workflows/external/mapping.yaml"
) and
refMap = mapping.lookup(ownerRepo) and
sha = refMap.lookup(ref).(YamlScalar).getValue()
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ private import DataFlowPublic
private import codeql.actions.dataflow.ExternalFlow
private import codeql.actions.dataflow.FlowSteps
private import codeql.actions.dataflow.FlowSources
private import codeql.actions.MappingHelper

class DataFlowSecondLevelScope = Unit;

Expand Down Expand Up @@ -50,7 +51,7 @@ predicate isArgumentNode(ArgumentNode arg, DataFlowCall call, ArgumentPosition p
}

DataFlowCallable nodeGetEnclosingCallable(Node node) {
node = TExprNode(any(DataFlowExpr e | result = e.getScope()))
result = node.(ExprNode).getCfgNode().(DataFlowExpr).getScope()
}

DataFlowType getNodeType(Node node) { any() }
Expand Down Expand Up @@ -80,6 +81,9 @@ class DataFlowCall instanceof Cfg::Node {

string getName() { result = super.getAstNode().(Uses).getCallee() }

/** Gets the version/ref of the action or workflow being called. */
string getVersion() { result = super.getAstNode().(Uses).getVersion() }

DataFlowCallable getEnclosingCallable() { result = super.getScope() }

/** Gets a best-effort total ordering. */
Expand Down Expand Up @@ -119,7 +123,40 @@ class NormalReturn extends ReturnKind, TNormalReturn {
}

/** Gets a viable implementation of the target of the given `Call`. */
DataFlowCallable viableCallable(DataFlowCall c) { c.getName() = result.getName() }
DataFlowCallable viableCallable(DataFlowCall c) {
// Direct name match (existing behavior for local actions and backward compatibility)
c.getName() = result.getName()
or
// SHA-based resolution via mapping.yaml for external composite actions.
// Resolves uses: owner/repo[/path]@ref to owner/repo/sha[/path] using the mapping file.
exists(string callee, string version, string ownerRepo, string sha |
callee = c.getName() and
version = c.getVersion() and
ownerRepo = callee.regexpCapture("([^/]+/[^/]+)(?:/.*)?", 1) and
externalActionRefMapping(ownerRepo, version, sha)
|
// With sub-path: e.g. actions/cache/restore@v4 -> actions/cache/{sha}/restore
exists(string subpath |
subpath = callee.regexpCapture("[^/]+/[^/]+/(.*)", 1) and
result.getName() = [ownerRepo + "/" + sha + "/" + subpath, "./" + ownerRepo + "/" + sha + "/" + subpath]
)
or
// No sub-path: e.g. actions/cache@v4 -> actions/cache/{sha}
not callee.regexpMatch("[^/]+/[^/]+/.*") and
result.getName() = [ownerRepo + "/" + sha, "./" + ownerRepo + "/" + sha]
)
or
// SHA-based resolution via mapping.yaml for external callable workflows.
// Resolves uses: owner/repo/path/to/workflow.yml@ref to owner/repo/sha/path/to/workflow.yml.
exists(string callee, string version, string ownerRepo, string sha, string workflowPath |
callee = c.getName() and
version = c.getVersion() and
ownerRepo = callee.regexpCapture("([^/]+/[^/]+)(?:/.*)?", 1) and
workflowPath = callee.regexpCapture("[^/]+/[^/]+/(.*)", 1) and
externalWorkflowRefMapping(ownerRepo, version, sha) and
result.getName() = [ownerRepo + "/" + sha + "/" + workflowPath, "./" + ownerRepo + "/" + sha + "/" + workflowPath]
)
}

/**
* Gets a node that can read the value returned from `call` with return kind
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,8 @@ where
) and
// the job writes to the cache
// (No need to follow the checkout/download step since the cache is normally write after the job completes)
job.getAStep() = step and
// Check both direct job steps and steps inside composite actions called from the job.
step.getEnclosingJob() = job and
step instanceof CacheWritingStep and
(
// we dont know what code can be controlled by the attacker
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
name: Legacy Action (no SHA)
description: An external action stored without SHA directory, for backward compat testing

runs:
using: composite
steps:
- shell: bash
run: echo "Legacy action without SHA-based resolution"
Comment on lines +7 to +8
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
name: Test Sub-Path Action
description: A composite action at a sub-path for testing mapping resolution
inputs:
path:
description: Path to restore
required: true

runs:
using: composite
steps:
- shell: bash
run: |
echo "Restoring ${{ inputs.path }}"
Comment on lines +11 to +13
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
name: Test Composite Action
description: A simple composite action for testing mapping resolution
inputs:
message:
description: A test message
required: true

runs:
using: composite
steps:
- shell: bash
run: |
echo "${{ inputs.message }}"
Comment on lines +11 to +13
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
name: Test Composite Action v2
description: A v2 composite action for testing mapping resolution
inputs:
message:
description: A test message
required: true

runs:
using: composite
steps:
- shell: bash
run: |
echo "v2: ${{ inputs.message }}"
Comment on lines +11 to +13
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
TestOrg/TestAction:
v1: abc123def456
v2: def789abc012
TestOrg/TestAction-Sub:
main: fedcba987654
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
name: Test Mapping Resolution

on:
push:

jobs:
test-simple:
runs-on: ubuntu-latest
steps:
- name: Call external action v1
uses: TestOrg/TestAction@v1
with:
message: "hello from v1"

- name: Call external action v2
uses: TestOrg/TestAction@v2
with:
message: "hello from v2"

test-subpath:
runs-on: ubuntu-latest
steps:
- name: Call sub-path action
uses: TestOrg/TestAction-Sub/sub-action@main
with:
path: "./my-cache"

test-legacy:
runs-on: ubuntu-latest
steps:
- name: Call legacy action (no mapping.yaml entry)
uses: LegacyOrg/LegacyAction@v1
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
actionMappings
| TestOrg/TestAction | v1 | abc123def456 |
| TestOrg/TestAction | v2 | def789abc012 |
| TestOrg/TestAction-Sub | main | fedcba987654 |
compositeActions
| .github/actions/external/LegacyOrg/LegacyAction/action.yml:1:1:8:61 | name: L ... no SHA) |
| .github/actions/external/TestOrg/TestAction-Sub/fedcba987654/sub-action/action.yml:1:1:13:42 | name: T ... Action |
| .github/actions/external/TestOrg/TestAction/abc123def456/action.yml:1:1:13:35 | name: T ... Action |
| .github/actions/external/TestOrg/TestAction/def789abc012/action.yml:1:1:13:39 | name: T ... tion v2 |
calls
| .github/workflows/test.yml:10:9:15:6 | Uses Step |
| .github/workflows/test.yml:15:9:20:2 | Uses Step |
| .github/workflows/test.yml:23:9:28:2 | Uses Step |
| .github/workflows/test.yml:31:9:32:40 | Uses Step |
resolvedCalls
| .github/workflows/test.yml:10:9:15:6 | Uses Step | ./TestOrg/TestAction/abc123def456 |
| .github/workflows/test.yml:10:9:15:6 | Uses Step | TestOrg/TestAction/abc123def456 |
| .github/workflows/test.yml:15:9:20:2 | Uses Step | ./TestOrg/TestAction/def789abc012 |
| .github/workflows/test.yml:15:9:20:2 | Uses Step | TestOrg/TestAction/def789abc012 |
| .github/workflows/test.yml:23:9:28:2 | Uses Step | ./TestOrg/TestAction-Sub/fedcba987654/sub-action |
| .github/workflows/test.yml:23:9:28:2 | Uses Step | TestOrg/TestAction-Sub/fedcba987654/sub-action |
| .github/workflows/test.yml:31:9:32:40 | Uses Step | ./LegacyOrg/LegacyAction |
| .github/workflows/test.yml:31:9:32:40 | Uses Step | LegacyOrg/LegacyAction |
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import codeql.actions.Ast
import codeql.actions.DataFlow
import codeql.actions.MappingHelper

/**
* Lists all mapping entries found in mapping.yaml files.
*/
query predicate actionMappings(string ownerRepo, string ref, string sha) {
externalActionRefMapping(ownerRepo, ref, sha)
}

/**
* Lists all composite actions discovered and their resolved paths.
*/
query predicate compositeActions(CompositeAction ca) { any() }

/**
* Lists all calls (uses: steps) and their callee names.
*/
query predicate calls(DataFlow::CallNode c) { any() }

/**
* Lists resolved call targets: for each call node, which callable does it resolve to.
* This exercises viableCallable through the public CallNode.getCalleeNode() API.
*/
query predicate resolvedCalls(DataFlow::CallNode c, string targetName) {
targetName = c.getCalleeNode().getName()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
name: Cache Setup Action
description: A composite action that wraps actions/cache for testing

runs:
using: composite
steps:
- uses: actions/cache@v4
with:
path: ./poison
key: poison-cache-key
Comment on lines +7 to +10
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
TestOrg/CacheAction:
v1: sha123abc
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
name: Cache Poisoning Via External Composite Action

on:
pull_request_target:
branches:
- main

permissions: read-all

jobs:
poison-via-external:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
ref: ${{ github.event.pull_request.head.sha }}
- uses: TestOrg/CacheAction@v1
- run: |
cat poison
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
edges
| .github/actions/external/TestOrg/CacheAction/sha123abc/action.yml:7:5:10:28 | Uses Step | .github/workflows/direct_cache7.yml:18:9:19:21 | Run Step |
| .github/workflows/code_injection2.yml:12:9:16:6 | Uses Step: modified_files | .github/workflows/code_injection2.yml:16:9:16:71 | Run Step |
| .github/workflows/direct_cache1.yml:10:9:13:6 | Uses Step: comment-branch | .github/workflows/direct_cache1.yml:13:9:18:6 | Uses Step |
| .github/workflows/direct_cache1.yml:13:9:18:6 | Uses Step | .github/workflows/direct_cache1.yml:18:9:22:6 | Uses Step |
Expand All @@ -14,6 +15,9 @@ edges
| .github/workflows/direct_cache5.yml:17:9:21:6 | Uses Step | .github/workflows/direct_cache5.yml:21:9:22:21 | Run Step |
| .github/workflows/direct_cache6.yml:13:9:16:6 | Uses Step | .github/workflows/direct_cache6.yml:16:9:20:6 | Uses Step |
| .github/workflows/direct_cache6.yml:16:9:20:6 | Uses Step | .github/workflows/direct_cache6.yml:20:9:26:46 | Uses Step: cache-pip |
| .github/workflows/direct_cache7.yml:14:9:17:6 | Uses Step | .github/workflows/direct_cache7.yml:17:9:18:6 | Uses Step |
| .github/workflows/direct_cache7.yml:17:9:18:6 | Uses Step | .github/actions/external/TestOrg/CacheAction/sha123abc/action.yml:7:5:10:28 | Uses Step |
| .github/workflows/direct_cache7.yml:17:9:18:6 | Uses Step | .github/workflows/direct_cache7.yml:18:9:19:21 | Run Step |
| .github/workflows/neg_direct_cache1.yml:14:9:17:6 | Uses Step | .github/workflows/neg_direct_cache1.yml:17:9:21:6 | Uses Step |
| .github/workflows/neg_direct_cache1.yml:17:9:21:6 | Uses Step | .github/workflows/neg_direct_cache1.yml:21:9:22:21 | Run Step |
| .github/workflows/neg_direct_cache2.yml:14:9:17:6 | Uses Step | .github/workflows/neg_direct_cache2.yml:17:9:21:6 | Uses Step |
Expand Down Expand Up @@ -44,6 +48,7 @@ edges
| .github/workflows/poisonable_step5.yml:17:9:22:6 | Uses Step | .github/workflows/poisonable_step5.yml:22:9:24:6 | Uses Step |
| .github/workflows/poisonable_step5.yml:22:9:24:6 | Uses Step | .github/workflows/poisonable_step5.yml:24:9:28:31 | Uses Step |
#select
| .github/actions/external/TestOrg/CacheAction/sha123abc/action.yml:7:5:10:28 | Uses Step | .github/workflows/direct_cache7.yml:14:9:17:6 | Uses Step | .github/actions/external/TestOrg/CacheAction/sha123abc/action.yml:7:5:10:28 | Uses Step | Potential cache poisoning in the context of the default branch due to privilege checkout of untrusted code. ($@). | .github/workflows/direct_cache7.yml:4:3:4:21 | pull_request_target | pull_request_target |
| .github/workflows/direct_cache1.yml:18:9:22:6 | Uses Step | .github/workflows/direct_cache1.yml:13:9:18:6 | Uses Step | .github/workflows/direct_cache1.yml:18:9:22:6 | Uses Step | Potential cache poisoning in the context of the default branch due to privilege checkout of untrusted code. ($@). | .github/workflows/direct_cache1.yml:2:3:2:15 | issue_comment | issue_comment |
| .github/workflows/direct_cache2.yml:14:9:18:6 | Uses Step | .github/workflows/direct_cache2.yml:11:9:14:6 | Uses Step | .github/workflows/direct_cache2.yml:14:9:18:6 | Uses Step | Potential cache poisoning in the context of the default branch due to privilege checkout of untrusted code. ($@). | .github/workflows/direct_cache2.yml:3:5:3:23 | pull_request_target | pull_request_target |
| .github/workflows/direct_cache3.yml:19:9:23:6 | Uses Step | .github/workflows/direct_cache3.yml:14:9:19:6 | Uses Step | .github/workflows/direct_cache3.yml:19:9:23:6 | Uses Step | Potential cache poisoning in the context of the default branch due to privilege checkout of untrusted code. ($@). | .github/workflows/direct_cache3.yml:2:3:2:15 | issue_comment | issue_comment |
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
edges
| .github/actions/external/TestOrg/CacheAction/sha123abc/action.yml:7:5:10:28 | Uses Step | .github/workflows/direct_cache7.yml:18:9:19:21 | Run Step |
| .github/workflows/code_injection2.yml:12:9:16:6 | Uses Step: modified_files | .github/workflows/code_injection2.yml:16:9:16:71 | Run Step |
| .github/workflows/direct_cache1.yml:10:9:13:6 | Uses Step: comment-branch | .github/workflows/direct_cache1.yml:13:9:18:6 | Uses Step |
| .github/workflows/direct_cache1.yml:13:9:18:6 | Uses Step | .github/workflows/direct_cache1.yml:18:9:22:6 | Uses Step |
Expand All @@ -14,6 +15,9 @@ edges
| .github/workflows/direct_cache5.yml:17:9:21:6 | Uses Step | .github/workflows/direct_cache5.yml:21:9:22:21 | Run Step |
| .github/workflows/direct_cache6.yml:13:9:16:6 | Uses Step | .github/workflows/direct_cache6.yml:16:9:20:6 | Uses Step |
| .github/workflows/direct_cache6.yml:16:9:20:6 | Uses Step | .github/workflows/direct_cache6.yml:20:9:26:46 | Uses Step: cache-pip |
| .github/workflows/direct_cache7.yml:14:9:17:6 | Uses Step | .github/workflows/direct_cache7.yml:17:9:18:6 | Uses Step |
| .github/workflows/direct_cache7.yml:17:9:18:6 | Uses Step | .github/actions/external/TestOrg/CacheAction/sha123abc/action.yml:7:5:10:28 | Uses Step |
| .github/workflows/direct_cache7.yml:17:9:18:6 | Uses Step | .github/workflows/direct_cache7.yml:18:9:19:21 | Run Step |
| .github/workflows/neg_direct_cache1.yml:14:9:17:6 | Uses Step | .github/workflows/neg_direct_cache1.yml:17:9:21:6 | Uses Step |
| .github/workflows/neg_direct_cache1.yml:17:9:21:6 | Uses Step | .github/workflows/neg_direct_cache1.yml:21:9:22:21 | Run Step |
| .github/workflows/neg_direct_cache2.yml:14:9:17:6 | Uses Step | .github/workflows/neg_direct_cache2.yml:17:9:21:6 | Uses Step |
Expand Down
Loading