Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
353 changes: 341 additions & 12 deletions java/ql/lib/semmle/code/java/security/CommandArguments.qll
Original file line number Diff line number Diff line change
Expand Up @@ -10,46 +10,354 @@

/**
* Holds if `ex` is used safely as an argument to a command;
* i.e. it's not in the first position and it's not a shell command.
* i.e. it's not in the first position and the command does not interpret arguments as commands.
*/
predicate isSafeCommandArgument(Expr ex) {
exists(ArrayInit ai, int i |
ex = ai.getInit(i) and
i > 0 and
not isShell(ai.getInit(0))
not isShell(ai.getInit(0)) and
(
not isDelegatingLauncher(ai.getInit(0))
or
TLiteralArguments(ai).(CommandArguments).hasSafeArgument(i) and
not writesCommandPrefix(_, ai, i) and
not arrayEscapes(ai)
)
)
or
exists(CommandArgumentList cal |
not cal.isShell() and
ex = cal.getASubsequentAdd().getArgument(0)
ex = cal.getASubsequentAdd().getArgument(0) and
(
not isDelegatingLauncher(cal.getAFirstAdd().getArgument(0))
or
exists(int i |
ex = TListArguments(cal).(CommandArguments).getArgument(i) and
TListArguments(cal).(CommandArguments).hasSafeArgument(i)
)
)
)
or
exists(CommandArgArrayImmutableFirst caa |
not caa.isShell() and
ex = caa.getAWrite(any(int i | i > 0))
exists(int i |
i > 0 and
ex = caa.getAWrite(i) and
(
not isDelegatingLauncher(caa.getFirstElement())
or
TMutableArguments(caa).(CommandArguments).hasSafeArgument(i) and
not arrayEscapes(containerOrigin(caa.getDefiningExpr())) and
not exists(ArrayAccess access |
writesCommandPrefix(access, containerOrigin(caa.getDefiningExpr()), i) and
not access.getArray() = caa.getARead()
)
)
)
)
}

