-
Notifications
You must be signed in to change notification settings - Fork 503
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
✨ Add DangerousWorkflow check for imposter commits. #2789
Changes from all commits
1d3efac
8abd4c5
d0f39cc
dd48285
bd350bf
b1b6b72
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -15,6 +15,7 @@ | |||||||||||||||
package raw | ||||||||||||||||
|
||||||||||||||||
import ( | ||||||||||||||||
"context" | ||||||||||||||||
"fmt" | ||||||||||||||||
"regexp" | ||||||||||||||||
"strings" | ||||||||||||||||
|
@@ -69,17 +70,24 @@ var ( | |||||||||||||||
func DangerousWorkflow(c clients.RepoClient) (checker.DangerousWorkflowData, error) { | ||||||||||||||||
// data is shared across all GitHub workflows. | ||||||||||||||||
var data checker.DangerousWorkflowData | ||||||||||||||||
|
||||||||||||||||
v := &validateGitHubActionWorkflowPatterns{ | ||||||||||||||||
client: c, | ||||||||||||||||
} | ||||||||||||||||
|
||||||||||||||||
err := fileparser.OnMatchingFileContentDo(c, fileparser.PathMatcher{ | ||||||||||||||||
Pattern: ".github/workflows/*", | ||||||||||||||||
CaseSensitive: false, | ||||||||||||||||
}, validateGitHubActionWorkflowPatterns, &data) | ||||||||||||||||
}, v.Validate, &data) | ||||||||||||||||
|
||||||||||||||||
return data, err | ||||||||||||||||
} | ||||||||||||||||
|
||||||||||||||||
// Check file content. | ||||||||||||||||
var validateGitHubActionWorkflowPatterns fileparser.DoWhileTrueOnFileContent = func(path string, | ||||||||||||||||
content []byte, | ||||||||||||||||
type validateGitHubActionWorkflowPatterns struct { | ||||||||||||||||
client clients.RepoClient | ||||||||||||||||
} | ||||||||||||||||
|
||||||||||||||||
func (v *validateGitHubActionWorkflowPatterns) Validate(path string, content []byte, | ||||||||||||||||
args ...interface{}, | ||||||||||||||||
) (bool, error) { | ||||||||||||||||
if !fileparser.IsWorkflowFile(path) { | ||||||||||||||||
|
@@ -117,6 +125,11 @@ var validateGitHubActionWorkflowPatterns fileparser.DoWhileTrueOnFileContent = f | |||||||||||||||
return false, err | ||||||||||||||||
} | ||||||||||||||||
|
||||||||||||||||
// 3. Check for imposter commit references from forks | ||||||||||||||||
if err := validateImposterCommits(v.client, workflow, path, pdata); err != nil { | ||||||||||||||||
return false, err | ||||||||||||||||
} | ||||||||||||||||
|
||||||||||||||||
// TODO: Check other dangerous patterns. | ||||||||||||||||
return true, nil | ||||||||||||||||
} | ||||||||||||||||
|
@@ -269,3 +282,102 @@ func checkVariablesInScript(script string, pos *actionlint.Pos, | |||||||||||||||
} | ||||||||||||||||
return nil | ||||||||||||||||
} | ||||||||||||||||
|
||||||||||||||||
func validateImposterCommits(client clients.RepoClient, workflow *actionlint.Workflow, path string, | ||||||||||||||||
pdata *checker.DangerousWorkflowData, | ||||||||||||||||
) error { | ||||||||||||||||
ctx := context.TODO() | ||||||||||||||||
cache := &containsCache{ | ||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. from the perspective of the cron, it would be nice for the cron if the cache persisted between calls. I'm curious how much overlap there would be. Not sure what the best approach would be There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. for reference, running just the Dangerous-Workflow check on 39 for urllib3/urllib3 It's going to be very repo-dependent, based on their CI. |
||||||||||||||||
client: client, | ||||||||||||||||
cache: make(map[commitKey]bool), | ||||||||||||||||
} | ||||||||||||||||
for _, job := range workflow.Jobs { | ||||||||||||||||
for _, step := range job.Steps { | ||||||||||||||||
Comment on lines
+294
to
+295
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This doesn't cover re-usable workflows ( i assume they are also vulnerable). Would it make sense to loop over https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_iduses I was trying to check how the code handled this job and it wasn't being checked: scorecard/.github/workflows/goreleaser.yaml Lines 69 to 75 in b6362b1
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Reusable workflows can't be hash pinned. Would be interesting to check whether the imposter commit holds true for reusable workflows too. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Taken from the docs:
And an experiment I did last year for a different reason |
||||||||||||||||
e, ok := step.Exec.(*actionlint.ExecAction) | ||||||||||||||||
if !ok { | ||||||||||||||||
continue | ||||||||||||||||
} | ||||||||||||||||
|
||||||||||||||||
// Parse out repo / SHA. | ||||||||||||||||
ref := e.Uses.Value | ||||||||||||||||
trimmedRef := strings.TrimPrefix(ref, "actions://") | ||||||||||||||||
s := strings.Split(trimmedRef, "@") | ||||||||||||||||
if len(s) != 2 { | ||||||||||||||||
return sce.WithMessage(sce.ErrorCheckRuntime, fmt.Sprintf("unexpected reference: %s", trimmedRef)) | ||||||||||||||||
} | ||||||||||||||||
// Repo references can include paths (e.g. github/codeql-action/init) - Trim first n | ||||||||||||||||
repoSplit := strings.SplitN(s[0], "/", 3) | ||||||||||||||||
if len(repoSplit) < 2 { | ||||||||||||||||
return sce.WithMessage(sce.ErrorCheckRuntime, fmt.Sprintf("unexpected repo reference: %s", s[0])) | ||||||||||||||||
} | ||||||||||||||||
repo := strings.Join(repoSplit[:2], "/") | ||||||||||||||||
sha := s[1] | ||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we do a check here for if the SHA is actually a sha? Because this could be a tag too? which wouldn't need the imposter commit verification. The branch protection check uses something like this to check for it:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. would also need to force |
||||||||||||||||
|
||||||||||||||||
// Check if repo contains SHA - we use a cache to reduce duplicate calls to GitHub, | ||||||||||||||||
// since reachability queries can be expensive. | ||||||||||||||||
ok, err := cache.Contains(ctx, repo, sha) | ||||||||||||||||
if err != nil { | ||||||||||||||||
return err | ||||||||||||||||
} | ||||||||||||||||
if !ok { | ||||||||||||||||
pdata.Workflows = append(pdata.Workflows, | ||||||||||||||||
checker.DangerousWorkflow{ | ||||||||||||||||
File: checker.File{ | ||||||||||||||||
Path: path, | ||||||||||||||||
Type: finding.FileTypeSource, | ||||||||||||||||
Offset: fileparser.GetLineNumber(step.Pos), | ||||||||||||||||
Snippet: trimmedRef, | ||||||||||||||||
}, | ||||||||||||||||
Job: createJob(job), | ||||||||||||||||
Type: checker.DangerousWorkflowImposterReference, | ||||||||||||||||
}, | ||||||||||||||||
) | ||||||||||||||||
} | ||||||||||||||||
} | ||||||||||||||||
} | ||||||||||||||||
|
||||||||||||||||
return nil | ||||||||||||||||
} | ||||||||||||||||
|
||||||||||||||||
type commitKey struct { | ||||||||||||||||
repo, sha string | ||||||||||||||||
} | ||||||||||||||||
|
||||||||||||||||
// containsCache caches response values for whether a commit is contained in a given repo. | ||||||||||||||||
// This allows us to deduplicate work if we've already checked this commit. | ||||||||||||||||
type containsCache struct { | ||||||||||||||||
client clients.RepoClient | ||||||||||||||||
cache map[commitKey]bool | ||||||||||||||||
} | ||||||||||||||||
|
||||||||||||||||
func (c *containsCache) Contains(ctx context.Context, repo, sha string) (bool, error) { | ||||||||||||||||
key := commitKey{ | ||||||||||||||||
repo: repo, | ||||||||||||||||
sha: sha, | ||||||||||||||||
} | ||||||||||||||||
|
||||||||||||||||
// See if we've already seen (repo, sha). | ||||||||||||||||
v, ok := c.cache[key] | ||||||||||||||||
if ok { | ||||||||||||||||
return v, nil | ||||||||||||||||
} | ||||||||||||||||
|
||||||||||||||||
// If not, query subrepo for commit reachability. | ||||||||||||||||
// Make new client for referenced repo. | ||||||||||||||||
subclient, err := c.client.NewClient(repo, "", 0) | ||||||||||||||||
wlynch marked this conversation as resolved.
Show resolved
Hide resolved
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. commitSHA probably shouldn't be "" here, perhaps |
||||||||||||||||
if err != nil { | ||||||||||||||||
return false, sce.WithMessage(sce.ErrorCheckRuntime, err.Error()) | ||||||||||||||||
} | ||||||||||||||||
|
||||||||||||||||
out, err := checkImposterCommit(subclient, sha) | ||||||||||||||||
c.cache[key] = out | ||||||||||||||||
return out, err | ||||||||||||||||
} | ||||||||||||||||
|
||||||||||||||||
func checkImposterCommit(c clients.RepoClient, target string) (bool, error) { | ||||||||||||||||
ok, err := c.ContainsRevision(clients.HeadSHA, target) | ||||||||||||||||
if err != nil { | ||||||||||||||||
return false, sce.WithMessage(sce.ErrorCheckRuntime, err.Error()) | ||||||||||||||||
} | ||||||||||||||||
return ok, nil | ||||||||||||||||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
# Copyright 2021 OpenSSF Scorecard Authors | ||
# | ||
# Licensed under the Apache License, Version 2.0 (the "License"); | ||
# you may not use this file except in compliance with the License. | ||
# You may obtain a copy of the License at | ||
# | ||
# http://www.apache.org/licenses/LICENSE-2.0 | ||
# | ||
# Unless required by applicable law or agreed to in writing, software | ||
# distributed under the License is distributed on an "AS IS" BASIS, | ||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
# See the License for the specific language governing permissions and | ||
# limitations under the License. | ||
|
||
name: Docker | ||
jobs: | ||
push: | ||
steps: | ||
- uses: actions/checkout/foo@imposter # imposter commit |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The check is still
checker.CommitBased
in my opinionThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1