-
-
Notifications
You must be signed in to change notification settings - Fork 3
128 lines (106 loc) · 3.6 KB
/
template-injection.yml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
# template-injection.yml
#
# what:
# GitHub Actions has a template and expression language, delimited by
# `${{ ... }}`, that can be inserted into most parts of a workflow.
# expression evaluation and expansion happens before the workflow is
# executed, meaning that an attacker who controls the expansion of a
# template may be able to bypass normal shell quoting and escaping
# rules to inject code directly into a `runs:` step.
#
# how:
# many workflows allow free-form user inputs, which an attacker can
# use for injection. similarly, many workflows run on triggers that
# include multiple sources of attacker-controlled input, such
# as `github.event.issue.title` being whatever string the attacker creates
# a new issue with.
#
# see also: https://www.kenmuse.com/blog/github-actions-injection-attacks/
name: example
on:
issues:
workflow_dispatch:
inputs:
hackme:
type: string
workflow_call:
inputs:
hackme-call:
type: string
env:
HACKME_ENV: hello
jobs:
vulnerable-1:
runs-on: ubuntu-latest
strategy:
matrix:
frob: ["nothing", "special"]
dynamic: ${{ github.event.client_payload.mystery_meat }}
steps:
- name: vulnerable-1
# NOT OK: attacker controlled issue title
run: |
echo "issue created: ${{ github.event.issue.title }}"
- name: vulnerable-2
# NOT OK: attacker controlled workflow_dispatch input
run: |
echo "doing a thing: ${{ inputs.hackme }}"
- name: not-vulnerable-3
# OK: expanding secrets is typically OK, since an attacker does not control them
run: |
frobulate ${{ secrets.GITHUB_TOKEN }}
- name: vulnerable-4
# NOT OK: `workflow_call` inputs may or may not be trusted
run: |
echo "doing a thing: ${{ inputs.hackme-call }}"
- name: not-vulnerable-5
# OK: attacker-controlled input is neutralized by an environment variable
run: |
echo "doing a thing: ${HACKME}"
env:
HACKME: ${{ inputs.hackme }}
- name: vulnerable-6
# NOT OK: typically not attacker-controllable, but bypasses proper shell escaping
run: |
echo "doing a thing: ${{ env.HACKME_ENV }}"
- name: not-vulnerable-7
# OK: matrix.frob is static
run: |
echo "doing a thing: ${{ matrix.frob }}"
- name: vulnerable-8
# NOT OK: matrix.dynamic is dynamic
run: |
echo "doing a thing: ${{ matrix.dynamic }}"
- name: vulnerable-9
# NOT OK: matrix is has dynamic members, so referencing the entire thing includes dynamic members
run: |
echo "doing a thing: ${{ matrix }}"
- name: not-vulnerable-10
# OK: github.workspace is not attacker controlled
run: |
echo "doing a thing: ${{ github.workspace }}"
vulnerable-2:
runs-on: ubuntu-latest
strategy:
matrix: ${{ github.event.client_payload.mystery_meat }}
steps:
- name: vulnerable-11
# NOT OK: entire matrix is dynamic
run: |
echo "doing a thing: ${{ matrix.unknown-key }}"
vulnerable-3:
runs-on: ubuntu-latest
steps:
- name: vulnerable-12
uses: actions/github-script@v7
with:
# NOT OK: attacker-controlled issue title
script: |
return "doing a thing: ${{ github.event.issue.title }}"
not-vulnerable-4:
runs-on: ubuntu-latest
steps:
- name: not-vulnerable-13
# OK: safe, since the expression can only ever evaluate to a boolean
run: |
${{ some.context == 'success' }}