Improve envvar injection

This commit is contained in:
Alvaro Muñoz
2024-04-08 17:12:00 +02:00
parent 45a51a9f74
commit 31a1ea9593
10 changed files with 229 additions and 15 deletions

View File

@@ -34,7 +34,7 @@ module Utils {
.regexpReplaceAll("^'", "")
.regexpReplaceAll("'$", "") or
assignment =
line.regexpCapture("(echo|Write-Output)\\s+([^'\"]*)\\s*>>\\s*(\"|')?\\$GITHUB_" +
line.regexpCapture("(echo|Write-Output)\\s+(.*)\\s*>>\\s*(\"|')?\\$GITHUB_" +
var.toUpperCase() + "(\"|')?", 2)
) and
key = assignment.splitAt("=", 0).trim() and

View File

@@ -64,3 +64,24 @@ predicate artifactToOutputStoreStep(DataFlow::Node pred, DataFlow::Node succ, Da
value.regexpMatch(["\\$\\(", "`"] + ["cat\\s+", "<"] + ".*" + ["`", "\\)"])
)
}
/**
* A downloaded artifact that gets assigned to an env var declaration.
* - uses: actions/download-artifact@v2
* - run: echo "::set-env name=id::$(<pr-id.txt)"
*/
predicate artifactToEnvStep(DataFlow::Node pred, DataFlow::Node succ) {
exists(Run run, string key, string value, ArtifactDownloadStep download |
pred.asExpr() = download and
succ.asExpr() = run and
download.getAFollowingStep() = run and
Utils::writeToGitHubEnv(run, key, value) and
value.regexpMatch(["\\$\\(", "`"] + ["cat\\s+", "<"] + ".*" + ["`", "\\)"])
)
}
class ArtifactDownloadToEnvTaintStep extends AdditionalTaintStep {
override predicate step(DataFlow::Node node1, DataFlow::Node node2) {
artifactToEnvStep(node1, node2)
}
}

View File

@@ -2,23 +2,38 @@ private import actions
private import codeql.actions.TaintTracking
private import codeql.actions.dataflow.ExternalFlow
import codeql.actions.dataflow.FlowSources
private import codeql.actions.security.ArtifactPoisoningQuery
import codeql.actions.DataFlow
predicate writeToGithubEnvSink(DataFlow::Node exprNode, string key, string value) {
exists(Expression expr, Run run, string script, string line |
script = run.getScript() and
line = script.splitAt("\n") and
key = line.regexpCapture("echo\\s+(\")?([^=]+)\\s*=(.*)(\")?\\s*>>\\s*\\$GITHUB_ENV", 2) and
value = line.regexpCapture("echo\\s+(\")?([^=]+)\\s*=(.*)(\")?\\s*>>\\s*\\$GITHUB_ENV", 3) and
expr = exprNode.asExpr() and
run.getAnScriptExpr() = expr and
value.indexOf(expr.getRawExpression()) > 0
)
class EnvVarInjectionFromExprSink extends DataFlow::Node {
EnvVarInjectionFromExprSink() {
exists(Expression expr, Run run, string script, string line, string key, string value |
script = run.getScript() and
line = script.splitAt("\n") and
Utils::extractAssignment(line, "ENV", key, value) and
expr = this.asExpr() and
run.getAnScriptExpr() = expr and
value.indexOf(expr.getRawExpression()) > 0
)
}
}
class EnvVarInjectionFromFileSink extends DataFlow::Node {
EnvVarInjectionFromFileSink() {
exists(Run run, ArtifactDownloadStep step, string value |
this.asExpr() = run and
step.getAFollowingStep() = run and
Utils::writeToGitHubEnv(run, _, value) and
// TODO: add support for other commands like `<`, `jq`, ...
value.regexpMatch(["\\$\\(", "`"] + ["cat\\s+", "<"] + ".*" + ["`", "\\)"])
)
}
}
private class EnvVarInjectionSink extends DataFlow::Node {
EnvVarInjectionSink() {
writeToGithubEnvSink(this, _, _) or
this instanceof EnvVarInjectionFromExprSink or
this instanceof EnvVarInjectionFromFileSink or
externallyDefinedSink(this, "envvar-injection")
}
}

View File

@@ -29,4 +29,4 @@ where
)
select sink.getNode(), source, sink,
"Potential environment variable injection in $@, which may be controlled by an external user.",
sink, sink.getNode().asExpr().(Expression).getRawExpression()
sink, sink.getNode().toString()

