Skip to content

Commit

Permalink
Merge pull request #29 from github/toctou_queries
Browse files Browse the repository at this point in the history
TOCTOU queries
  • Loading branch information
Alvaro Muñoz authored May 14, 2024
2 parents ff2cfa5 + 73fbd23 commit 67c9648
Show file tree
Hide file tree
Showing 15 changed files with 253 additions and 8 deletions.
45 changes: 37 additions & 8 deletions ql/lib/codeql/actions/security/UntrustedCheckoutQuery.qll
Original file line number Diff line number Diff line change
Expand Up @@ -227,17 +227,46 @@ class GhSHACheckout extends SHACheckoutStep instanceof Run {
}

/** An If node that contains an actor, user or label check */
class ControlCheck extends If {
ControlCheck() {
abstract class ControlCheck extends If { }

class LabelControlCheck extends ControlCheck {
LabelControlCheck() {
// eg: contains(github.event.pull_request.labels.*.name, 'safe to test')
// eg: github.event.label.name == 'safe to test'
exists(
Utils::normalizeExpr(this.getCondition())
.regexpFind([
"\\bgithub\\.event\\.pull_request\\.labels\\b", "\\bgithub\\.event\\.label\\.name\\b"
], _, _)
)
}
}

class ActorControlCheck extends ControlCheck {
ActorControlCheck() {
// eg: contains(github.actor, 'dependabot')
// eg: github.triggering_actor != 'CI Agent'
// eg: github.event.pull_request.user.login == 'mybot'
exists(
Utils::normalizeExpr(this.getCondition())
.regexpFind([
"\\bgithub\\.actor\\b", "\\bgithub\\.triggering_actor\\b",
"\\bgithub\\.event\\.comment\\.user\\.login\\b",
"\\bgithub\\.event\\.pull_request\\.user\\.login\\b",
], _, _)
)
}
}

class AssociationControlCheck extends ControlCheck {
AssociationControlCheck() {
// eg: contains(fromJson('["MEMBER", "OWNER"]'), github.event.comment.author_association)
exists(
Utils::normalizeExpr(this.getCondition())
.regexpFind([
"\\bgithub\\.actor\\b", // actor
"\\bgithub\\.triggering_actor\\b", // actor
"\\bgithub\\.event\\.comment\\.user\\.login\\b", //user
"\\bgithub\\.event\\.pull_request\\.user\\.login\\b", //user
"\\bgithub\\.event\\.pull_request\\.labels\\b", // label
"\\bgithub\\.event\\.label\\.name\\b" // label
"\\bgithub\\.event\\.comment\\.author_association\\b",
"\\bgithub\\.event\\.issue\\.author_association\\b",
"\\bgithub\\.event\\.pull_request\\.author_association\\b",
], _, _)
)
}
Expand Down
30 changes: 30 additions & 0 deletions ql/src/Security/CWE-285/ImproperAccessControl.ql
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/**
* @name Improper Access Control
* @description The access control mechanism is not properly implemented, allowing untrusted code to be executed in a privileged context.
* @kind problem
* @problem.severity error
* @precision high
* @security-severity 9.3
* @id actions/improper-access-control
* @tags actions
* security
* external/cwe/cwe-285
*/

import codeql.actions.security.UntrustedCheckoutQuery

from LocalJob job, LabelControlCheck check, MutableRefCheckoutStep checkout, Event event
where
job = checkout.getEnclosingJob() and
job.isPrivileged() and
job.getATriggerEvent() = event and
event.getName() = "pull_request_target" and
event.getAnActivityType() = "synchronize" and
job.getAStep() = checkout and
(
checkout.getIf() = check
or
checkout.getEnclosingJob().getIf() = check
)
select checkout, "The checked-out code can be changed after the authorization check o step $@.",
check, check.toString()
25 changes: 25 additions & 0 deletions ql/src/Security/CWE-367/UntrustedCheckoutTOCTOUCritical.ql
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/**
* @name Untrusted Checkout TOCTOU
* @description Untrusted Checkout is protected by a security check but the checked-out branch can be changed after the check.
* @kind problem
* @problem.severity error
* @precision high
* @security-severity 9.3
* @id actions/untrusted-checkout-toctou/critical
* @tags actions
* security
* external/cwe/cwe-367
*/

import actions
import codeql.actions.security.UntrustedCheckoutQuery
import codeql.actions.security.PoisonableSteps

from ControlCheck check, MutableRefCheckoutStep checkout
where
// the mutable checkout step is protected by an access check
check = [checkout.getIf(), checkout.getEnclosingJob().getIf()] and
// the checked-out code may lead to arbitrary code execution
checkout.getAFollowingStep() instanceof PoisonableStep
select checkout, "The checked-out code can be changed after the authorization check o step $@.",
check, check.toString()
25 changes: 25 additions & 0 deletions ql/src/Security/CWE-367/UntrustedCheckoutTOCTOUHigh.ql
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/**
* @name Untrusted Checkout TOCTOU
* @description Untrusted Checkout is protected by a security check but the checked-out branch can be changed after the check.
* @kind problem
* @problem.severity warning
* @precision medium
* @security-severity 5.3
* @id actions/untrusted-checkout-toctou/high
* @tags actions
* security
* external/cwe/cwe-367
*/

import actions
import codeql.actions.security.UntrustedCheckoutQuery
import codeql.actions.security.PoisonableSteps

from ControlCheck check, MutableRefCheckoutStep checkout
where
// the mutable checkout step is protected by an access check
check = [checkout.getIf(), checkout.getEnclosingJob().getIf()] and
// there are no evidences that the checked-out code can lead to arbitrary code execution
not checkout.getAFollowingStep() instanceof PoisonableStep
select checkout, "The checked-out code can be changed after the authorization check o step $@.",
check, check.toString()
20 changes: 20 additions & 0 deletions ql/test/query-tests/Security/CWE-285/.github/workflows/test1.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
name: Pull request feedback

on:
pull_request_target:
types: [ opened, synchronize ]

permissions: {}
jobs:
test:
permissions:
contents: write
pull-requests: write
runs-on: ubuntu-latest
steps:
- name: Checkout repo for OWNER TEST
uses: actions/checkout@v3
if: contains(github.event.pull_request.labels.*.name, 'safe to test')
with:
ref: ${{ github.event.pull_request.head.ref }}
- run: ./cmd
20 changes: 20 additions & 0 deletions ql/test/query-tests/Security/CWE-285/.github/workflows/test2.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
name: Pull request feedback

on:
pull_request_target:
types: [ labeled ]

permissions: {}
jobs:
test:
permissions:
contents: write
pull-requests: write
runs-on: ubuntu-latest
steps:
- name: Checkout repo for OWNER TEST
uses: actions/checkout@v3
if: contains(github.event.pull_request.labels.*.name, 'safe to test')
with:
ref: ${{ github.event.pull_request.head.ref }}
- run: ./cmd
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
| .github/workflows/test1.yml:15:7:20:4 | Uses Step | The checked-out code can be changed after the authorization check o step $@. | .github/workflows/test1.yml:17:11:17:75 | contain ... test') | contain ... test') |
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Security/CWE-285/ImproperAccessControl.ql

41 changes: 41 additions & 0 deletions ql/test/query-tests/Security/CWE-367/.github/workflows/comment.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# https://github.com/AdnaneKhan/ActionsTOCTOU/blob/main/.github/workflows/comment_victim.yml
name: Comment Triggered Test
on:
issue_comment:
types: [created]
permissions: 'write-all'
jobs:
benchmark:
name: Integration Tests
if: ${{ github.event.issue.pull_request && contains(fromJson('["MEMBER", "OWNER"]'), github.event.comment.author_association) && startsWith(github.event.comment.body, '/run-tests ') }}
runs-on: [ubuntu-latest]
steps:

# test1
- uses: actions/github-script@v6
name: Get PR branch
id: issue
with:
script: |
const pr = context.payload.issue.number
const data = await github.rest.pulls.get({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: pr
})
return {
ref: data.data.head.ref,
sha: data.data.head.sha,
}
- uses: actions/checkout@v4
with:
submodules: recursive
ref: ${{ fromJson(steps.issue.outputs.result).sha }}
- run: bash comment_example/tests.sh

# test2
- uses: actions/checkout@v4
with:
submodules: recursive
ref: "refs/pull/${{ github.event.number }}/merge"
- run: bash comment_example/tests.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# https://github.com/AdnaneKhan/ActionsTOCTOU/blob/main/.github/workflows/deployment_victim.yml
name: Environment PR Check

on:
pull_request_target:
branches:
- main
paths:
- 'README.md'
workflow_dispatch:
jobs:
test:
environment: Public CI
runs-on: ubuntu-latest
steps:
- name: Checkout from PR branch
uses: actions/checkout@v4
with:
repository: ${{ github.event.pull_request.head.repo.full_name }}
ref: ${{ github.event.pull_request.head.ref }}

- name: Set Node.js 20.x for GitHub Action
uses: actions/setup-node@v4
with:
node-version: 20.x

- name: installing node_modules
run: cd deployment_example && npm install

- name: Build GitHub Action
run: cd deployment_example && npm run build
17 changes: 17 additions & 0 deletions ql/test/query-tests/Security/CWE-367/.github/workflows/label.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# https://github.com/AdnaneKhan/ActionsTOCTOU/blob/main/.github/workflows/label_victim.yml
name: Label Trigger Test
on:
pull_request_target:
types: [labeled]
branches: [main]

jobs:
integration-tests:
runs-on: ubuntu-latest
if: contains(github.event.pull_request.labels.*.name, 'safe-to-test')
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.ref }}
repository: ${{ github.event.pull_request.head.repo.full_name }}
- run: bash label_example/tests.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
| .github/workflows/comment.yml:37:9:41:6 | Uses Step | The checked-out code can be changed after the authorization check o step $@. | .github/workflows/comment.yml:10:9:10:188 | ${{ git ... s ') }} | ${{ git ... s ') }} |
| .github/workflows/label.yml:13:9:17:6 | Uses Step | The checked-out code can be changed after the authorization check o step $@. | .github/workflows/label.yml:11:9:11:73 | contain ... -test') | contain ... -test') |
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Security/CWE-367/UntrustedCheckoutTOCTOUCritical.ql
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Security/CWE-367/UntrustedCheckoutTOCTOUHigh.ql

0 comments on commit 67c9648

Please sign in to comment.