From c6dbbca1c5e80a17b18346e2a276b71dea31660a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nora=20Dimitrijevi=C4=87?= Date: Tue, 15 Jul 2025 11:46:59 +0200 Subject: [PATCH 1/6] [DIFF-INFORMED] Actions: ArgumentInjection Query: - https://github.com/d10c/codeql/blob/d10c/diff-informed-phase-3/actions/ql/src/experimental/Security/CWE-088/ArgumentInjectionMedium.ql#L23 - https://github.com/d10c/codeql/blob/d10c/diff-informed-phase-3/actions/ql/src/experimental/Security/CWE-088/ArgumentInjectionCritical.ql#L27 --- .../actions/security/ArgumentInjectionQuery.qll | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/actions/ql/lib/codeql/actions/security/ArgumentInjectionQuery.qll b/actions/ql/lib/codeql/actions/security/ArgumentInjectionQuery.qll index 1d461cca3df2..6ac385c78d31 100644 --- a/actions/ql/lib/codeql/actions/security/ArgumentInjectionQuery.qll +++ b/actions/ql/lib/codeql/actions/security/ArgumentInjectionQuery.qll @@ -1,6 +1,7 @@ private import actions private import codeql.actions.TaintTracking private import codeql.actions.dataflow.ExternalFlow +private import codeql.actions.security.ControlChecks import codeql.actions.dataflow.FlowSources import codeql.actions.DataFlow @@ -88,6 +89,19 @@ private module ArgumentInjectionConfig implements DataFlow::ConfigSig { run.getScript().getAnEnvReachingArgumentInjectionSink(var, _, _) ) } + + predicate observeDiffInformedIncrementalMode() { any() } + + Location getASelectedSourceLocation(DataFlow::Node source) { none() } + + Location getASelectedSinkLocation(DataFlow::Node sink) { + result = sink.getLocation() + or + exists(Event event | result = event.getLocation() | + inPrivilegedContext(sink.asExpr(), event) and + not exists(ControlCheck check | check.protects(sink.asExpr(), event, "argument-injection")) + ) + } } /** Tracks flow of unsafe user input that is used to construct and evaluate a code script. */ From f1e1f8017d6fefa29760dad2b5068235ca59852d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nora=20Dimitrijevi=C4=87?= Date: Tue, 15 Jul 2025 14:28:55 +0200 Subject: [PATCH 2/6] [DIFF-INFORMED] Actions: ArtifactPoisoning Queries: - https://github.com/d10c/codeql/blob/d10c/diff-informed-phase-3/actions/ql/src/Security/CWE-829/ArtifactPoisoningMedium.ql#L23 - https://github.com/d10c/codeql/blob/d10c/diff-informed-phase-3/actions/ql/src/Security/CWE-829/ArtifactPoisoningCritical.ql#L26 --- .../actions/security/ArtifactPoisoningQuery.qll | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/actions/ql/lib/codeql/actions/security/ArtifactPoisoningQuery.qll b/actions/ql/lib/codeql/actions/security/ArtifactPoisoningQuery.qll index f0649bb8174e..b6480a53fc9a 100644 --- a/actions/ql/lib/codeql/actions/security/ArtifactPoisoningQuery.qll +++ b/actions/ql/lib/codeql/actions/security/ArtifactPoisoningQuery.qll @@ -4,6 +4,7 @@ import codeql.actions.DataFlow import codeql.actions.dataflow.FlowSources import codeql.actions.security.PoisonableSteps import codeql.actions.security.UntrustedCheckoutQuery +import codeql.actions.security.ControlChecks string unzipRegexp() { result = "(unzip|tar)\\s+.*" } @@ -318,6 +319,19 @@ private module ArtifactPoisoningConfig implements DataFlow::ConfigSig { exists(run.getScript().getAFileReadCommand()) ) } + + predicate observeDiffInformedIncrementalMode() { any() } + + Location getASelectedSourceLocation(DataFlow::Node source) { none() } + + Location getASelectedSinkLocation(DataFlow::Node sink) { + result = sink.getLocation() + or + exists(Event event | result = event.getLocation() | + inPrivilegedContext(sink.asExpr(), event) and + not exists(ControlCheck check | check.protects(sink.asExpr(), event, "artifact-poisoning")) + ) + } } /** Tracks flow of unsafe artifacts that is used in an insecure way. */ From 35dbe85b4aa738d5f5b861761a0ae150eddec502 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nora=20Dimitrijevi=C4=87?= Date: Tue, 15 Jul 2025 14:34:15 +0200 Subject: [PATCH 3/6] [DIFF-INFORMED] Actions: CodeInjection Query: https://github.com/d10c/codeql/blob/d10c/diff-informed-phase-3/actions/ql/src/Security/CWE-349/CachePoisoningViaCodeInjection.ql#L46 --- .../actions/security/CodeInjectionQuery.qll | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/actions/ql/lib/codeql/actions/security/CodeInjectionQuery.qll b/actions/ql/lib/codeql/actions/security/CodeInjectionQuery.qll index fac498f72dab..a75ac84ef772 100644 --- a/actions/ql/lib/codeql/actions/security/CodeInjectionQuery.qll +++ b/actions/ql/lib/codeql/actions/security/CodeInjectionQuery.qll @@ -3,6 +3,8 @@ private import codeql.actions.TaintTracking private import codeql.actions.dataflow.ExternalFlow import codeql.actions.dataflow.FlowSources import codeql.actions.DataFlow +import codeql.actions.security.ControlChecks +import codeql.actions.security.CachePoisoningQuery class CodeInjectionSink extends DataFlow::Node { CodeInjectionSink() { @@ -35,6 +37,53 @@ private module CodeInjectionConfig implements DataFlow::ConfigSig { exists(run.getScript().getAFileReadCommand()) ) } + + predicate observeDiffInformedIncrementalMode() { any() } + + Location getASelectedSourceLocation(DataFlow::Node source) { none() } + + Location getASelectedSinkLocation(DataFlow::Node sink) { + result = sink.getLocation() + or + // where clause from CodeInjectionCritical.ql + exists(Event event, RemoteFlowSource source | result = event.getLocation() | + inPrivilegedContext(sink.asExpr(), event) and + isSource(source) and + source.getEventName() = event.getName() and + not exists(ControlCheck check | check.protects(sink.asExpr(), event, "code-injection")) and + // exclude cases where the sink is a JS script and the expression uses toJson + not exists(UsesStep script | + script.getCallee() = "actions/github-script" and + script.getArgumentExpr("script") = sink.asExpr() and + exists(getAToJsonReferenceExpression(sink.asExpr().(Expression).getExpression(), _)) + ) + ) + or + // where clause from CachePoisoningViaCodeInjection.ql + exists(Event event, LocalJob job, DataFlow::Node source | result = event.getLocation() | + job = sink.asExpr().getEnclosingJob() and + job.getATriggerEvent() = event and + // job can be triggered by an external user + event.isExternallyTriggerable() and + // the checkout is not controlled by an access check + isSource(source) and + not exists(ControlCheck check | check.protects(source.asExpr(), event, "code-injection")) and + // excluding privileged workflows since they can be exploited in easier circumstances + // which is covered by `actions/code-injection/critical` + not job.isPrivilegedExternallyTriggerable(event) and + ( + // the workflow runs in the context of the default branch + runsOnDefaultBranch(event) + or + // the workflow caller runs in the context of the default branch + event.getName() = "workflow_call" and + exists(ExternalJob caller | + caller.getCallee() = job.getLocation().getFile().getRelativePath() and + runsOnDefaultBranch(caller.getATriggerEvent()) + ) + ) + ) + } } /** Tracks flow of unsafe user input that is used to construct and evaluate a code script. */ From ed243bc1d160de1b3936dd90038841c44898be77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nora=20Dimitrijevi=C4=87?= Date: Tue, 15 Jul 2025 15:18:24 +0200 Subject: [PATCH 4/6] [DIFF-INFORMED] Actions: CommandInjection https://github.com/d10c/codeql/blob/d10c/diff-informed-phase-3/actions/ql/src/experimental/Security/CWE-078/CommandInjectionMedium.ql#L24 https://github.com/d10c/codeql/blob/d10c/diff-informed-phase-3/actions/ql/src/experimental/Security/CWE-078/CommandInjectionCritical.ql#L28 --- .../actions/security/CommandInjectionQuery.qll | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/actions/ql/lib/codeql/actions/security/CommandInjectionQuery.qll b/actions/ql/lib/codeql/actions/security/CommandInjectionQuery.qll index 59d523cd5827..878b7a598056 100644 --- a/actions/ql/lib/codeql/actions/security/CommandInjectionQuery.qll +++ b/actions/ql/lib/codeql/actions/security/CommandInjectionQuery.qll @@ -3,6 +3,7 @@ private import codeql.actions.TaintTracking private import codeql.actions.dataflow.ExternalFlow import codeql.actions.dataflow.FlowSources import codeql.actions.DataFlow +import codeql.actions.security.ControlChecks private class CommandInjectionSink extends DataFlow::Node { CommandInjectionSink() { madSink(this, "command-injection") } @@ -16,6 +17,22 @@ private module CommandInjectionConfig implements DataFlow::ConfigSig { predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource } predicate isSink(DataFlow::Node sink) { sink instanceof CommandInjectionSink } + + predicate observeDiffInformedIncrementalMode() { any() } + + Location getASelectedSourceLocation(DataFlow::Node source) { none() } + + Location getASelectedSinkLocation(DataFlow::Node sink) { + result = sink.getLocation() + or + // where clause from CommandInjectionCritical.ql + exists(Event event | result = event.getLocation() | + inPrivilegedContext(sink.asExpr(), event) and + not exists(ControlCheck check | + check.protects(sink.asExpr(), event, ["command-injection", "code-injection"]) + ) + ) + } } /** Tracks flow of unsafe user input that is used to construct and evaluate a system command. */ From 7b4dd0eca3ce794ee481e53a3456a897c67a21e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nora=20Dimitrijevi=C4=87?= Date: Tue, 15 Jul 2025 15:21:37 +0200 Subject: [PATCH 5/6] [DIFF-INFORMED] Actions: EnvPathInjection https://github.com/d10c/codeql/blob/d10c/diff-informed-phase-3/actions/ql/src/Security/CWE-077/EnvPathInjectionMedium.ql#L30 https://github.com/d10c/codeql/blob/d10c/diff-informed-phase-3/actions/ql/src/Security/CWE-077/EnvPathInjectionCritical.ql#L37 --- .../security/EnvPathInjectionQuery.qll | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/actions/ql/lib/codeql/actions/security/EnvPathInjectionQuery.qll b/actions/ql/lib/codeql/actions/security/EnvPathInjectionQuery.qll index 33efc9b1bc8f..0ed0d2a877e9 100644 --- a/actions/ql/lib/codeql/actions/security/EnvPathInjectionQuery.qll +++ b/actions/ql/lib/codeql/actions/security/EnvPathInjectionQuery.qll @@ -108,6 +108,30 @@ private module EnvPathInjectionConfig implements DataFlow::ConfigSig { exists(run.getScript().getAFileReadCommand()) ) } + + predicate observeDiffInformedIncrementalMode() { any() } + + Location getASelectedSourceLocation(DataFlow::Node source) { none() } + + Location getASelectedSinkLocation(DataFlow::Node sink) { + result = sink.getLocation() + or + // where clause from EnvPathInjectionCritical.ql + exists(Event event, RemoteFlowSource source | result = event.getLocation() | + inPrivilegedContext(sink.asExpr(), event) and + isSource(source) and + ( + not source.getSourceType() = "artifact" and + not exists(ControlCheck check | check.protects(sink.asExpr(), event, "code-injection")) + or + source.getSourceType() = "artifact" and + not exists(ControlCheck check | + check.protects(sink.asExpr(), event, ["untrusted-checkout", "artifact-poisoning"]) + ) and + sink instanceof EnvPathInjectionFromFileReadSink + ) + ) + } } /** Tracks flow of unsafe user input that is used to construct and evaluate the PATH environment variable. */ From 3f931cd0d943823c1f6b77e88e41a7185ca605c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nora=20Dimitrijevi=C4=87?= Date: Tue, 15 Jul 2025 15:23:10 +0200 Subject: [PATCH 6/6] [DIFF-INFORMED] Actions: EnvVarInjection https://github.com/d10c/codeql/blob/d10c/diff-informed-phase-3/actions/ql/src/Security/CWE-077/EnvVarInjectionMedium.ql#L35 https://github.com/d10c/codeql/blob/d10c/diff-informed-phase-3/actions/ql/src/Security/CWE-077/EnvVarInjectionCritical.ql#L46 --- .../actions/security/EnvVarInjectionQuery.qll | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/actions/ql/lib/codeql/actions/security/EnvVarInjectionQuery.qll b/actions/ql/lib/codeql/actions/security/EnvVarInjectionQuery.qll index 656ea1207b51..a99c346e16d3 100644 --- a/actions/ql/lib/codeql/actions/security/EnvVarInjectionQuery.qll +++ b/actions/ql/lib/codeql/actions/security/EnvVarInjectionQuery.qll @@ -163,6 +163,40 @@ private module EnvVarInjectionConfig implements DataFlow::ConfigSig { exists(run.getScript().getAFileReadCommand()) ) } + + predicate observeDiffInformedIncrementalMode() { any() } + + Location getASelectedSourceLocation(DataFlow::Node source) { none() } + + Location getASelectedSinkLocation(DataFlow::Node sink) { + result = sink.getLocation() + or + // where clause from EnvVarInjectionCritical.ql + exists(Event event, RemoteFlowSource source | result = event.getLocation() | + inPrivilegedContext(sink.asExpr(), event) and + isSource(source) and + // exclude paths to file read sinks from non-artifact sources + ( + // source is text + not source.getSourceType() = "artifact" and + not exists(ControlCheck check | + check.protects(sink.asExpr(), event, ["envvar-injection", "code-injection"]) + ) + or + // source is an artifact or a file from an untrusted checkout + source.getSourceType() = "artifact" and + not exists(ControlCheck check | + check + .protects(sink.asExpr(), event, + ["envvar-injection", "untrusted-checkout", "artifact-poisoning"]) + ) and + ( + sink instanceof EnvVarInjectionFromFileReadSink or + madSink(sink, "envvar-injection") + ) + ) + ) + } } /** Tracks flow of unsafe user input that is used to construct and evaluate an environment variable. */