initial commit of actions

This commit is contained in:
2026-01-31 18:56:04 +01:00
commit 949ece5785
44660 changed files with 12034344 additions and 0 deletions

View File

@@ -0,0 +1,45 @@
/**
* @name Exec call vulnerable to binary planting
* @description On Windows, executing a binary with an unqualified name will execute a binary in the working directory in preference to a binary on PATH.
* @kind path-problem
* @problem.severity error
* @id javascript/codeql-action/binary-planting
*/
import javascript
import DataFlow
import DataFlow::PathGraph
class WhichBarrierGuardNode extends DataFlow::BarrierGuardNode, DataFlow::InvokeNode {
WhichBarrierGuardNode() { getCalleeName() = "which" }
override predicate blocks(boolean outcome, Expr e) {
outcome = true and
e = getArgument(0).asExpr()
}
}
class BinaryPlantingConfiguration extends DataFlow::Configuration {
BinaryPlantingConfiguration() {
this = "BinaryPlantingConfiguration"
}
override predicate isSource(Node node) {
node.asExpr() instanceof StringLiteral and
not node.asExpr().(StringLiteral).getValue().matches("%/%") and
not node.getFile().getBaseName().matches("%.test.ts")
}
override predicate isSink(Node node) {
node instanceof SystemCommandExecution or
exists(InvokeExpr e | e.getCalleeName() = "ToolRunner" and e.getArgument(0) = node.asExpr())
}
override predicate isBarrierGuard(DataFlow::BarrierGuardNode guard) {
guard instanceof WhichBarrierGuardNode
}
}
from BinaryPlantingConfiguration cfg, PathNode source, PathNode sink
where cfg.hasFlowPath(source, sink)
select source.getNode(), source, sink, "This exec call might be vulnerable to Windows binary planting vulnerabilities."

View File

@@ -0,0 +1,14 @@
---
lockVersion: 1.0.0
dependencies:
codeql-javascript:
version: 0.6.1
codeql/regex:
version: 0.0.12
codeql/tutorial:
version: 0.0.9
codeql/util:
version: 0.0.9
codeql/yaml:
version: 0.0.1
compiled: false

View File

@@ -0,0 +1,4 @@
name: codeql-action-custom-queries-javascript
version: 0.0.0
dependencies:
codeql/javascript-all: 0.6.1

View File

@@ -0,0 +1,53 @@
/**
* @name Some environment variables may not exist in default setup workflows
* @id javascript/codeql-action/default-setup-env-vars
* @kind problem
* @severity warning
*/
import javascript
bindingset[envVar]
predicate isSafeForDefaultSetup(string envVar) {
// Ignore internal Code Scanning environment variables
envVar.matches("CODE_SCANNING_%") or
envVar.matches("CODEQL_%") or
envVar.matches("CODESCANNING_%") or
envVar.matches("LGTM_%") or
// We flag up usage of potentially unsafe parts of the GitHub event in `default-setup-event-context.ql`.
envVar = "GITHUB_EVENT_PATH" or
// The following environment variables are known to be safe for use with default setup
envVar =
[
"GITHUB_ACTION_REF", "GITHUB_ACTION_REPOSITORY", "GITHUB_ACTOR", "GITHUB_API_URL",
"GITHUB_BASE_REF", "GITHUB_EVENT_NAME", "GITHUB_JOB", "GITHUB_RUN_ATTEMPT", "GITHUB_RUN_ID",
"GITHUB_SHA", "GITHUB_REPOSITORY", "GITHUB_SERVER_URL", "GITHUB_TOKEN", "GITHUB_WORKFLOW",
"GITHUB_WORKSPACE", "GOFLAGS", "ImageVersion", "JAVA_TOOL_OPTIONS", "RUNNER_ARCH",
"RUNNER_ENVIRONMENT", "RUNNER_NAME", "RUNNER_OS", "RUNNER_TEMP", "RUNNER_TOOL_CACHE"
]
}
predicate envVarRead(DataFlow::Node node, string envVar) {
node =
any(DataFlow::PropRead read |
read = NodeJSLib::process().getAPropertyRead("env").getAPropertyRead() and
envVar = read.getPropertyName()
) or
node =
any(DataFlow::CallNode call |
call.getCalleeName().matches("get%EnvParam") and
envVar = call.getArgument(0).getStringValue()
)
}
from DataFlow::Node read, string envVar
where
envVarRead(read, envVar) and
not read.getFile().getBaseName().matches("%.test.ts") and
not isSafeForDefaultSetup(envVar)
select read,
"The environment variable " + envVar +
" may not exist in default setup workflows. If all uses are safe, add it to the list of " +
"environment variables that are known to be safe in " +
"'queries/default-setup-environment-variables.ql'. If this use is safe but others are not, " +
"dismiss this alert as a false positive."

