-
-
- Using user-controlled input in GitHub Actions may lead to
- code injection in contexts like run: or script:.
-
-
- Code injection in GitHub Actions may allow an attacker to
- exfiltrate any secrets used in the workflow and
- the temporary GitHub repository authorization token.
- The token might have write access to the repository, allowing an attacker
- to use the token to make changes to the repository.
-
-
-
-
-
- The best practice to avoid code injection vulnerabilities
- in GitHub workflows is to set the untrusted input value of the expression
- to an intermediate environment variable and then use the environment variable
- using the native syntax of the shell/script interpreter (that is, not ${{ env.VAR }}).
-
-
- It is also recommended to limit the permissions of any tokens used
- by a workflow such as the GITHUB_TOKEN.
-
-
-
-
-
- The following example lets a user inject an arbitrary shell command:
-
-
-
-
- The following example uses an environment variable, but
- still allows the injection because of the use of expression syntax:
-
-
-
-
- The following example uses shell syntax to read
- the environment variable and will prevent the attack:
-
-
-
-
-
- GitHub Security Lab Research: Keeping your GitHub Actions and workflows secure: Untrusted input.
- GitHub Docs: Security hardening for GitHub Actions.
- GitHub Docs: Permissions for the GITHUB_TOKEN.
-
-
diff --git a/javascript/ql/src/Security/CWE-094/ExpressionInjection.ql b/javascript/ql/src/Security/CWE-094/ExpressionInjection.ql
deleted file mode 100644
index 6c01edb330f0..000000000000
--- a/javascript/ql/src/Security/CWE-094/ExpressionInjection.ql
+++ /dev/null
@@ -1,270 +0,0 @@
-/**
- * @name Expression injection in Actions
- * @description Using user-controlled GitHub Actions contexts like `run:` or `script:` may allow a malicious
- * user to inject code into the GitHub action.
- * @kind problem
- * @problem.severity warning
- * @security-severity 9.3
- * @precision high
- * @id js/actions/command-injection
- * @tags actions
- * security
- * external/cwe/cwe-094
- */
-
-import javascript
-import semmle.javascript.Actions
-
-/**
- * A `script:` field within an Actions `with:` specific to `actions/github-script` action.
- *
- * For example:
- * ```
- * uses: actions/github-script@v3
- * with:
- * script: console.log('${{ github.event.pull_request.head.sha }}')
- * ```
- */
-class GitHubScript extends YamlNode, YamlString {
- GitHubScriptWith with;
-
- GitHubScript() { with.lookup("script") = this }
-
- /** Gets the `with` field this field belongs to. */
- GitHubScriptWith getWith() { result = with }
-}
-
-/**
- * A step that uses `actions/github-script` action.
- */
-class GitHubScriptStep extends Actions::Step {
- GitHubScriptStep() { this.getUses().getGitHubRepository() = "actions/github-script" }
-}
-
-/**
- * A `with:` field sibling to `uses: actions/github-script`.
- */
-class GitHubScriptWith extends YamlNode, YamlMapping {
- GitHubScriptStep step;
-
- GitHubScriptWith() { step.lookup("with") = this }
-
- /** Gets the step this field belongs to. */
- GitHubScriptStep getStep() { result = step }
-}
-
-bindingset[context]
-private predicate isExternalUserControlledIssue(string context) {
- context.regexpMatch("\\bgithub\\s*\\.\\s*event\\s*\\.\\s*issue\\s*\\.\\s*title\\b") or
- context.regexpMatch("\\bgithub\\s*\\.\\s*event\\s*\\.\\s*issue\\s*\\.\\s*body\\b")
-}
-
-bindingset[context]
-private predicate isExternalUserControlledPullRequest(string context) {
- exists(string reg |
- reg =
- [
- "\\bgithub\\s*\\.\\s*event\\s*\\.\\s*pull_request\\s*\\.\\s*title\\b",
- "\\bgithub\\s*\\.\\s*event\\s*\\.\\s*pull_request\\s*\\.\\s*body\\b",
- "\\bgithub\\s*\\.\\s*event\\s*\\.\\s*pull_request\\s*\\.\\s*head\\s*\\.\\s*label\\b",
- "\\bgithub\\s*\\.\\s*event\\s*\\.\\s*pull_request\\s*\\.\\s*head\\s*\\.\\s*repo\\s*\\.\\s*default_branch\\b",
- "\\bgithub\\s*\\.\\s*event\\s*\\.\\s*pull_request\\s*\\.\\s*head\\s*\\.\\s*repo\\s*\\.\\s*description\\b",
- "\\bgithub\\s*\\.\\s*event\\s*\\.\\s*pull_request\\s*\\.\\s*head\\s*\\.\\s*repo\\s*\\.\\s*homepage\\b",
- "\\bgithub\\s*\\.\\s*event\\s*\\.\\s*pull_request\\s*\\.\\s*head\\s*\\.\\s*ref\\b",
- "\\bgithub\\s*\\.\\s*head_ref\\b"
- ]
- |
- context.regexpMatch(reg)
- )
-}
-
-bindingset[context]
-private predicate isExternalUserControlledReview(string context) {
- context.regexpMatch("\\bgithub\\s*\\.\\s*event\\s*\\.\\s*review\\s*\\.\\s*body\\b")
-}
-
-bindingset[context]
-private predicate isExternalUserControlledComment(string context) {
- context.regexpMatch("\\bgithub\\s*\\.\\s*event\\s*\\.\\s*comment\\s*\\.\\s*body\\b")
-}
-
-bindingset[context]
-private predicate isExternalUserControlledGollum(string context) {
- context
- .regexpMatch("\\bgithub\\s*\\.\\s*event\\s*\\.\\s*pages\\[[0-9]+\\]\\s*\\.\\s*page_name\\b") or
- context.regexpMatch("\\bgithub\\s*\\.\\s*event\\s*\\.\\s*pages\\[[0-9]+\\]\\s*\\.\\s*title\\b")
-}
-
-bindingset[context]
-private predicate isExternalUserControlledCommit(string context) {
- exists(string reg |
- reg =
- [
- "\\bgithub\\s*\\.\\s*event\\s*\\.\\s*commits\\[[0-9]+\\]\\s*\\.\\s*message\\b",
- "\\bgithub\\s*\\.\\s*event\\s*\\.\\s*head_commit\\s*\\.\\s*message\\b",
- "\\bgithub\\s*\\.\\s*event\\s*\\.\\s*head_commit\\s*\\.\\s*author\\s*\\.\\s*email\\b",
- "\\bgithub\\s*\\.\\s*event\\s*\\.\\s*head_commit\\s*\\.\\s*author\\s*\\.\\s*name\\b",
- "\\bgithub\\s*\\.\\s*event\\s*\\.\\s*head_commit\\s*\\.\\s*committer\\s*\\.\\s*email\\b",
- "\\bgithub\\s*\\.\\s*event\\s*\\.\\s*head_commit\\s*\\.\\s*committer\\s*\\.\\s*name\\b",
- "\\bgithub\\s*\\.\\s*event\\s*\\.\\s*commits\\[[0-9]+\\]\\s*\\.\\s*author\\s*\\.\\s*email\\b",
- "\\bgithub\\s*\\.\\s*event\\s*\\.\\s*commits\\[[0-9]+\\]\\s*\\.\\s*author\\s*\\.\\s*name\\b",
- "\\bgithub\\s*\\.\\s*event\\s*\\.\\s*commits\\[[0-9]+\\]\\s*\\.\\s*committer\\s*\\.\\s*email\\b",
- "\\bgithub\\s*\\.\\s*event\\s*\\.\\s*commits\\[[0-9]+\\]\\s*\\.\\s*committer\\s*\\.\\s*name\\b",
- ]
- |
- context.regexpMatch(reg)
- )
-}
-
-bindingset[context]
-private predicate isExternalUserControlledDiscussion(string context) {
- context.regexpMatch("\\bgithub\\s*\\.\\s*event\\s*\\.\\s*discussion\\s*\\.\\s*title\\b") or
- context.regexpMatch("\\bgithub\\s*\\.\\s*event\\s*\\.\\s*discussion\\s*\\.\\s*body\\b")
-}
-
-bindingset[context]
-private predicate isExternalUserControlledWorkflowRun(string context) {
- exists(string reg |
- reg =
- [
- "\\bgithub\\s*\\.\\s*event\\s*\\.\\s*workflow_run\\s*\\.\\s*head_branch\\b",
- "\\bgithub\\s*\\.\\s*event\\s*\\.\\s*workflow_run\\s*\\.\\s*display_title\\b",
- "\\bgithub\\s*\\.\\s*event\\s*\\.\\s*workflow_run\\s*\\.\\s*head_repository\\b\\s*\\.\\s*description\\b",
- "\\bgithub\\s*\\.\\s*event\\s*\\.\\s*workflow_run\\s*\\.\\s*head_commit\\b\\s*\\.\\s*message\\b",
- "\\bgithub\\s*\\.\\s*event\\s*\\.\\s*workflow_run\\s*\\.\\s*head_commit\\b\\s*\\.\\s*author\\b\\s*\\.\\s*email\\b",
- "\\bgithub\\s*\\.\\s*event\\s*\\.\\s*workflow_run\\s*\\.\\s*head_commit\\b\\s*\\.\\s*author\\b\\s*\\.\\s*name\\b",
- "\\bgithub\\s*\\.\\s*event\\s*\\.\\s*workflow_run\\s*\\.\\s*head_commit\\b\\s*\\.\\s*committer\\b\\s*\\.\\s*email\\b",
- "\\bgithub\\s*\\.\\s*event\\s*\\.\\s*workflow_run\\s*\\.\\s*head_commit\\b\\s*\\.\\s*committer\\b\\s*\\.\\s*name\\b",
- ]
- |
- context.regexpMatch(reg)
- )
-}
-
-/**
- * Holds if environment name in the `injection` (in a form of `env.name`)
- * is tainted by the `context` (in a form of `github.event.xxx.xxx`).
- */
-bindingset[injection]
-predicate isEnvInterpolationTainted(string injection, string context) {
- exists(Actions::Env env, string envName, YamlString envValue |
- envValue = env.lookup(envName) and
- Actions::getEnvName(injection) = envName and
- Actions::getASimpleReferenceExpression(envValue) = context
- )
-}
-
-/**
- * Holds if the `run` contains any expression interpolation `${{ e }}`.
- * Sets `context` to the initial untrusted value assignment in case of `${{ env... }}` interpolation
- */
-predicate isRunInjectable(Actions::Run run, string injection, string context) {
- Actions::getASimpleReferenceExpression(run) = injection and
- (
- injection = context
- or
- isEnvInterpolationTainted(injection, context)
- )
-}
-
-/**
- * Holds if the `actions/github-script` contains any expression interpolation `${{ e }}`.
- * Sets `context` to the initial untrusted value assignment in case of `${{ env... }}` interpolation
- */
-predicate isScriptInjectable(GitHubScript script, string injection, string context) {
- Actions::getASimpleReferenceExpression(script) = injection and
- (
- injection = context
- or
- isEnvInterpolationTainted(injection, context)
- )
-}
-
-/**
- * Holds if the composite action contains untrusted expression interpolation `${{ e }}`.
- */
-YamlNode getInjectableCompositeActionNode(Actions::Runs runs, string injection, string context) {
- exists(Actions::Run run |
- isRunInjectable(run, injection, context) and
- result = run and
- run.getStep().getRuns() = runs
- )
- or
- exists(GitHubScript script |
- isScriptInjectable(script, injection, context) and
- result = script and
- script.getWith().getStep().getRuns() = runs
- )
-}
-
-/**
- * Holds if the workflow contains untrusted expression interpolation `${{ e }}`.
- */
-YamlNode getInjectableWorkflowNode(Actions::On on, string injection, string context) {
- exists(Actions::Run run |
- isRunInjectable(run, injection, context) and
- result = run and
- run.getStep().getJob().getWorkflow().getOn() = on
- )
- or
- exists(GitHubScript script |
- isScriptInjectable(script, injection, context) and
- result = script and
- script.getWith().getStep().getJob().getWorkflow().getOn() = on
- )
-}
-
-from YamlNode node, string injection, string context
-where
- exists(Actions::CompositeAction action, Actions::Runs runs |
- action.getRuns() = runs and
- node = getInjectableCompositeActionNode(runs, injection, context) and
- (
- isExternalUserControlledIssue(context) or
- isExternalUserControlledPullRequest(context) or
- isExternalUserControlledReview(context) or
- isExternalUserControlledComment(context) or
- isExternalUserControlledGollum(context) or
- isExternalUserControlledCommit(context) or
- isExternalUserControlledDiscussion(context) or
- isExternalUserControlledWorkflowRun(context)
- )
- )
- or
- exists(Actions::On on |
- node = getInjectableWorkflowNode(on, injection, context) and
- (
- exists(on.getNode("issues")) and
- isExternalUserControlledIssue(context)
- or
- exists(on.getNode("pull_request_target")) and
- isExternalUserControlledPullRequest(context)
- or
- exists(on.getNode("pull_request_review")) and
- (isExternalUserControlledReview(context) or isExternalUserControlledPullRequest(context))
- or
- exists(on.getNode("pull_request_review_comment")) and
- (isExternalUserControlledComment(context) or isExternalUserControlledPullRequest(context))
- or
- exists(on.getNode("issue_comment")) and
- (isExternalUserControlledComment(context) or isExternalUserControlledIssue(context))
- or
- exists(on.getNode("gollum")) and
- isExternalUserControlledGollum(context)
- or
- exists(on.getNode("push")) and
- isExternalUserControlledCommit(context)
- or
- exists(on.getNode("discussion")) and
- isExternalUserControlledDiscussion(context)
- or
- exists(on.getNode("discussion_comment")) and
- (isExternalUserControlledDiscussion(context) or isExternalUserControlledComment(context))
- or
- exists(on.getNode("workflow_run")) and
- isExternalUserControlledWorkflowRun(context)
- )
- )
-select node,
- "Potential injection from the ${{ " + injection +
- " }}, which may be controlled by an external user."
diff --git a/javascript/ql/src/Security/CWE-094/examples/comment_issue_bad.yml b/javascript/ql/src/Security/CWE-094/examples/comment_issue_bad.yml
deleted file mode 100644
index 1a25d44693b2..000000000000
--- a/javascript/ql/src/Security/CWE-094/examples/comment_issue_bad.yml
+++ /dev/null
@@ -1,8 +0,0 @@
-on: issue_comment
-
-jobs:
- echo-body:
- runs-on: ubuntu-latest
- steps:
- - run: |
- echo '${{ github.event.comment.body }}'
\ No newline at end of file
diff --git a/javascript/ql/src/Security/CWE-094/examples/comment_issue_bad_env.yml b/javascript/ql/src/Security/CWE-094/examples/comment_issue_bad_env.yml
deleted file mode 100644
index b7698938de75..000000000000
--- a/javascript/ql/src/Security/CWE-094/examples/comment_issue_bad_env.yml
+++ /dev/null
@@ -1,10 +0,0 @@
-on: issue_comment
-
-jobs:
- echo-body:
- runs-on: ubuntu-latest
- steps:
- - env:
- BODY: ${{ github.event.issue.body }}
- run: |
- echo '${{ env.BODY }}'
\ No newline at end of file
diff --git a/javascript/ql/src/Security/CWE-094/examples/comment_issue_good.yml b/javascript/ql/src/Security/CWE-094/examples/comment_issue_good.yml
deleted file mode 100644
index 07254a8b2043..000000000000
--- a/javascript/ql/src/Security/CWE-094/examples/comment_issue_good.yml
+++ /dev/null
@@ -1,10 +0,0 @@
-on: issue_comment
-
-jobs:
- echo-body:
- runs-on: ubuntu-latest
- steps:
- - env:
- BODY: ${{ github.event.issue.body }}
- run: |
- echo "$BODY"
diff --git a/javascript/ql/src/Security/CWE-312/ActionsArtifactLeak.qhelp b/javascript/ql/src/Security/CWE-312/ActionsArtifactLeak.qhelp
deleted file mode 100644
index 7ec9c1fe7770..000000000000
--- a/javascript/ql/src/Security/CWE-312/ActionsArtifactLeak.qhelp
+++ /dev/null
@@ -1,30 +0,0 @@
-
-