Skip to content

Commit 2120f57

Browse files
authored
Reapply [workflows] Split pr-code-format into two parts to make it more secure (#78215) (#80495)
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 e64e15e commit 2120f57

File tree

3 files changed

+167
-7
lines changed

3 files changed

+167
-7
lines changed

.github/workflows/issue-write.yml

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
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+
uses: actions/download-artifact@6b208ae046db98c579e8a3aa621ab581ff575935 # v4.1.1
22+
with:
23+
github-token: ${{ secrets.ISSUE_WRITE_DOWNLOAD_ARTIFACT }}
24+
run-id: ${{ github.event.workflow_run.id }}
25+
name: workflow-args
26+
27+
- name: 'Comment on PR'
28+
uses: actions/github-script@v3
29+
with:
30+
github-token: ${{ secrets.GITHUB_TOKEN }}
31+
script: |
32+
var fs = require('fs');
33+
const comments = JSON.parse(fs.readFileSync('./comments'));
34+
if (!comments) {
35+
return;
36+
}
37+
38+
let runInfo = await github.actions.getWorkflowRun({
39+
owner: context.repo.owner,
40+
repo: context.repo.repo,
41+
run_id: context.payload.workflow_run.id
42+
});
43+
44+
console.log(runInfo);
45+
46+
47+
// Query to find the number of the pull request that triggered this job.
48+
// The associated pull requests are based off of the branch name, so if
49+
// you create a pull request for a branch, close it, and then create
50+
// another pull request with the same branch, then this query will return
51+
// two associated pull requests. This is why we have to fetch all the
52+
// associated pull requests and then iterate through them to find the
53+
// one that is open.
54+
const gql_query = `
55+
query($repo_owner : String!, $repo_name : String!, $branch: String!) {
56+
repository(owner: $repo_owner, name: $repo_name) {
57+
ref (qualifiedName: $branch) {
58+
associatedPullRequests(first: 100) {
59+
nodes {
60+
baseRepository {
61+
owner {
62+
login
63+
}
64+
}
65+
number
66+
state
67+
}
68+
}
69+
}
70+
}
71+
}
72+
`
73+
const gql_variables = {
74+
repo_owner: runInfo.data.head_repository.owner.login,
75+
repo_name: runInfo.data.head_repository.name,
76+
branch: runInfo.data.head_branch
77+
}
78+
const gql_result = await github.graphql(gql_query, gql_variables);
79+
console.log(gql_result);
80+
console.log(gql_result.repository.ref.associatedPullRequests.nodes);
81+
82+
var pr_number = 0;
83+
gql_result.repository.ref.associatedPullRequests.nodes.forEach((pr) => {
84+
if (pr.baseRepository.owner.login = context.repo.owner && pr.state == 'OPEN') {
85+
pr_number = pr.number;
86+
}
87+
});
88+
if (pr_number == 0) {
89+
console.log("Error retrieving pull request number");
90+
return;
91+
}
92+
93+
await comments.forEach(function (comment) {
94+
if (comment.id) {
95+
// Security check: Ensure that this comment was created by
96+
// the github-actions bot, so a malicious input won't overwrite
97+
// a user's comment.
98+
github.issues.getComment({
99+
owner: context.repo.owner,
100+
repo: context.repo.repo,
101+
comment_id: comment.id
102+
}).then((old_comment) => {
103+
console.log(old_comment);
104+
if (old_comment.data.user.login != "github-actions[bot]") {
105+
console.log("Invalid comment id: " + comment.id);
106+
return;
107+
}
108+
github.issues.updateComment({
109+
owner: context.repo.owner,
110+
repo: context.repo.repo,
111+
issue_number: pr_number,
112+
comment_id: comment.id,
113+
body: comment.body
114+
});
115+
});
116+
} else {
117+
github.issues.createComment({
118+
owner: context.repo.owner,
119+
repo: context.repo.repo,
120+
issue_number: pr_number,
121+
body: comment.body
122+
});
123+
}
124+
});
125+
126+
- name: Dump comments file
127+
if: always()
128+
run: cat comments

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

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,9 @@
11
name: "Check code formatting"
22
on:
3-
pull_request_target:
3+
pull_request:
44
branches:
55
- main
66

7-
permissions:
8-
pull-requests: write
9-
107
jobs:
118
code_formatter:
129
runs-on: ubuntu-latest
@@ -31,12 +28,13 @@ jobs:
3128
separator: ","
3229
skip_initial_fetch: true
3330

34-
# We need to make sure that we aren't executing/using any code from the
35-
# PR for security reasons as we're using pull_request_target. Checkout
36-
# the target branch with the necessary files.
31+
# We need to pull the script from the main branch, so that we ensure
32+
# we get the latest version of this script.
3733
- name: Fetch code formatting utils
3834
uses: actions/checkout@v4
3935
with:
36+
reository: ${{ github.repository }}
37+
ref: ${{ github.base_ref }}
4038
sparse-checkout: |
4139
llvm/utils/git/requirements_formatting.txt
4240
llvm/utils/git/code-format-helper.py
@@ -77,8 +75,16 @@ jobs:
7775
# the merge base.
7876
run: |
7977
python ./code-format-tools/llvm/utils/git/code-format-helper.py \
78+
--write-comment-to-file \
8079
--token ${{ secrets.GITHUB_TOKEN }} \
8180
--issue-number $GITHUB_PR_NUMBER \
8281
--start-rev $(git merge-base $START_REV $END_REV) \
8382
--end-rev $END_REV \
8483
--changed-files "$CHANGED_FILES"
84+
85+
- uses: actions/upload-artifact@26f96dfa697d77e81fd5907df203aa23a56210a8 #v4.3.0
86+
if: always()
87+
with:
88+
name: workflow-args
89+
path: |
90+
comments

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

Lines changed: 26 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,13 @@ 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 = {"body": comment_text}
128+
if existing_comment:
129+
self.comment["id"] = existing_comment.id
130+
return
131+
122132
if existing_comment:
123133
existing_comment.edit(comment_text)
124134
elif create_new:
@@ -310,6 +320,8 @@ def hook_main():
310320
if fmt.has_tool():
311321
if not fmt.run(args.changed_files, args):
312322
failed_fmts.append(fmt.name)
323+
if fmt.comment:
324+
comments.append(fmt.comment)
313325
else:
314326
print(f"Couldn't find {fmt.name}, can't check " + fmt.friendly_name.lower())
315327

@@ -350,6 +362,11 @@ def hook_main():
350362
type=str,
351363
help="Comma separated list of files that has been changed",
352364
)
365+
parser.add_argument(
366+
"--write-comment-to-file",
367+
action="store_true",
368+
help="Don't post comments on the PR, instead write the comments and metadata a file called 'comment'",
369+
)
353370

354371
args = FormatArgs(parser.parse_args())
355372

@@ -358,9 +375,18 @@ def hook_main():
358375
changed_files = args.changed_files.split(",")
359376

360377
failed_formatters = []
378+
comments = []
361379
for fmt in ALL_FORMATTERS:
362380
if not fmt.run(changed_files, args):
363381
failed_formatters.append(fmt.name)
382+
if fmt.comment:
383+
comments.append(fmt.comment)
384+
385+
if len(comments):
386+
with open("comments", "w") as f:
387+
import json
388+
389+
json.dump(comments, f)
364390

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

0 commit comments

Comments
 (0)