View File

@@ -0,0 +1,58 @@
/**
* @name Some context properties may not exist in default setup workflows
* @id javascript/codeql-action/default-setup-context-properties
* @kind path-problem
* @severity warning
*/
import javascript
import DataFlow::PathGraph
class NotParsedLabel extends DataFlow::FlowLabel {
NotParsedLabel() { this = "not-parsed" }
}
class ParsedLabel extends DataFlow::FlowLabel {
ParsedLabel() { this = "parsed" }
}
class EventContextAccessConfiguration extends DataFlow::Configuration {
EventContextAccessConfiguration() { this = "EventContextAccessConfiguration" }
override predicate isSource(DataFlow::Node source, DataFlow::FlowLabel lbl) {
source = NodeJSLib::process().getAPropertyRead("env").getAPropertyRead("GITHUB_EVENT_PATH") and
lbl instanceof NotParsedLabel
}
override predicate isSink(DataFlow::Node sink, DataFlow::FlowLabel lbl) {
sink instanceof DataFlow::PropRead and
lbl instanceof ParsedLabel and
not exists(DataFlow::PropRead n | sink = n.getBase()) and
not sink.asExpr().getFile().getBaseName().matches("%.test.ts")
}
override predicate isAdditionalFlowStep(
DataFlow::Node src, DataFlow::Node trg, DataFlow::FlowLabel inlbl, DataFlow::FlowLabel outlbl
) {
src = trg.(FileSystemReadAccess).getAPathArgument() and inlbl = outlbl
or
exists(JsonParserCall c |
src = c.getInput() and
trg = c.getOutput() and
inlbl instanceof NotParsedLabel and
outlbl instanceof ParsedLabel
)
or
(
TaintTracking::sharedTaintStep(src, trg) or
DataFlow::SharedFlowStep::step(src, trg) or
DataFlow::SharedFlowStep::step(src, trg, _, _)
) and
inlbl = outlbl
}
}
from EventContextAccessConfiguration cfg, DataFlow::PathNode source, DataFlow::PathNode sink
where cfg.hasFlowPath(source, sink)
select sink.getNode(), source, sink,
"This event context property may not exist in default setup workflows."

View File

@@ -0,0 +1,24 @@
/**
* @name Import action entrypoint
* @description Importing the entrypoint file for an action is dangerous
* because the code from that action will be run when the file is imported.
* @kind problem
* @problem.severity error
* @id javascript/codeql-action/import-action-entrypoint
*/
import javascript
class ActionEntrypointFile extends File {
ActionEntrypointFile() {
exists(Module m | m.getPath() = this.getAbsolutePath() and
// This is quite a broad check and relies on the function name, but hopefully it'll be accurate enough
m.getAStmt().getAChildExpr+().(CallExpr).getCalleeName() = "run") and
// Requiring the relative path to exist limits us to files in the code repository and avoid libraries
exists(this.getRelativePath())
}
}
from ImportDeclaration i
where exists(ActionEntrypointFile f | i.getImportedModule().getPath() = f.getAbsolutePath())
select i, "This imports the entrypoint file for an action. This will execute the code from the action."

