Skip to content

Commit 4edf3dd

Browse files
owen-mcCopilot
andcommitted
Detect commands passed through simple launchers
Recognize fixed command positions without parsing launcher options. Normalize supported argument representations behind a shared abstraction and preserve safe arguments after a known non-shell command. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 79ca7b7 commit 4edf3dd

3 files changed

Lines changed: 239 additions & 57 deletions

File tree

‎java/ql/lib/semmle/code/java/security/CommandArguments.qll‎

Lines changed: 131 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -10,46 +10,114 @@ import semmle.code.java.Collections
1010

1111
/**
1212
* Holds if `ex` is used safely as an argument to a command;
13-
* i.e. it's not in the first position and it's not a shell command.
13+
* i.e. it's not in a position that selects a command to execute.
1414
*/
1515
predicate isSafeCommandArgument(Expr ex) {
16-
exists(ArrayInit ai, int i |
17-
ex = ai.getInit(i) and
16+
exists(CommandArguments arguments, int i |
1817
i > 0 and
19-
not isShell(ai.getInit(0))
20-
)
21-
or
22-
exists(CommandArgumentList cal |
23-
not cal.isShell() and
24-
ex = cal.getASubsequentAdd().getArgument(0)
25-
)
26-
or
27-
exists(CommandArgArrayImmutableFirst caa |
28-
not caa.isShell() and
29-
ex = caa.getAWrite(any(int i | i > 0))
18+
ex = arguments.getArgumentUse(i) and
19+
arguments.hasSafeArgument(i)
3020
)
3121
}
3222

33-
/**
34-
* Holds if the given expression is the name of a shell command such as bash or python
35-
*/
36-
private predicate isShell(Expr ex) {
37-
exists(string cmd | cmd = ex.(StringLiteral).getValue() |
38-
cmd.regexpMatch(".*(sh|javac?|python[23]?|osascript|cmd)(\\.exe)?$")
39-
)
23+
private string commandName(Expr ex) {
24+
result = ex.(StringLiteral).getValue()
4025
or
4126
exists(SsaDefinition ssa, SsaExplicitWrite def |
4227
ex = ssa.getARead() and
43-
isShell(def.getDefiningExpr())
28+
result = commandName(def.getDefiningExpr())
4429
|
4530
ssa.getAnUltimateDefinition() = def
4631
or
4732
ssa.(SsaCapturedDefinition).getAnUltimateCapturedDefinition() = def
4833
)
4934
or
50-
isShell(ex.(Assignment).getRhs())
35+
result = commandName(ex.(Assignment).getRhs())
5136
or
52-
isShell(ex.(LocalVariableDeclExpr).getInit())
37+
result = commandName(ex.(LocalVariableDeclExpr).getInit())
38+
}
39+
40+
/**
41+
* Holds if the given expression is the name of a shell command such as bash or python
42+
*/
43+
private predicate isShell(Expr ex) {
44+
commandName(ex).regexpMatch(".*(sh|javac?|python[23]?|osascript|cmd)(\\.exe)?$")
45+
}
46+
47+
/**
48+
* Gets the fixed position of the command executed by a supported delegating launcher.
49+
*
50+
* Only simple invocation forms are recognized. Launcher options are intentionally not parsed.
51+
*/
52+
private int delegatedCommandIndex(Expr ex) {
53+
exists(string launcher |
54+
launcher = commandName(ex).regexpCapture("(.*/)?([^/.]+)(\\.exe)?$", 2) and
55+
(
56+
launcher = ["env", "nice", "nohup", "setsid", "sudo", "xargs"] and result = 1
57+
or
58+
launcher = ["chroot", "stdbuf", "timeout"] and result = 2
59+
)
60+
)
61+
}
62+
63+
private newtype TCommandArguments =
64+
TLiteralArguments(ArrayInit init) or
65+
TListArguments(CommandArgumentList list) or
66+
TMutableArguments(CommandArgArrayImmutableFirst array)
67+
68+
/** An ordered sequence of command arguments. */
69+
private class CommandArguments extends TCommandArguments {
70+
string toString() { result = this.getArgument(0).toString() }
71+
72+
/** Gets a possible value at the given index. */
73+
private Expr getArgument(int index) {
74+
exists(ArrayInit init | this = TLiteralArguments(init) | result = init.getInit(index))
75+
or
76+
exists(CommandArgumentList list |
77+
this = TListArguments(list) and
78+
result = list.getArgument(index)
79+
)
80+
or
81+
exists(CommandArgArrayImmutableFirst array |
82+
this = TMutableArguments(array) and
83+
result = array.getAnElement(index)
84+
)
85+
}
86+
87+
/** Gets an expression whose value is stored at the given index. */
88+
Expr getArgumentUse(int index) {
89+
exists(ArrayInit init | this = TLiteralArguments(init) | result = init.getInit(index))
90+
or
91+
exists(CommandArgumentList list |
92+
this = TListArguments(list) and
93+
result = list.getArgument(index)
94+
)
95+
or
96+
exists(CommandArgArrayImmutableFirst array |
97+
this = TMutableArguments(array) and
98+
result = array.getAWrite(index)
99+
)
100+
}
101+
102+
predicate hasSafeArgument(int index) {
103+
exists(this.getArgumentUse(index)) and
104+
not isShell(this.getArgument(0)) and
105+
(
106+
not exists(delegatedCommandIndex(this.getArgument(0)))
107+
or
108+
exists(int commandIndex |
109+
commandIndex = delegatedCommandIndex(this.getArgument(0)) and
110+
(
111+
index < commandIndex
112+
or
113+
index > commandIndex and
114+
exists(this.getArgument(commandIndex)) and
115+
not isShell(this.getArgument(commandIndex)) and
116+
not exists(delegatedCommandIndex(this.getArgument(commandIndex)))
117+
)
118+
)
119+
)
120+
}
53121
}
54122

55123
/**
@@ -101,16 +169,25 @@ private class CommandArgumentList extends SsaExplicitWrite {
101169
result.getQualifier() = this.getAUseBeforeFirstAdd()
102170
}
103171

104-
/** Gets an addition to this list which is not the first element. */
105-
MethodCall getASubsequentAdd() {
106-
result = this.getAnAdd() and
107-
not result = this.getAFirstAdd()
172+
/** Gets a single-element addition at the given index. */
173+
private MethodCall getAnAddAtIndex(int index) {
174+
index = 0 and
175+
result = this.getAFirstAdd() and
176+
result.getMethod().hasName("add") and
177+
result.getNumArgument() = 1
178+
or
179+
index > 0 and
180+
exists(MethodCall previous |
181+
previous = this.getAnAddAtIndex(index - 1) and
182+
result = this.getAnAdd() and
183+
result.getMethod().hasName("add") and
184+
result.getNumArgument() = 1 and
185+
adjacentUseUse(previous.getQualifier(), result.getQualifier())
186+
)
108187
}
109188

110-
/** Holds if the first element of this list is a shell command. */
111-
predicate isShell() {
112-
exists(MethodCall ma | ma = this.getAFirstAdd() and isShell(ma.getArgument(0)))
113-
}
189+
/** Gets the argument added at the given index. */
190+
Expr getArgument(int index) { result = this.getAnAddAtIndex(index).getArgument(0) }
114191
}
115192

116193
/**
@@ -153,7 +230,7 @@ private class CommandArgumentArray extends SsaExplicitWrite {
153230
*/
154231
private class CommandArgArrayImmutableFirst extends CommandArgumentArray {
155232
CommandArgArrayImmutableFirst() {
156-
(exists(this.getAWrite(0)) or exists(firstElementOf(this.getDefiningExpr()))) and
233+
(exists(this.getAWrite(0)) or exists(arrayElementOf(this.getDefiningExpr(), 0))) and
157234
forall(VarRead use | exists(this.getAWrite(0, use)) | use = ssaGetAFirstUse(this))
158235
}
159236

@@ -162,41 +239,50 @@ private class CommandArgArrayImmutableFirst extends CommandArgumentArray {
162239
result = this.getAWrite(0)
163240
or
164241
not exists(this.getAWrite(0)) and
165-
result = firstElementOf(this.getDefiningExpr())
242+
result = arrayElementOf(this.getDefiningExpr(), 0)
166243
}
167244

168-
/** Holds if the first element of this array is a shell command. */
169-
predicate isShell() { isShell(this.getFirstElement()) }
245+
/** Gets a possible value for the element at the given index. */
246+
Expr getAnElement(int index) {
247+
index = 0 and result = this.getFirstElement()
248+
or
249+
index > 0 and
250+
(
251+
result = this.getAWrite(index)
252+
or
253+
result = arrayElementOf(this.getDefiningExpr(), index)
254+
)
255+
}
170256
}
171257

172-
/** Gets the first element of an imutable array of strings */
173-
private Expr firstElementOf(Expr arr) {
258+
/** Gets an element of an immutable array of strings. */
259+
private Expr arrayElementOf(Expr arr, int index) {
174260
arr.getType() instanceof ArrayOfStringType and
175261
(
176-
result = firstElementOf(arr.(Assignment).getRhs())
262+
result = arrayElementOf(arr.(Assignment).getRhs(), index)
177263
or
178-
result = firstElementOf(arr.(LocalVariableDeclExpr).getInit())
264+
result = arrayElementOf(arr.(LocalVariableDeclExpr).getInit(), index)
179265
or
180266
exists(CommandArgArrayImmutableFirst caa | arr = caa.getARead() |
181-
result = caa.getFirstElement()
267+
result = caa.getAnElement(index)
182268
)
183269
or
184270
exists(MethodCall ma, Method m |
185271
arr = ma and
186272
ma.getMethod() = m and
187273
m.getDeclaringType().hasQualifiedName("java.util", "Arrays") and
188274
m.hasName("copyOf") and
189-
result = firstElementOf(ma.getArgument(0))
275+
result = arrayElementOf(ma.getArgument(0), index)
190276
)
191277
or
192278
exists(Field f |
193279
f.isStatic() and
194280
arr.(FieldRead).getField() = f and
195-
result = firstElementOf(f.getInitializer())
281+
result = arrayElementOf(f.getInitializer(), index)
196282
)
197283
or
198-
result = arr.(ArrayInit).getInit(0)
284+
result = arr.(ArrayInit).getInit(index)
199285
or
200-
result = arr.(ArrayCreationExpr).getInit().getInit(0)
286+
result = arr.(ArrayCreationExpr).getInit().getInit(index)
201287
)
202288
}

0 commit comments

Comments
 (0)