View File

@@ -25,4 +25,4 @@ where
)
select sink.getNode(), source, sink,
"Potential privileged environment variable injection in $@, which may be controlled by an external user.",
sink, sink.getNode().asExpr().(Expression).getRawExpression()
sink, sink.getNode().toString()

View File

@@ -329,7 +329,9 @@ sources
| jitterbit/get-changed-files | * | output.removed | PR changed files |
| jitterbit/get-changed-files | * | output.renamed | PR changed files |
| khan/pull-request-comment-trigger | * | output.comment_body | Comment body |
| marocchino/on_artifact | * | output.* | Downloaded artifact |
| octo-org/source-repo/.github/workflows/workflow.yml | * | output.workflow-output | Foo |
| redhat-plumbers-in-action/download-artifact | * | output.* | Downloaded artifact |
| tj-actions/branch-names | * | output.current_branch | PR current branch |
| tj-actions/branch-names | * | output.head_ref_branch | PR head branch |
| tj-actions/branch-names | * | output.ref_branch | Branch tirggering workflow run |
@@ -426,6 +428,8 @@ testNormalizeExpr
| github.event.pull_request.user['login'] | github.event.pull_request.user.login |
| github.event.pull_request['user']['login'] | github.event.pull_request.user.login |
writeToGitHubEnv
| "sha1 | $(<test-results1/sha-number)" |
| 'sha2 | $(<test-results2/sha-number)' |
| id1 | $(<pr-id1.txt) |
| id2 | $(<pr-id2.txt) |
| id3 | $(<pr-id3.txt) |
@@ -433,6 +437,8 @@ writeToGitHubEnv
| sha2 | $(<test-results2/sha-number) |
| sha3 | $(<test-results3/sha-number) |
writeToGitHubOutput
| "sha1 | $(<test-results1/sha-number)" |
| 'sha2 | $(<test-results2/sha-number)' |
| id1 | $(<pr-id1.txt) |
| id2 | $(<pr-id2.txt) |
| id3 | $(<pr-id3.txt) |

View File

@@ -0,0 +1,75 @@
name: Sonar Code Coverage Upload
on:
workflow_run:
workflows: ["Build/Test"]
types: [completed]
jobs:
sonar:
name: Sonar
runs-on: ubuntu-latest
if: github.event.workflow_run.conclusion == 'success'
steps:
- uses: actions/checkout@v4
with:
repository: ${{ github.event.workflow_run.head_repository.full_name }}
ref: ${{ github.event.workflow_run.head_branch }}
fetch-depth: 0
- name: 'Download code coverage'
uses: actions/github-script@v7
with:
script: |
let allArtifacts = await github.rest.actions.listWorkflowRunArtifacts({
owner: context.repo.owner,
repo: context.repo.repo,
run_id: context.payload.workflow_run.id,
});
let matchArtifact = allArtifacts.data.artifacts.filter((artifact) => {
return artifact.name == "oc-code-coverage"
})[0];
let download = await github.rest.actions.downloadArtifact({
owner: context.repo.owner,
repo: context.repo.repo,
artifact_id: matchArtifact.id,
archive_format: 'zip',
});
let fs = require('fs');
fs.writeFileSync(`${process.env.GITHUB_WORKSPACE}/oc-code-coverage.zip`, Buffer.from(download.data));
- name: 'Unzip code coverage'
run: unzip oc-code-coverage.zip -d coverage
- name: set env vars
run: |
echo "SONAR_PR_NUM=$(cat coverage/pr_num.txt)" >> $GITHUB_ENV
echo "SONAR_BASE=$(cat coverage/base.txt)" >> $GITHUB_ENV
echo "SONAR_HEAD=$(cat coverage/head.txt)" >> $GITHUB_ENV
# on develop branch, only run a baseline scan
- name: SonarCloud Scan (Baseline)
uses: sonarsource/sonarcloud-github-action@master
if: env.SONAR_HEAD == 'develop'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
with:
args: >
-Dsonar.scm.revision=${{ github.event.workflow_run.head_sha }}
-Dsonar.projectKey=opencost_opencost
-Dsonar.organization=opencost
-Dsonar.branch.name=develop
-Dsonar.branch.target=develop
- uses: actions/github-script@v6
with:
script: |
print("${{enb.SONAR_PR_NUM}}")
- name: SonarCloud Scan (PR)
uses: sonarsource/sonarcloud-github-action@master
if: env.SONAR_HEAD != 'develop'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
with:
args: >
-Dsonar.scm.revision=${{ github.event.workflow_run.head_sha }}
-Dsonar.pullrequest.key=${{ env.SONAR_PR_NUM }}
-Dsonar.pullrequest.branch=${{ env.SONAR_HEAD }}
-Dsonar.pullrequest.base=${{ env.SONAR_BASE }}
-Dsonar.projectKey=opencost_opencost
-Dsonar.organization=opencost