/**
* Holds if the given expression is the name of a shell command such as bash or python
*/
private predicate isShell(Expr ex) {
exists(string cmd | cmd = ex.(StringLiteral).getValue() |
cmd.regexpMatch(".*(sh|javac?|python[23]?|osascript|cmd)(\\.exe)?$")
commandName(ex).regexpMatch(".*(sh|javac?|python[23]?|osascript|cmd)(\\.exe)?$")
}

/**
* Holds if the given expression is the name of a launcher that delegates to a command supplied in
* one of its arguments, rather than interpreting that command itself like a shell.
*/
private predicate isDelegatingLauncher(Expr ex) {
commandName(ex)
.regexpMatch("(.*/)?(busybox|chroot|doas|env|flock|ionice|nice|nohup|runuser|script|setsid|stdbuf|su|sudo|taskset|time|timeout|unbuffer|watch|xargs)$")
}

private string commandName(Expr ex) {
result = ex.(StringLiteral).getValue()
or
exists(SsaDefinition ssa |
ex = ssa.getARead() and
result = commandName(ultimateDefinition(ssa).(SsaExplicitWrite).getDefiningExpr())
)
or
exists(SsaDefinition ssa, SsaExplicitWrite def |
result = commandName(ex.(Assignment).getRhs())
or
result = commandName(ex.(LocalVariableDeclExpr).getInit())
}

private SsaDefinition ultimateDefinition(SsaDefinition ssa) {
result = ssa.getAnUltimateDefinition() and
not result instanceof SsaCapturedDefinition
or
result =
ultimateDefinition(ssa.getAnUltimateDefinition()
.(SsaCapturedDefinition)
.getAnUltimateCapturedDefinition())
}

/** Gets a command name only when every reaching definition resolves to the same constant. */
private string fixedCommandName(Expr ex) {
result = ex.(StringLiteral).getValue()
or
exists(SsaDefinition ssa |
ex = ssa.getARead() and
isShell(def.getDefiningExpr())
result =
fixedCommandName(any(SsaExplicitWrite def | def = ultimateDefinition(ssa)).getDefiningExpr())
|
ssa.getAnUltimateDefinition() = def
forall(SsaDefinition def | def = ultimateDefinition(ssa) |
fixedCommandName(def.(SsaExplicitWrite).getDefiningExpr()) = result
)
)
or
result = fixedCommandName(ex.(Assignment).getRhs())
or
result = fixedCommandName(ex.(LocalVariableDeclExpr).getInit())
}

private newtype TCommandArguments =
TLiteralArguments(ArrayInit init) or
TListArguments(CommandArgumentList list) or
TMutableArguments(CommandArgArrayImmutableFirst array)

/** An ordered argument sequence whose fixed command prefixes can be inspected. */
private class CommandArguments extends TCommandArguments {
string toString() { result = this.getArgument(0).toString() }

Expr getArgument(int index) {
exists(ArrayInit init | this = TLiteralArguments(init) | result = init.getInit(index))
or
ssa.(SsaCapturedDefinition).getAnUltimateCapturedDefinition() = def
exists(CommandArgumentList list |
this = TListArguments(list) and
not listEscapes(list) and
forall(MethodCall add | add = list.getAnAdd() |
add.getMethod().hasName("add") and add.getNumArgument() = 1
) and
result = list.getAnAddAtIndex(index).getArgument(0)
)
or
exists(CommandArgArrayImmutableFirst array |
this = TMutableArguments(array) and
(
index = 0 and result = array.getFirstElement()
or
index > 0 and
(
result = array.getAWrite(index)
or
result = arrayInitializerOf(array.getDefiningExpr()).getInit(index)
)
)
)
}

/** Gets a constant shared by every possible value at this position. */
private string getFixedArgument(int index) {
result = fixedCommandName(this.getArgument(index)) and
forall(Expr value | value = this.getArgument(index) | fixedCommandName(value) = result)
}

/**
* Gets a command operand after skipping only understood options and required operands.
* Shell-interpreting forms (such as `su -c`, `watch`, and `script -c`) are not skipped.
*/
private int commandOperand(string launcher, int index, boolean options) {
launcher = this.getFixedArgument(_).regexpCapture("(.*/)?([^/]+)", 2) and
options = [true, false] and
exists(Expr arg | arg = this.getArgument(index) |
(options = false or not this.getFixedArgument(index).matches("-%")) and
not (launcher = "env" and this.getFixedArgument(index).matches("%=%")) and
result = index
or
options = true and
this.getFixedArgument(index) = "--" and
result = this.commandOperand(launcher, index + 1, false)
or
options = true and
exists(int width |
launcherOption(launcher, this.getFixedArgument(index), width) and
(width = 1 or exists(this.getArgument(index + 1))) and
result = this.commandOperand(launcher, index + width, true)
)
or
launcher = "env" and
this.getFixedArgument(index).matches("%=%") and
result = this.commandOperand(launcher, index + 1, options)
Comment on lines +166 to +186
)
}

private int delegatedCommand(int index) {
exists(string launcher |
launcher = this.getFixedArgument(index).regexpCapture("(.*/)?([^/]+)", 2) and
isDelegatingLauncher(this.getArgument(index)) and
(
exists(int start |
launcher =
[
"busybox", "doas", "env", "ionice", "nice", "nohup", "setsid", "stdbuf", "sudo",
"time", "unbuffer", "xargs"
] and
start = index + 1
or
launcher = "runuser" and
this.getFixedArgument(index + 1) = ["-u", "--user"] and
start = index + 3
|
result = this.commandOperand(launcher, start, true)
)
or
launcher = ["chroot", "flock", "taskset", "timeout"] and
// Locate the required root, lock file, CPU mask, or duration first.
result = this.commandOperand(launcher, index + 1, true) + 1 and
not this.getFixedArgument(result).matches("-%")
)
)
}

private predicate safeAfter(int command, int argument) {
command < argument and
exists(this.getArgument(argument)) and
(
exists(this.getFixedArgument(command)) and
not isShell(this.getArgument(command)) and
not isDelegatingLauncher(this.getArgument(command))
or
this.safeAfter(this.delegatedCommand(command), argument)
)
}

predicate hasSafeArgument(int index) { this.safeAfter(0, index) }
}

private ArrayInit arrayInitializerOf(Expr ex) { result = containerOrigin(ex).(ArrayInit) }

/** Gets a container's allocation or initializer, including through local aliases. */
private Expr containerOrigin(Expr ex) {
result = ex.(ArrayInit)
or
result = ex.(ClassInstanceExpr)
or
ex.getUnderlyingExpr() != ex and
result = containerOrigin(ex.getUnderlyingExpr())
or
result = containerOrigin(ex.(ConditionalExpr).getBranchExpr(_))
or
result = ex.(ArrayCreationExpr).getInit()
or
ex instanceof ArrayCreationExpr and
not exists(ex.(ArrayCreationExpr).getInit()) and
result = ex
or
result = containerOrigin(ex.(LocalVariableDeclExpr).getInit())
or
result = containerOrigin(ex.(Assignment).getRhs())
or
exists(SsaDefinition ssa |
ex = ssa.getARead() and
result = containerOrigin(ultimateDefinition(ssa).(SsaExplicitWrite).getDefiningExpr())
)
}

/** Holds if the array may be mutated through a use not covered by the local alias analysis. */
private predicate arrayEscapes(Expr origin) {
containerIsPassedOut(origin)
or
containerOrigin(any(MethodCall call).getQualifier()) = origin
}

private predicate listEscapes(CommandArgumentList list) {
containerIsPassedOut(containerOrigin(list.getDefiningExpr()))
or
isShell(ex.(Assignment).getRhs())
exists(MethodCall call |
containerOrigin(call.getQualifier()) = containerOrigin(list.getDefiningExpr()) and
not call = list.getAnAdd()
)
}

private predicate containerIsPassedOut(Expr origin) {
exists(Call call |
containerOrigin(call.getAnArgument()) = origin and
not (
call.getCallee().getDeclaringType().hasQualifiedName("java.lang", "ProcessBuilder") and
(call.getCallee() instanceof Constructor or call.getCallee().hasName("command"))
or
call.getCallee().getDeclaringType().hasQualifiedName("java.lang", "Runtime") and
call.getCallee().hasName("exec")
)
)
or
containerOrigin(any(ReturnStmt ret).getResult()) = origin
or
containerOrigin(any(ArrayInit init).getAnInit()) = origin
or
exists(Assignment assignment |
containerOrigin(assignment.getRhs()) = origin and
(assignment.getDest() instanceof FieldAccess or assignment.getDest() instanceof ArrayAccess)
)
}

bindingset[origin, index]
private predicate writesCommandPrefix(ArrayAccess access, Expr origin, int index) {
containerOrigin(access.getArray()) = origin and
arrayVarWrite(access) and
(
not access.getIndexExpr() instanceof CompileTimeConstantExpr
or
access.getIndexExpr().(CompileTimeConstantExpr).getIntValue() < index
)
}

/** The width of an understood launcher option, including a separate option operand. */
bindingset[launcher, option]
private predicate launcherOption(string launcher, string option, int width) {
width = 1 and
(
launcher = "env" and option = ["-i", "--ignore-environment", "-0", "--null"]
or
launcher = "nice" and option.regexpMatch("-[0-9]+")
or
launcher = "setsid" and option = ["-c", "-f", "-w", "--ctty", "--fork", "--wait"]
or
launcher = "stdbuf" and option.regexpMatch("-[ioe].+")
or
launcher = "sudo" and option = ["-n", "-E", "-H", "--non-interactive"]
or
launcher = "doas" and option = ["-n", "-L"]
or
launcher = "time" and option = ["-p", "-v", "--portability", "--verbose"]
or
launcher = "taskset" and option = ["-a", "-c", "--all-tasks", "--cpu-list"]
or
launcher = "flock" and
option = ["-s", "-x", "-n", "-o", "--shared", "--exclusive", "--nonblock", "--close"]
or
launcher = "timeout" and option = ["--foreground", "--preserve-status", "-v", "--verbose"]
or
launcher = "xargs" and option = ["-0", "-r", "-t", "--null", "--no-run-if-empty", "--verbose"]
)
or
isShell(ex.(LocalVariableDeclExpr).getInit())
width = 2 and
(
launcher = "env" and option = ["-u", "--unset", "-C", "--chdir"]
or
launcher = "nice" and option = ["-n", "--adjustment"]
or
launcher = "ionice" and option = ["-c", "-n", "--class", "--classdata"]
or
launcher = "sudo" and option = ["-u", "-g", "--user", "--group"]
or
launcher = "doas" and option = ["-u"]
or
launcher = "time" and option = ["-o", "-f", "--output", "--format"]
or
launcher = "flock" and option = ["-w", "-E", "--timeout", "--conflict-exit-code"]
or
launcher = "timeout" and option = ["-s", "-k", "--signal", "--kill-after"]
or
launcher = "xargs" and
option = ["-n", "-P", "-I", "-d", "--max-args", "--max-procs", "--replace", "--delimiter"]
)
}

/**
Expand Down Expand Up @@ -107,6 +415,27 @@
not result = this.getAFirstAdd()
}

/** Gets a single-element append in a straight-line prefix of additions. */
MethodCall getAnAddAtIndex(int index) {
result = this.getAnAdd() and
result.getMethod().hasName("add") and
result.getNumArgument() = 1 and
index in [0 .. count(this.getAnAdd())] and
(
index = 0 and
result = this.getAFirstAdd() and
forall(MethodCall first | first = this.getAFirstAdd() | first = result)
or
exists(MethodCall previous |
previous = this.getAnAddAtIndex(index - 1) and
adjacentUseUse(previous.getQualifier(), result.getQualifier()) and
forall(VarRead use | adjacentUseUse(use, result.getQualifier()) |
use = previous.getQualifier()
)
)
)
}

/** Holds if the first element of this list is a shell command. */
predicate isShell() {
exists(MethodCall ma | ma = this.getAFirstAdd() and isShell(ma.getArgument(0)))
Expand Down
Loading
Loading