Skip to content

Commit afab330

Browse files
pnachtAndré Backman
authored andcommitted
🐛 Forgive job-level permissions (ossf#3162)
* Forgive all job-level permissions Signed-off-by: Pedro Kaj Kjellerup Nacht <[email protected]> * Update tests Signed-off-by: Pedro Kaj Kjellerup Nacht <[email protected]> * Replace magic number Signed-off-by: Pedro Kaj Kjellerup Nacht <[email protected]> * Rename test Signed-off-by: Pedro Kaj Kjellerup Nacht <[email protected]> * Test that multiple job-level permissions are forgiven Signed-off-by: Pedro Kaj Kjellerup Nacht <[email protected]> * Drop unused permissionIsPresent Signed-off-by: Pedro Kaj Kjellerup Nacht <[email protected]> * Update documentation Signed-off-by: Pedro Kaj Kjellerup Nacht <[email protected]> * Modify score descriptions Signed-off-by: Pedro Kaj Kjellerup Nacht <[email protected]> * Document warning for job-level permissions Signed-off-by: Pedro Kaj Kjellerup Nacht <[email protected]> * List job-level permissions that get WARNed Signed-off-by: Pedro Kaj Kjellerup Nacht <[email protected]> --------- Signed-off-by: Pedro Kaj Kjellerup Nacht <[email protected]> Signed-off-by: André Backman <[email protected]>
1 parent aacf508 commit afab330

File tree

5 files changed

+101
-57
lines changed

5 files changed

+101
-57
lines changed

checks/evaluation/permissions/permissions.go

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -56,11 +56,11 @@ func TokenPermissions(name string, c *checker.CheckRequest, r *checker.TokenPerm
5656

5757
if score != checker.MaxResultScore {
5858
return checker.CreateResultWithScore(name,
59-
"non read-only tokens detected in GitHub workflows", score)
59+
"detected GitHub workflow tokens with excessive permissions", score)
6060
}
6161

6262
return checker.CreateMaxScoreResult(name,
63-
"tokens are read-only in GitHub workflows")
63+
"GitHub workflow tokens follow principle of least privilege")
6464
}
6565