View File

@@ -1,5 +1,16 @@
edges
| .github/workflows/sonar-source.yml:17:9:37:6 | Uses Step | .github/workflows/sonar-source.yml:39:9:45:6 | Run Step |
| .github/workflows/test2.yml:17:9:47:6 | Uses Step | .github/workflows/test2.yml:47:9:52:6 | Run Step |
| .github/workflows/test3.yml:17:7:24:4 | Uses Step | .github/workflows/test3.yml:39:7:44:4 | Run Step |
| .github/workflows/test3.yml:24:7:31:4 | Uses Step | .github/workflows/test3.yml:39:7:44:4 | Run Step |
nodes
| .github/workflows/sonar-source.yml:17:9:37:6 | Uses Step | semmle.label | Uses Step |
| .github/workflows/sonar-source.yml:39:9:45:6 | Run Step | semmle.label | Run Step |
| .github/workflows/test1.yml:22:38:22:75 | github.event.pull_request.title | semmle.label | github.event.pull_request.title |
| .github/workflows/test2.yml:17:9:47:6 | Uses Step | semmle.label | Uses Step |
| .github/workflows/test2.yml:47:9:52:6 | Run Step | semmle.label | Run Step |
| .github/workflows/test3.yml:17:7:24:4 | Uses Step | semmle.label | Uses Step |
| .github/workflows/test3.yml:24:7:31:4 | Uses Step | semmle.label | Uses Step |
| .github/workflows/test3.yml:39:7:44:4 | Run Step | semmle.label | Run Step |
subpaths
#select

View File

@@ -1,6 +1,21 @@
edges
| .github/workflows/sonar-source.yml:17:9:37:6 | Uses Step | .github/workflows/sonar-source.yml:39:9:45:6 | Run Step |
| .github/workflows/test2.yml:17:9:47:6 | Uses Step | .github/workflows/test2.yml:47:9:52:6 | Run Step |
| .github/workflows/test3.yml:17:7:24:4 | Uses Step | .github/workflows/test3.yml:39:7:44:4 | Run Step |
| .github/workflows/test3.yml:24:7:31:4 | Uses Step | .github/workflows/test3.yml:39:7:44:4 | Run Step |
nodes
| .github/workflows/sonar-source.yml:17:9:37:6 | Uses Step | semmle.label | Uses Step |
| .github/workflows/sonar-source.yml:39:9:45:6 | Run Step | semmle.label | Run Step |
| .github/workflows/test1.yml:22:38:22:75 | github.event.pull_request.title | semmle.label | github.event.pull_request.title |
| .github/workflows/test2.yml:17:9:47:6 | Uses Step | semmle.label | Uses Step |
| .github/workflows/test2.yml:47:9:52:6 | Run Step | semmle.label | Run Step |
| .github/workflows/test3.yml:17:7:24:4 | Uses Step | semmle.label | Uses Step |
| .github/workflows/test3.yml:24:7:31:4 | Uses Step | semmle.label | Uses Step |
| .github/workflows/test3.yml:39:7:44:4 | Run Step | semmle.label | Run Step |
subpaths
#select
| .github/workflows/test1.yml:22:38:22:75 | github.event.pull_request.title | .github/workflows/test1.yml:22:38:22:75 | github.event.pull_request.title | .github/workflows/test1.yml:22:38:22:75 | github.event.pull_request.title | Potential privileged environment variable injection in $@, which may be controlled by an external user. | .github/workflows/test1.yml:22:38:22:75 | github.event.pull_request.title | ${{ github.event.pull_request.title }} |
| .github/workflows/sonar-source.yml:39:9:45:6 | Run Step | .github/workflows/sonar-source.yml:17:9:37:6 | Uses Step | .github/workflows/sonar-source.yml:39:9:45:6 | Run Step | Potential privileged environment variable injection in $@, which may be controlled by an external user. | .github/workflows/sonar-source.yml:39:9:45:6 | Run Step | Run Step |
| .github/workflows/test1.yml:22:38:22:75 | github.event.pull_request.title | .github/workflows/test1.yml:22:38:22:75 | github.event.pull_request.title | .github/workflows/test1.yml:22:38:22:75 | github.event.pull_request.title | Potential privileged environment variable injection in $@, which may be controlled by an external user. | .github/workflows/test1.yml:22:38:22:75 | github.event.pull_request.title | github.event.pull_request.title |
| .github/workflows/test2.yml:47:9:52:6 | Run Step | .github/workflows/test2.yml:17:9:47:6 | Uses Step | .github/workflows/test2.yml:47:9:52:6 | Run Step | Potential privileged environment variable injection in $@, which may be controlled by an external user. | .github/workflows/test2.yml:47:9:52:6 | Run Step | Run Step |
| .github/workflows/test3.yml:39:7:44:4 | Run Step | .github/workflows/test3.yml:17:7:24:4 | Uses Step | .github/workflows/test3.yml:39:7:44:4 | Run Step | Potential privileged environment variable injection in $@, which may be controlled by an external user. | .github/workflows/test3.yml:39:7:44:4 | Run Step | Run Step |
| .github/workflows/test3.yml:39:7:44:4 | Run Step | .github/workflows/test3.yml:24:7:31:4 | Uses Step | .github/workflows/test3.yml:39:7:44:4 | Run Step | Potential privileged environment variable injection in $@, which may be controlled by an external user. | .github/workflows/test3.yml:39:7:44:4 | Run Step | Run Step |

