Skip to content

Commit 147c1d2

Browse files
committed
[workflows] Split pr-code-format into two parts to make it more secure
Actions triggered by pull_request_target events have access to all repository secrets, so it is unsafe to use them when executing untrusted code. The pr-code-format workflow does not execute any untrusted code, but it passes untrused input into clang-format. An attacker could use this to exploit a flaw in clang-format and potentially gain access to the repository secrets. By splitting the workflow, we can use the pull_request target which is more secure and isolate the issue write permissions in a separate job. The pull_request target also makes it easier to test changes to the code-format-helepr.py script, because the version of the script from the pull request will be used rather than the version of the script from main. Fixes #77142
1 parent 58d5a48 commit 147c1d2

File tree

3 files changed

+111
-18
lines changed

3 files changed

+111
-18
lines changed

.github/workflows/issue-write.yml

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
name: Comment on an issue
2+
3+
on:
4+
workflow_run:
5+
workflows: ["Check code formatting"]
6+
types:
7+
- completed
8+
9+
permissions:
10+
contents: read
11+
12+
jobs:
13+
pr-comment:
14+
runs-on: ubuntu-latest
15+
permissions:
16+
pull-requests: write
17+
if: >
18+
github.event.workflow_run.event == 'pull_request'
19+
steps:
20+
- name: 'Download artifact'
21+
# v7.0.1
22+
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea
23+
with:
24+
script: |
25+
let allArtifacts = await github.rest.actions.listWorkflowRunArtifacts({
26+
owner: context.repo.owner,
27+
repo: context.repo.repo,
28+
run_id: context.payload.workflow_run.id,
29+
});
30+
let matchArtifact = allArtifacts.data.artifacts.filter((artifact) => {
31+
return artifact.name == "workflow-args"
32+
})[0];
33+
let download = await github.rest.actions.downloadArtifact({
34+
owner: context.repo.owner,
35+
repo: context.repo.repo,
36+
artifact_id: matchArtifact.id,
37+
archive_format: 'zip',
38+
});
39+
let fs = require('fs');
40+
fs.writeFileSync(`${process.env.GITHUB_WORKSPACE}/workflow-args.zip`, Buffer.from(download.data));
41+
42+
- run: unzip workflow-args.zip
43+
44+
- name: 'Comment on PR'
45+
uses: actions/github-script@v3
46+
with:
47+
github-token: ${{ secrets.GITHUB_TOKEN }}
48+
script: |
49+
var fs = require('fs');
50+
const comments = JSON.parse(fs.readFileSync('./comments'));
51+
if (!comments) {
52+
return;
53+
}
54+
console.log(comments);
55+
await comments.forEach(function (comment) {
56+
if (comment.id) {
57+
github.issues.updateComment({
58+
owner: context.repo.owner,
59+
repo: context.repo.repo,
60+
issue_number: comment.number,
61+
comment_id: comment.id,
62+
body: comment.body
63+
});
64+
} else {
65+
github.issues.createComment({
66+
owner: context.repo.owner,
67+
repo: context.repo.repo,
68+
issue_number: comment.number,
69+
body: comment.body
70+
});
71+
}
72+
});

.github/workflows/pr-code-format.yml

Lines changed: 12 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
name: "Check code formatting"
2-
on: pull_request_target
3-
permissions:
4-
pull-requests: write
2+
on: pull_request
53

64
jobs:
75
code_formatter:
@@ -27,18 +25,6 @@ jobs:
2725
separator: ","
2826
skip_initial_fetch: true
2927

30-
# We need to make sure that we aren't executing/using any code from the
31-
# PR for security reasons as we're using pull_request_target. Checkout
32-
# the target branch with the necessary files.
33-
- name: Fetch code formatting utils
34-
uses: actions/checkout@v4
35-
with:
36-
sparse-checkout: |
37-
llvm/utils/git/requirements_formatting.txt
38-
llvm/utils/git/code-format-helper.py
39-
sparse-checkout-cone-mode: false
40-
path: code-format-tools
41-
4228
- name: "Listed files"
4329
env:
4430
CHANGED_FILES: ${{ steps.changed-files.outputs.all_changed_files }}
@@ -56,10 +42,10 @@ jobs:
5642
with:
5743
python-version: '3.11'
5844
cache: 'pip'
59-
cache-dependency-path: 'code-format-tools/llvm/utils/git/requirements_formatting.txt'
45+
cache-dependency-path: 'llvm/utils/git/requirements_formatting.txt'
6046