View File

@@ -0,0 +1,55 @@
/**
* @name Inconsistent action input
* @description If multiple actions define an input with the same name, then the input
* must be defined in an identical way to avoid confusion for the user.
* This also makes writing queries like required-action-input.ql easier.
* @kind problem
* @severity error
* @id javascript/codeql-action/inconsistent-action-input
*/
import javascript
/**
* A declaration of a github action.
*/
class ActionDeclaration extends File {
ActionDeclaration() {
getRelativePath().matches("%/action.yml") and
// Ignore internal Actions
not getRelativePath().matches(".github/actions/%")
}
/**
* The name of the action.
*/
string getName() {
result = getRelativePath().regexpCapture("(.*)/action.yml", 1)
}
YamlDocument getRootNode() {
result.getFile() = this
}
YamlValue getInput(string inputName) {
result = getRootNode().(YamlMapping).lookup("inputs").(YamlMapping).lookup(inputName)
}
}
predicate areNotEquivalent(YamlValue x, YamlValue y) {
x.getTag() != y.getTag()
or
x.(YamlScalar).getValue() != y.(YamlScalar).getValue()
or
x.getNumChild() != y.getNumChild()
or
exists(int i | areNotEquivalent(x.getChild(i), y.getChild(i)))
}
from ActionDeclaration actionA, ActionDeclaration actionB, string inputName
where actionA.getName() < actionB.getName() // prevent duplicates which are permutations of the names
and areNotEquivalent(actionA.getInput(inputName), actionB.getInput(inputName))
// ram and threads inputs in different actions are supposed to have different description
and inputName != "ram" and inputName != "threads"
select actionA, "Action $@ and action $@ both declare input $@, however their definitions are not identical. This may be confusing to users.",
actionA, actionA.getName(), actionB, actionB.getName(), inputName, inputName

View File

@@ -0,0 +1,86 @@
/**
* @name Required action input
* @description For action inputs the core.input represents input with no value as the emptystring.
* This doesn't promote good type checking. Instead, use either actions-util.getOptionalInput or
* actions-util.getRequiredInput depending on if the input always has a value or not. The input
* will always have a value if it is required or has a default value.
* @kind problem
* @problem.severity error
* @id javascript/codeql-action/required-action-input
*/
import javascript
/**
* A declaration of a github action.
*/
class ActionDeclaration extends File {
ActionDeclaration() {
getRelativePath().matches("%/action.yml")
}
YamlDocument getRootNode() {
result.getFile() = this
}
/**
* The name of any input to this action.
*/
string getAnInput() {
result = getRootNode().(YamlMapping).lookup("inputs").(YamlMapping).getKey(_).(YamlString).getValue()
}
/**
* The given input always has a value, either because it is required,
* or because it has a default value.
*/
predicate inputAlwaysHasValue(string input) {
exists(YamlMapping value |
value = getRootNode().(YamlMapping).lookup("inputs").(YamlMapping).lookup(input) and
(exists(value.lookup("default")) or
value.lookup("required").(YamlBool).getBoolValue() = true))
}
}
/**
* An import from "@actions/core"
*/
class ActionsLibImport extends ImportDeclaration {
ActionsLibImport() {
getImportedPath().getValue() = "@actions/core"
}
Variable getAProvidedVariable() {
result = getASpecifier().getLocal().getVariable()
}
}
/**
* A call to the core.getInput method.
*/
class CoreGetInputMethodCallExpr extends MethodCallExpr {
CoreGetInputMethodCallExpr() {
getMethodName() = "getInput" and
exists(ActionsLibImport libImport |
this.getReceiver() = libImport.getAProvidedVariable().getAnAccess() or
this.getReceiver().(PropAccess).getBase() = libImport.getAProvidedVariable().getAnAccess())
}
/**
* The name of the input being accessed.
*/
string getInputName() {
result = getArgument(0).(StringLiteral).getValue()
}
}
from ActionDeclaration action, CoreGetInputMethodCallExpr getInputCall, string inputName, string alternateFunction
where action.getAnInput() = inputName
// We don't want to create an alert for the users core.getInput in the getRequiredInput
// and getOptionalInput functions themselves, and this check here does that in a
// roundabout way by checking the parameter is a string literal. This should be enough
// and hopefully won't discount any real calls to core.getInput, but is worth noting here.
and getInputCall.getInputName() = inputName
and ((action.inputAlwaysHasValue(inputName) and alternateFunction = "getRequiredInput")
or (not action.inputAlwaysHasValue(inputName) and alternateFunction = "geOptionalInput"))
select getInputCall, "This input may be undefined. Please use actions-util.$@ instead.", alternateFunction, alternateFunction

