forked from jflournoy/for-funsies
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcheck_workflow_injection.py
More file actions
62 lines (51 loc) · 2.01 KB
/
Copy pathcheck_workflow_injection.py
File metadata and controls
62 lines (51 loc) · 2.01 KB
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
#!/usr/bin/env python3
"""Fail if a workflow interpolates PR-authored text into a shell block.
GitHub Actions expands expressions *before* bash runs, so PR-authored text
placed directly in a `run:` block is executed on the runner. A pull request
titled with backticks is then arbitrary code execution. The safe pattern is to
pass such values through `env:` and reference them as shell variables.
This repository exists to invite pull requests from untrusted autonomous
agents, so this check is not theoretical.
Usage: python scripts/check_workflow_injection.py
"""
import glob
import re
import sys
# Fields an outside contributor controls the contents of.
TAINTED = re.compile(r"github\.event\.(pull_request|issue|comment|review)\.")
EXPANSION = "$" + "{{"
RUN_KEY = re.compile(r"\s*run:")
def check(path: str) -> list[str]:
problems = []
in_run = False
indent = 0
for n, line in enumerate(open(path), 1):
m = RUN_KEY.match(line)
if m:
in_run = True
indent = len(line) - len(line.lstrip())
continue
if in_run and line.strip():
# A line at or left of the `run:` key ends the block.
if len(line) - len(line.lstrip()) <= indent:
in_run = False
if in_run and EXPANSION in line and TAINTED.search(line):
problems.append(
f"::error file={path},line={n}::Untrusted PR text is interpolated "
f"into a run: block. Pass it via env: and use \"$VAR\" instead."
)
return problems
def main() -> int:
paths = sorted(
glob.glob(".github/workflows/*.yml") + glob.glob(".github/workflows/*.yaml")
)
problems = [p for path in paths for p in check(path)]
for p in problems:
print(p)
if problems:
print(f"\n{len(problems)} injectable interpolation(s) found.", file=sys.stderr)
return 1
print(f"No injectable interpolation in {len(paths)} workflow(s): OK")
return 0
if __name__ == "__main__":
raise SystemExit(main())