View File

@@ -0,0 +1,71 @@
name: Sonar Code Coverage Upload
on:
workflow_run:
workflows: ["Build/Test"]
types: [completed]
jobs:
sonar:
name: Sonar
runs-on: ubuntu-latest
if: github.event.workflow_run.conclusion == 'success'
steps:
- uses: actions/checkout@v4
with:
repository: ${{ github.event.workflow_run.head_repository.full_name }}
ref: ${{ github.event.workflow_run.head_branch }}
fetch-depth: 0
- name: 'Download code coverage'
uses: actions/github-script@v7
with:
script: |
let allArtifacts = await github.rest.actions.listWorkflowRunArtifacts({
owner: context.repo.owner,
repo: context.repo.repo,
run_id: context.payload.workflow_run.id,
});
let matchArtifact = allArtifacts.data.artifacts.filter((artifact) => {
return artifact.name == "oc-code-coverage"
})[0];
let download = await github.rest.actions.downloadArtifact({
owner: context.repo.owner,
repo: context.repo.repo,
artifact_id: matchArtifact.id,
archive_format: 'zip',
});
let fs = require('fs');
fs.writeFileSync(`${process.env.GITHUB_WORKSPACE}/oc-code-coverage.zip`, Buffer.from(download.data));
- name: 'Unzip code coverage'
run: unzip oc-code-coverage.zip -d coverage
- name: set env vars
run: |
echo "SONAR_PR_NUM=$(cat coverage/pr_num.txt)" >> $GITHUB_ENV
echo "SONAR_BASE=$(cat coverage/base.txt)" >> $GITHUB_ENV
echo "SONAR_HEAD=$(cat coverage/head.txt)" >> $GITHUB_ENV
# on develop branch, only run a baseline scan
- name: SonarCloud Scan (Baseline)
uses: sonarsource/sonarcloud-github-action@master
if: env.SONAR_HEAD == 'develop'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
with:
args: >
-Dsonar.scm.revision=${{ github.event.workflow_run.head_sha }}
-Dsonar.projectKey=opencost_opencost
-Dsonar.organization=opencost
-Dsonar.branch.name=develop
-Dsonar.branch.target=develop
- name: SonarCloud Scan (PR)
uses: sonarsource/sonarcloud-github-action@master
if: env.SONAR_HEAD != 'develop'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
with:
args: >
-Dsonar.scm.revision=${{ github.event.workflow_run.head_sha }}
-Dsonar.pullrequest.key=${{ env.SONAR_PR_NUM }}
-Dsonar.pullrequest.branch=${{ env.SONAR_HEAD }}
-Dsonar.pullrequest.base=${{ env.SONAR_BASE }}
-Dsonar.projectKey=opencost_opencost
-Dsonar.organization=opencost