Skip to content

Commit c2e8ec7

Browse files
owen-mcCopilot
andcommitted
Detect commands passed through simple launchers
Recognize fixed command positions without parsing launcher options. Preserve safe arguments after a known non-shell command. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 79ca7b7 commit c2e8ec7

3 files changed

Lines changed: 230 additions & 46 deletions

File tree

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

Lines changed: 122 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -10,46 +10,110 @@ 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) {
1616
exists(ArrayInit ai, int i |
1717
ex = ai.getInit(i) and
1818
i > 0 and
19-
not isShell(ai.getInit(0))
19+
not isShell(ai.getInit(0)) and
20+
(
21+
not exists(delegatedCommandIndex(ai.getInit(0)))
22+
or
23+
exists(int commandIndex |
24+
commandIndex = delegatedCommandIndex(ai.getInit(0)) and
25+
(
26+
i < commandIndex
27+
or
28+
i > commandIndex and
29+
not isShell(ai.getInit(commandIndex)) and
30+
not exists(delegatedCommandIndex(ai.getInit(commandIndex)))
31+
)
32+
)
33+
)
2034
)
2135
or
22-
exists(CommandArgumentList cal |
36+
exists(CommandArgumentList cal, int i |
2337
not cal.isShell() and
24-
ex = cal.getASubsequentAdd().getArgument(0)
38+
i > 0 and
39+
ex = cal.getArgument(i) and
40+
(
41+
not exists(delegatedCommandIndex(cal.getArgument(0)))
42+
or
43+
exists(int commandIndex |
44+
commandIndex = delegatedCommandIndex(cal.getArgument(0)) and
45+
(
46+
i < commandIndex
47+
or
48+
i > commandIndex and
49+
exists(cal.getArgument(commandIndex)) and
50+
not isShell(cal.getArgument(commandIndex)) and
51+
not exists(delegatedCommandIndex(cal.getArgument(commandIndex)))
52+
)
53+
)
54+
)
2555
)
2656
or
27-
exists(CommandArgArrayImmutableFirst caa |
57+
exists(CommandArgArrayImmutableFirst caa, int i |
2858
not caa.isShell() and
29-
ex = caa.getAWrite(any(int i | i > 0))
59+
i > 0 and
60+
ex = caa.getAWrite(i) and
61+
(
62+
not exists(delegatedCommandIndex(caa.getFirstElement()))
63+
or
64+
exists(int commandIndex |
65+
commandIndex = delegatedCommandIndex(caa.getFirstElement()) and
66+
(
67+
i < commandIndex
68+
or
69+
i > commandIndex and
70+
exists(caa.getAnElement(commandIndex)) and
71+
not isShell(caa.getAnElement(commandIndex)) and
72+
not exists(delegatedCommandIndex(caa.getAnElement(commandIndex)))
73+
)
74+
)
75+
)
3076
)
3177
}
3278

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-
)
79+
private string commandName(Expr ex) {
80+
result = ex.(StringLiteral).getValue()
4081
or
4182
exists(SsaDefinition ssa, SsaExplicitWrite def |
4283
ex = ssa.getARead() and
43-
isShell(def.getDefiningExpr())
84+
result = commandName(def.getDefiningExpr())
4485
|
4586
ssa.getAnUltimateDefinition() = def
4687
or
4788
ssa.(SsaCapturedDefinition).getAnUltimateCapturedDefinition() = def
4889
)
4990
or
50-
isShell(ex.(Assignment).getRhs())
91+
result = commandName(ex.(Assignment).getRhs())
5192
or
52-
isShell(ex.(LocalVariableDeclExpr).getInit())
93+
result = commandName(ex.(LocalVariableDeclExpr).getInit())
94+
}
95+
96+
/**
97+
* Holds if the given expression is the name of a shell command such as bash or python
98+
*/
99+
private predicate isShell(Expr ex) {
100+
commandName(ex).regexpMatch(".*(sh|javac?|python[23]?|osascript|cmd)(\\.exe)?$")
101+
}
102+
103+
/**
104+
* Gets the fixed position of the command executed by a supported delegating launcher.
105+
*
106+
* Only simple invocation forms are recognized. Launcher options are intentionally not parsed.
107+
*/
108+
private int delegatedCommandIndex(Expr ex) {
109+
exists(string launcher |
110+
launcher = commandName(ex).regexpCapture("(.*/)?([^/.]+)(\\.exe)?$", 2) and
111+
(
112+
launcher = ["env", "nice", "nohup", "setsid", "sudo", "xargs"] and result = 1
113+
or
114+
launcher = ["chroot", "stdbuf", "timeout"] and result = 2
115+
)
116+
)
53117
}
54118

55119
/**
@@ -101,16 +165,28 @@ private class CommandArgumentList extends SsaExplicitWrite {
101165
result.getQualifier() = this.getAUseBeforeFirstAdd()
102166
}
103167

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()
168+
/** Gets a single-element addition at the given index. */
169+
private MethodCall getAnAddAtIndex(int index) {
170+
index = 0 and
171+
result = this.getAFirstAdd() and
172+
result.getMethod().hasName("add") and
173+
result.getNumArgument() = 1
174+
or
175+
index > 0 and
176+
exists(MethodCall previous |
177+
previous = this.getAnAddAtIndex(index - 1) and
178+
result = this.getAnAdd() and
179+
result.getMethod().hasName("add") and
180+
result.getNumArgument() = 1 and
181+
adjacentUseUse(previous.getQualifier(), result.getQualifier())
182+
)
108183
}
109184

185+
/** Gets the argument added at the given index. */
186+
Expr getArgument(int index) { result = this.getAnAddAtIndex(index).getArgument(0) }
187+
110188
/** 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+
predicate isShell() { isShell(this.getArgument(0)) }
114190
}
115191

116192
/**
@@ -153,7 +229,7 @@ private class CommandArgumentArray extends SsaExplicitWrite {
153229
*/
154230
private class CommandArgArrayImmutableFirst extends CommandArgumentArray {
155231
CommandArgArrayImmutableFirst() {
156-
(exists(this.getAWrite(0)) or exists(firstElementOf(this.getDefiningExpr()))) and
232+
(exists(this.getAWrite(0)) or exists(arrayElementOf(this.getDefiningExpr(), 0))) and
157233
forall(VarRead use | exists(this.getAWrite(0, use)) | use = ssaGetAFirstUse(this))
158234
}
159235

@@ -162,41 +238,53 @@ private class CommandArgArrayImmutableFirst extends CommandArgumentArray {
162238
result = this.getAWrite(0)
163239
or
164240
not exists(this.getAWrite(0)) and
165-
result = firstElementOf(this.getDefiningExpr())
241+
result = arrayElementOf(this.getDefiningExpr(), 0)
242+
}
243+
244+
/** Gets a possible value for the element at the given index. */
245+
Expr getAnElement(int index) {
246+
index = 0 and result = this.getFirstElement()
247+
or
248+
index > 0 and
249+
(
250+
result = this.getAWrite(index)
251+
or
252+
result = arrayElementOf(this.getDefiningExpr(), index)
253+
)
166254
}
167255

168256
/** Holds if the first element of this array is a shell command. */
169257
predicate isShell() { isShell(this.getFirstElement()) }
170258
}
171259

172-
/** Gets the first element of an imutable array of strings */
173-
private Expr firstElementOf(Expr arr) {
260+
/** Gets an element of an immutable array of strings. */
261+
private Expr arrayElementOf(Expr arr, int index) {
174262
arr.getType() instanceof ArrayOfStringType and
175263
(
176-
result = firstElementOf(arr.(Assignment).getRhs())
264+
result = arrayElementOf(arr.(Assignment).getRhs(), index)
177265
or
178-
result = firstElementOf(arr.(LocalVariableDeclExpr).getInit())
266+
result = arrayElementOf(arr.(LocalVariableDeclExpr).getInit(), index)
179267
or
180268
exists(CommandArgArrayImmutableFirst caa | arr = caa.getARead() |
181-
result = caa.getFirstElement()
269+
result = caa.getAnElement(index)
182270
)
183271
or
184272
exists(MethodCall ma, Method m |
185273
arr = ma and
186274
ma.getMethod() = m and
187275
m.getDeclaringType().hasQualifiedName("java.util", "Arrays") and
188276
m.hasName("copyOf") and
189-
result = firstElementOf(ma.getArgument(0))
277+
result = arrayElementOf(ma.getArgument(0), index)
190278
)
191279
or
192280
exists(Field f |
193281
f.isStatic() and
194282
arr.(FieldRead).getField() = f and
195-
result = firstElementOf(f.getInitializer())
283+
result = arrayElementOf(f.getInitializer(), index)
196284
)
197285
or
198-
result = arr.(ArrayInit).getInit(0)
286+
result = arr.(ArrayInit).getInit(index)
199287
or
200-
result = arr.(ArrayCreationExpr).getInit().getInit(0)
288+
result = arr.(ArrayCreationExpr).getInit().getInit(index)
201289
)
202290
}

0 commit comments

Comments
 (0)