View File

@@ -0,0 +1,103 @@
/**
* @name Undeclared action input
* @description Code tries to use an input parameter that is not defined for this action.
Perhaps this code is shared by multiple actions.
* @kind problem
* @problem.severity error
* @id javascript/codeql-action/undeclared-action-input
*/
import javascript
/**
* A declaration of a github action, including its inputs and entrypoint.
*/
class ActionDeclaration extends File {
ActionDeclaration() {
getRelativePath().matches("%/action.yml")
}
/**
* The name of the action.
*/
string getName() {
result = getRelativePath().regexpCapture("(.*)/action.yml", 1)
}
YamlDocument getRootNode() {
result.getFile() = this
}
/**
* The name of any input to this action.
*/
string getAnInput() {
result = getRootNode().(YamlMapping).lookup("inputs").(YamlMapping).getKey(_).(YamlString).getValue()
}
/**
* The function that is the entrypoint to this action.
*/
FunctionDeclStmt getEntrypoint() {
result.getFile().getRelativePath() = getRootNode().
(YamlMapping).lookup("runs").
(YamlMapping).lookup("main").
(YamlString).getValue().regexpReplaceAll("\\.\\./lib/(.*)\\.js", "src/$1.ts") and
result.getName() = "run"
}
}
/**
* A function declared on CodeQL interface from codeql.ts
*/
class CodeQLFunction extends Function {
CodeQLFunction() {
exists(Function getCodeQLForCmd, ObjectExpr obj |
getCodeQLForCmd.getName() = "getCodeQLForCmd" and
obj = getCodeQLForCmd.getAStmt().(ReturnStmt).getExpr() and
obj.getAProperty().getInit() = this)
}
}
/**
* Any expr that is a transitive child of the given function.
*/
Expr getAFunctionChildExpr(Function f) {
result.getContainer() = f
}
/*
* Result is a function that is called from the body of the given function `f`
*/
Function calledBy(Function f) {
result = getAFunctionChildExpr(f).(InvokeExpr).getResolvedCallee()
or
// Assume outer function causes inner function to be called,
// except for the special case of the CodeQL functions.
(result.getEnclosingContainer() = f and not result instanceof CodeQLFunction)
or
// Handle calls to CodeQL functions by name
getAFunctionChildExpr(f).(InvokeExpr).getCalleeName() = result.(CodeQLFunction).getName()
}
/**
* A call to the core.getInput method.
*/
class GetInputMethodCallExpr extends MethodCallExpr {
GetInputMethodCallExpr() {
getMethodName() = "getInput"
}
/**
* The name of the input being accessed.
*/
string getInputName() {
result = getArgument(0).(StringLiteral).getValue()
}
}
from ActionDeclaration action, GetInputMethodCallExpr getInputCall, string inputName
where getAFunctionChildExpr(calledBy*(action.getEntrypoint())) = getInputCall and
inputName = getInputCall.getInputName() and
not inputName = action.getAnInput()
select getInputCall, "The $@ input is not defined for the $@ action", inputName, inputName, action, action.getName()