6666
func applyScorePolicy(results *checker.TokenPermissionsData, c *checker.CheckRequest) (int, error) {
@@ -325,21 +325,21 @@ func calculateScore(result map[string]permissions) int {
325325
// status: https://docs.github.com/en/rest/reference/repos#statuses.
326326
// May allow an attacker to change the result of pre-submit and get a PR merged.
327327
// Low risk: -0.5.
328-
if permissionIsPresent(perms, "statuses") {
328+
if permissionIsPresentInTopLevel(perms, "statuses") {
329329
score -= 0.5
330330
}
331331

332332
// checks.
333333
// May allow an attacker to edit checks to remove pre-submit and introduce a bug.
334334
// Low risk: -0.5.
335-
if permissionIsPresent(perms, "checks") {
335+
if permissionIsPresentInTopLevel(perms, "checks") {
336336
score -= 0.5
337337
}
338338

339339
// secEvents.
340340
// May allow attacker to read vuln reports before patch available.
341341
// Low risk: -1
342-
if permissionIsPresent(perms, "security-events") {
342+
if permissionIsPresentInTopLevel(perms, "security-events") {
343343
score--
344344
}
345345

@@ -348,7 +348,7 @@ func calculateScore(result map[string]permissions) int {
348348
// and tiny chance an attacker can trigger a remote
349349
// service with code they own if server accepts code/location var unsanitized.
350350
// Low risk: -1
351-
if permissionIsPresent(perms, "deployments") {
351+
if permissionIsPresentInTopLevel(perms, "deployments") {
352352
score--
353353
}
354354

@@ -386,11 +386,6 @@ func calculateScore(result map[string]permissions) int {
386386
return int(score)
387387
}
388388

389-
func permissionIsPresent(perms permissions, name string) bool {
390-
return permissionIsPresentInTopLevel(perms, name) ||
391-
permissionIsPresentInRunLevel(perms, name)
392-
}
393-
394389
func permissionIsPresentInTopLevel(perms permissions, name string) bool {
395390
_, ok := perms.topLevelWritePermissions[name]
396391
return ok

checks/permissions_test.go

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ func TestGithubTokenPermissions(t *testing.T) {
5353
filenames: []string{"./testdata/.github/workflows/github-workflow-permissions-run-no-codeql-write.yaml"},
5454
expected: scut.TestReturn{
5555
Error: nil,
56-
Score: checker.MaxResultScore - 1,
56+
Score: checker.MaxResultScore,
5757
NumberOfWarn: 1,
5858
NumberOfInfo: 1,
5959
NumberOfDebug: 4,
@@ -302,11 +302,11 @@ func TestGithubTokenPermissions(t *testing.T) {
302302
},
303303
},
304304
{
305-
name: "workflow jobs only",
305+
name: "penalize job-level read without top level permissions",
306306
filenames: []string{"./testdata/.github/workflows/github-workflow-permissions-jobs-only.yaml"},
307307
expected: scut.TestReturn{
308308
Error: nil,
309-
Score: 9,
309+
Score: checker.MaxResultScore - 1,
310310
NumberOfWarn: 1,
311311
NumberOfInfo: 4,
312312
NumberOfDebug: 4,
@@ -317,7 +317,7 @@ func TestGithubTokenPermissions(t *testing.T) {
317317
filenames: []string{"./testdata/.github/workflows/github-workflow-permissions-run-write-codeql-comment.yaml"},
318318
expected: scut.TestReturn{
319319
Error: nil,
320-
Score: checker.MaxResultScore - 1,
320+
Score: checker.MaxResultScore,
321321
NumberOfWarn: 1,
322322
NumberOfInfo: 1,
323323
NumberOfDebug: 4,
@@ -389,6 +389,19 @@ func TestGithubTokenPermissions(t *testing.T) {
389389
NumberOfDebug: 5,
390390
},
391391
},
392+
{
393+
name: "don't penalize job-level writes",
394+
filenames: []string{
395+
"./testdata/.github/workflows/github-workflow-permissions-run-multiple-writes.yaml",
396+
},
397+
expected: scut.TestReturn{
398+
Error: nil,
399+
Score: checker.MaxResultScore,
400+
NumberOfWarn: 7, // number of job-level write permissions
401+
NumberOfInfo: 1, // read-only top-level permissions
402+
NumberOfDebug: 4, // This is 4 + (number of actions = 0)
403+
},
404+
},
392405
}
393406
for _, tt := range tests {
394407
tt := tt // Re-initializing variable so it is not changed while executing the closure below
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# Copyright 2021 OpenSSF Scorecard Authors
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
name: write-and-read workflow
15+
on: [push]
16+
permissions: read-all
17+
18+
jobs:
19+
Explore-GitHub-Actions:
20+
runs-on: ubuntu-latest
21+
permissions:
22+
statuses: write
23+
checks: write
24+
security-events: write
25+
deployments: write
26+
contents: write
27+
packages: write
28+
actions: write
29+
steps:
30+
- run: echo "write-and-read workflow"

docs/checks.md

Lines changed: 20 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -613,13 +613,13 @@ Note: The check does not verify the signatures.
613613

614614
Risk: `High` (vulnerable to malicious code additions)
615615

616-
This check determines whether the project's automated workflows tokens are set
617-
to read-only by default. It is currently limited to repositories hosted on
618-
GitHub, and does not support other source hosting repositories (i.e., Forges).
616+
This check determines whether the project's automated workflows tokens follow the
617+
principle of least privilege. This is important because attackers may use a
618+
compromised token with write access to, for example, push malicious code into the
619+
project.
619620

620-
Setting token permissions to read-only follows the principle of least privilege.
621-
This is important because attackers may use a compromised token with write
622-
access to push malicious code into the project.
621+
It is currently limited to repositories hosted on GitHub, and does not support
622+
other source hosting repositories (i.e., Forges).
623623

624624
The highest score is awarded when the permissions definitions in each workflow's
625625
yaml file are set as read-only at the
@@ -630,25 +630,27 @@ One point is reduced from the score if all jobs have their permissions defined b
630630
This configuration is secure, but there is a chance that when a new job is added to the workflow, its job permissions could be
631631
left undefined because of human error.
632632

633-
The check cannot detect if the "read-only" GitHub permission setting is
634-
enabled, as there is no API available.
635-
636-
Additionally, points are reduced if certain write permissions are defined for a job.
633+
Though a project's score won't be penalized, the check's details will include
634+
warnings for more sensitive run-level permissions, listed below:
637635

638-
### Write permissions causing a small reduction
639-
* `statuses` - May allow an attacker to change the result of pre-submit checks and get a PR merged.
636+
* `actions` - May allow an attacker to steal GitHub secrets by approving to run an action that needs approval.
640637
* `checks` - May allow an attacker to remove pre-submit checks and introduce a bug.
641-
* `security-events` - May allow an attacker to read vulnerability reports before a patch is available. However, points are not reduced if the job utilizes a recognized action for uploading SARIF results.
642-
* `deployments` - May allow an attacker to charge repo owner by triggering VM runs, and tiny chance an attacker can trigger a remote service with code they own if server accepts code/location variables unsanitized.
643-
644-
### Write permissions causing a large reduction
645638
* `contents` - Allows an attacker to commit unreviewed code. However, points are not reduced if the job utilizes a recognized packaging action or command.
639+
* `deployments` - May allow an attacker to charge repo owner by triggering VM runs, and tiny chance an attacker can trigger a remote service with code they own if server accepts code/location variables unsanitized.
646640
* `packages` - Allows an attacker to publish packages. However, points are not reduced if the job utilizes a recognized packaging action or command.
647-
* `actions` - May allow an attacker to steal GitHub secrets by approving to run an action that needs approval.
641+
* `security-events` - May allow an attacker to read vulnerability reports before a patch is available. However, points are not reduced if the job utilizes a recognized action for uploading SARIF results.
642+
* `statuses` - May allow an attacker to change the result of pre-submit checks and get a PR merged.
643+
644+
This compromise makes it clear the maintainer has done what's possible to use those permissions safety,
645+
but allows users to identify that the permissions are used.
646+
647+
The check cannot detect if the "read-only" GitHub permission setting is
648+
enabled, as there is no API available.
648649

649650

650651
**Remediation steps**
651-
- Set permissions as `read-all` or `contents: read` as described in GitHub's [documentation](https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#permissions).
652+
- Set top-level permissions as `read-all` or `contents: read` as described in GitHub's [documentation](https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#permissions).
653+
- Set any required write permissions at the job-level. Only set the permissions required for that job; do not set `permissions: write-all` at the job level.
652654
- To help determine the permissions needed for your workflows, you may use [StepSecurity's online tool](https://app.stepsecurity.io/secureworkflow/) by ticking the "Restrict permissions for GITHUB_TOKEN". You may also tick the "Pin actions to a full length commit SHA" to fix issues found by the Pinned-dependencies check.
653655

654656
## Vulnerabilities

docs/checks/internal/checks.yaml

Lines changed: 28 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -653,13 +653,13 @@ checks:
653653
description: |
654654
Risk: `High` (vulnerable to malicious code additions)
655655
656-
This check determines whether the project's automated workflows tokens are set
657-
to read-only by default. It is currently limited to repositories hosted on
658-
GitHub, and does not support other source hosting repositories (i.e., Forges).
656+
This check determines whether the project's automated workflows tokens follow the
657+
principle of least privilege. This is important because attackers may use a
658+
compromised token with write access to, for example, push malicious code into the
659+
project.
659660
660-
Setting token permissions to read-only follows the principle of least privilege.
661-
This is important because attackers may use a compromised token with write
662-
access to push malicious code into the project.
661+
It is currently limited to repositories hosted on GitHub, and does not support
662+
other source hosting repositories (i.e., Forges).
663663
664664
The highest score is awarded when the permissions definitions in each workflow's
665665
yaml file are set as read-only at the
@@ -670,26 +670,30 @@ checks:
670670
This configuration is secure, but there is a chance that when a new job is added to the workflow, its job permissions could be
671671
left undefined because of human error.
672672
673-
The check cannot detect if the "read-only" GitHub permission setting is
674-
enabled, as there is no API available.
675-
676-
Additionally, points are reduced if certain write permissions are defined for a job.
673+
Though a project's score won't be penalized, the check's details will include
674+
warnings for more sensitive run-level permissions, listed below:
677675
678-
### Write permissions causing a small reduction
679-
* `statuses` - May allow an attacker to change the result of pre-submit checks and get a PR merged.
676+
* `actions` - May allow an attacker to steal GitHub secrets by approving to run an action that needs approval.
680677
* `checks` - May allow an attacker to remove pre-submit checks and introduce a bug.
681-
* `security-events` - May allow an attacker to read vulnerability reports before a patch is available. However, points are not reduced if the job utilizes a recognized action for uploading SARIF results.
682-
* `deployments` - May allow an attacker to charge repo owner by triggering VM runs, and tiny chance an attacker can trigger a remote service with code they own if server accepts code/location variables unsanitized.
683-
684-
### Write permissions causing a large reduction
685678
* `contents` - Allows an attacker to commit unreviewed code. However, points are not reduced if the job utilizes a recognized packaging action or command.
679+
* `deployments` - May allow an attacker to charge repo owner by triggering VM runs, and tiny chance an attacker can trigger a remote service with code they own if server accepts code/location variables unsanitized.
686680
* `packages` - Allows an attacker to publish packages. However, points are not reduced if the job utilizes a recognized packaging action or command.
687-
* `actions` - May allow an attacker to steal GitHub secrets by approving to run an action that needs approval.
681+
* `security-events` - May allow an attacker to read vulnerability reports before a patch is available. However, points are not reduced if the job utilizes a recognized action for uploading SARIF results.
682+
* `statuses` - May allow an attacker to change the result of pre-submit checks and get a PR merged.
683+
684+
This compromise makes it clear the maintainer has done what's possible to use those permissions safety,
685+
but allows users to identify that the permissions are used.
686+
687+
The check cannot detect if the "read-only" GitHub permission setting is
688+
enabled, as there is no API available.
688689
689690
remediation:
690691
- >-
691-
Set permissions as `read-all` or `contents: read` as described in
692+
Set top-level permissions as `read-all` or `contents: read` as described in
692693
GitHub's [documentation](https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#permissions).
694+
- >-
695+
Set any required write permissions at the job-level. Only set the permissions
696+
required for that job; do not set `permissions: write-all` at the job level.
693697
- >-
694698
To help determine the permissions needed for your workflows, you may use [StepSecurity's online tool](https://app.stepsecurity.io/secureworkflow/) by ticking
695699
the "Restrict permissions for GITHUB_TOKEN". You may also tick the "Pin actions to a full length commit SHA" to fix issues found
@@ -819,9 +823,9 @@ checks:
819823
820824
This check determines whether the webhook defined in the repository has a token configured to authenticate the origins of requests.
821825
remediation:
822-
- >-
823-
Check whether your service supports token authentication.
824-
- >-
825-
If there is support for token authentication, set the secret in the webhook configuration. See [Setting up a webhook](https://docs.github.com/en/developers/webhooks-and-events/webhooks/creating-webhooks#setting-up-a-webhook)
826-
- >-
827-
If there is no support for token authentication, consider implementing it by following [these directions](https://docs.github.com/en/developers/webhooks-and-events/webhooks/securing-your-webhooks).
826+
- >-
827+
Check whether your service supports token authentication.
828+
- >-
829+
If there is support for token authentication, set the secret in the webhook configuration. See [Setting up a webhook](https://docs.github.com/en/developers/webhooks-and-events/webhooks/creating-webhooks#setting-up-a-webhook)
830+
- >-
831+
If there is no support for token authentication, consider implementing it by following [these directions](https://docs.github.com/en/developers/webhooks-and-events/webhooks/securing-your-webhooks).

0 commit comments

Comments
 (0)