ossf / scorecard

OpenSSF Scorecard - Security health metrics for Open Source
https://scorecard.dev
Apache License 2.0
4.46k stars 489 forks source link

:sparkles: Add machine-readable patch to fix script injections in workflows #4218

Open pnacht opened 2 months ago

pnacht commented 2 months ago

What kind of change does this PR introduce?

What is the current behavior?

Findings have a .Remediation.Patch field which is meant to contain a machine-readable patch fixing that particular finding:

https://github.com/ossf/scorecard/blob/3155309aa81adf3395f4d62ee133b524ff316da1/finding/probe.go#L50-L52

This field currently isn't used by any Scorecard probe.

What is the new behavior (if this is a feature change)?**

This PR adds a machine-readable patch (following the "unified diff" format which users can then apply using patch or git apply) fixing all hasDangerousWorkflowScriptInjection findings.

Each finding is patched by creating a global environment variable that wraps the dangerous GitHub variable and replacing that GitHub variable by the envvar in the relevant run command. That is:

+ env:
+   ISSUE_BODY: ${{ github.event.issue.body}}
+
 jobs:
   foo:
     steps:
-      - run: echo "${{ github.event.issue.body}}"
+      - run: echo "$ISSUE_BODY"

Or, on a real example:

go run main.go \
  --repo pnacht/cronk \
  --commit 6619fad1e79493034363b8865ab5dcbf5442a76c \
  --probes hasDangerousWorkflowScriptInjection \
  --format probe | jq .
{
  "...":,
  "findings": [
    {
      "...": "...",
      "remediation": {
        "patch": "<pretty-printed below>",
        "...": "..."
      },
      "probe": "hasDangerousWorkflowScriptInjection",
    }
  ]
}

Where the patch is:

diff --git a/.github/workflows/awesome-action.yml b/.github/workflows/awesome-action.yml
index 5eeb62ef6d3e94fc63676e9f9557a9389d05a99c..d234fa3415691e8f3c4f40183770fcea755f8d2c 100644
--- a/.github/workflows/awesome-action.yml
+++ b/.github/workflows/awesome-action.yml
@@ -4,6 +4,9 @@   push:
     branches: ["main"]
   pull_request:

+env:
+  PR_BODY: ${{ github.event.pull_request.body }}
+
 jobs:
   this_is_safe:
     runs-on: ubuntu-latest
@@ -14,7 +17,7 @@         uses: actions/checkout@v3

       - name: "Print PR title"
         if: github.event_name == 'pull_request'
-        run: echo "${{ github.event.pull_request.body }}"
+        run: echo "$PR_BODY"

       - name: "Do something awesome"
         uses: super-safe/nothing-to-see-here-action@v1.2.3

The patched workflow is then validated by parsing it with actionlint. As long as the patch added no new parsing errors, it is accepted.

Which issue(s) this PR fixes

Fixes #3950

Special notes for your reviewer

Known limitations:

Open questions:

Does this PR introduce a user-facing change?

When detecting a potential script injection in a GitHub workflow, Scorecard now adds a machine-readable patch to fix the vulnerability. This patch can be applied to your project using `git apply` or `patch -p1` from the repository's root.

Thanks to @joycebrum and @diogoteles08 who helped come up with the tests and the logic to integrate with hasDangerousWorkflowScriptInjection.Run.

spencerschrock commented 2 months ago

Note: this feature is large enough it won't make the v5.0.0 cutoff, but excited to take a look later

spencerschrock commented 2 months ago

Thanks for the PR, I'll try to take a more in-depth look tomorrow but a few questions now based only on your PR description:

Each finding is patched by creating a global environment variable that wraps the dangerous GitHub variable and replacing that GitHub variable by the envvar in the relevant run command

My initial thoughts were around clobbering the environment variables, but it seems like you have a lot of test cases that deal with these scenarios. So I'll have to wait until my deep dive review tomorrow.

The patched workflow is then validated by parsing it with actionlint. As long as the patch added no new parsing errors, it is accepted.

This is a really cool validation step! Nicely done.

Questions for you

Where the patch is:

diff --git a/.github/workflows/awesome-action.yml b/.github/workflows/awesome-action.yml index 5eeb62ef6d3e94fc63676e9f9557a9389d05a99c..d234fa3415691e8f3c4f40183770fcea755f8d2c 100644

  1. Do we know if the patch will still work if the repo HEAD changes? I assume this is a git related question

  2. Any idea how expensive this remediation is? Part of my thoughts with regard to remediation is that there should be some flag to control whether or not it gets surfaced/generated.

Open question responses

Should this feature be added to the Scorecard documentation? If so, where? checks.yml/md?

In the hasDangerousWorkflowScriptInjection def.yml file would be a good starting place probably.

The logic to generate a patch diff is pretty generic and could easily be used in in other probes' Remediation.patch implementations.

Until something else wants to use it, I'd say don't worry about where it could live. I'd say a good practice is marking the code as internal until we want others thing to use the code.

So making probes/hasDangerousWorkflowScriptInjection/patch -> probes/hasDangerousWorkflowScriptInjection/internal/patch would be a good move.

pnacht commented 3 weeks ago

Sorry, I'd missed these questions before.

Where the patch is: diff --git a/.github/workflows/awesome-action.yml b/.github/workflows/awesome-action.yml index 5eeb62ef6d3e94fc63676e9f9557a9389d05a99c..d234fa3415691e8f3c4f40183770fcea755f8d2c 100644

  1. Do we know if the patch will still work if the repo HEAD changes? I assume this is a git related question

Those hashes aren't relevant; the resulting patch could be applied at any time, at any stage of the repository.

In fact, those hashes aren't even for the actual repository, they're the hashes for the "in-memory" repository used to generate the diff.

  1. Any idea how expensive this remediation is? Part of my thoughts with regard to remediation is that there should be some flag to control whether or not it gets surfaced/generated.

I don't really know how expensive this will be in the worst case. But on the vast majority of cases it'll be a no-op, since most projects don't have workflows vulnerable to script injection. Looking at the latest BQ data, out of the 1.2M projects scanned, it only found ~2.5k workflows with script injections, each of which likely only has one or two vulnerabilities.

But if we were to try to fix a "malicious" workflow with hundreds of script injections... yeah, I don't know how long that'd take (I'd still guess not too long, though?).

Should this feature be added to the Scorecard documentation? If so, where? checks.yml/md?

In the hasDangerousWorkflowScriptInjection def.yml file would be a good starting place probably.

Done. PTAL.

I added documentation to def.yml describing that each finding has the patch. I also added the patch to the markdown remediation in def.yml. This works when testing on the CLI, but I'm not 100% how it'll appear in the Security Panel, since I don't know how to test that (I tried using --format sarif with the probe, but the SARIF came out empty, so I don't know if SARIF and probes are integrated yet).

So making probes/hasDangerousWorkflowScriptInjection/patch -> probes/hasDangerousWorkflowScriptInjection/internal/patch would be a good move.

Done.

spencerschrock commented 1 week ago

also DCO and make generate-docs

spencerschrock commented 1 week ago

/scdiff generate Dangerous-Workflow

github-actions[bot] commented 1 week ago

Here's a link to the scdiff run