6147
- name: Install python dependencies
62-
run: pip install -r code-format-tools/llvm/utils/git/requirements_formatting.txt
48+
run: pip install -r llvm/utils/git/requirements_formatting.txt
6349

6450
- name: Run code formatter
6551
env:
@@ -72,9 +58,17 @@ jobs:
7258
# explicitly in code-format-helper.py and not have to diff starting at
7359
# the merge base.
7460
run: |
75-
python ./code-format-tools/llvm/utils/git/code-format-helper.py \
61+
python ./llvm/utils/git/code-format-helper.py \
62+
--write-comment-to-file \
7663
--token ${{ secrets.GITHUB_TOKEN }} \
7764
--issue-number $GITHUB_PR_NUMBER \
7865
--start-rev $(git merge-base $START_REV $END_REV) \
7966
--end-rev $END_REV \
8067
--changed-files "$CHANGED_FILES"
68+
69+
- uses: actions/upload-artifact@v2
70+
if: always()
71+
with:
72+
name: workflow-args
73+
path: |
74+
comments

llvm/utils/git/code-format-helper.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ class FormatArgs:
4444
token: str = None
4545
verbose: bool = True
4646
issue_number: int = 0
47+
write_comment_to_file: bool = False
4748

4849
def __init__(self, args: argparse.Namespace = None) -> None:
4950
if not args is None:
@@ -53,12 +54,14 @@ def __init__(self, args: argparse.Namespace = None) -> None:
5354
self.token = args.token
5455
self.changed_files = args.changed_files
5556
self.issue_number = args.issue_number
57+
self.write_comment_to_file = args.write_comment_to_file
5658

5759

5860
class FormatHelper:
5961
COMMENT_TAG = "<!--LLVM CODE FORMAT COMMENT: {fmt}-->"
6062
name: str
6163
friendly_name: str
64+
comment: dict = None
6265

6366
@property
6467
def comment_tag(self) -> str:
@@ -119,6 +122,16 @@ def update_pr(self, comment_text: str, args: FormatArgs, create_new: bool) -> No
119122
comment_text = self.comment_tag + "\n\n" + comment_text
120123

121124
existing_comment = self.find_comment(pr)
125+
126+
if args.write_comment_to_file:
127+
self.comment = {
128+
'number' : pr.number,
129+
'body' : comment_text
130+
}
131+
if existing_comment:
132+
self.comment['id'] = existing_comment.id
133+
return
134+
122135
if existing_comment:
123136
existing_comment.edit(comment_text)
124137
elif create_new:
@@ -309,6 +322,8 @@ def hook_main():
309322
if fmt.has_tool():
310323
if not fmt.run(args.changed_files, args):
311324
failed_fmts.append(fmt.name)
325+
if fmt.comment:
326+
comments.append(fmt.comment)
312327
else:
313328
print(f"Couldn't find {fmt.name}, can't check " + fmt.friendly_name.lower())
314329

@@ -349,6 +364,10 @@ def hook_main():
349364
type=str,
350365
help="Comma separated list of files that has been changed",
351366
)
367+
parser.add_argument(
368+
"--write-comment-to-file",
369+
action='store_true',
370+
help="Don't create a comments on the PR, instead write the comments and metadata a file called 'comment'" )
352371

353372
args = FormatArgs(parser.parse_args())
354373

@@ -357,9 +376,17 @@ def hook_main():
357376
changed_files = args.changed_files.split(",")
358377

359378
failed_formatters = []
379+
comments = []
360380
for fmt in ALL_FORMATTERS:
361381
if not fmt.run(changed_files, args):
362382
failed_formatters.append(fmt.name)
383+
if fmt.comment:
384+
comments.append(fmt.comment)
385+
386+
if len(comments):
387+
with open('comments', 'w') as f:
388+
import json
389+
json.dump(comments, f)
363390

364391
if len(failed_formatters) > 0:
365392
print(f"error: some formatters failed: {' '.join(failed_formatters)}")

0 commit comments

Comments
 (0)