Skip to content

Commit d2b7b55

Browse files
committed
Add command-parsing-leaking-file-content.ql
1 parent 26b7738 commit d2b7b55

File tree

1 file changed

+187
-0
lines changed

1 file changed

+187
-0
lines changed
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
/**
2+
* Detects flow from remote user input to a command parsing library which supports
3+
* syntax for reading arguments from a file (usually `@filename`). This can allow
4+
* an attacker to obtain the content of arbitrary files.
5+
*
6+
* To prevent this, use the corresponding library settings to disable this behavior:
7+
* - Args4J: `ParserProperties.withAtSyntax(false)`
8+
* - JCommander: `JCommander$Builder.expandAtSign(false)`
9+
*
10+
* This issue lead to [CVE-2024-23897 in Jenkins](https://www.jenkins.io/security/advisory/2024-01-24/).
11+
*
12+
* @id todo
13+
* @kind path-problem
14+
*/
15+
16+
// TODO: This query has not found any vulnerable project yet, and also for Jenkins it did
17+
// not detect this, probably because it did not consider the input to be remote user input
18+
19+
import java
20+
import semmle.code.java.dataflow.DataFlow
21+
import semmle.code.java.dataflow.FlowSources
22+
23+
/* -- Args4J -- */
24+
class ClassParserProperties extends Class {
25+
ClassParserProperties() { hasQualifiedName("org.kohsuke.args4j", "ParserProperties") }
26+
}
27+
28+
class ClassCmdLineParser extends Class {
29+
ClassCmdLineParser() { hasQualifiedName("org.kohsuke.args4j", "CmdLineParser") }
30+
}
31+
32+
/** Value flow for `ParserProperties.withX` methods */
33+
class ParserPropertiesFlowStep extends AdditionalValueStep {
34+
override predicate step(DataFlow::Node node1, DataFlow::Node node2) {
35+
node1.getType() instanceof ClassParserProperties and
36+
exists(MethodAccess builderCall |
37+
builderCall = node2.asExpr() and
38+
builderCall.getQualifier() = node1.asExpr() and
39+
builderCall.getMethod().getName().matches("with%")
40+
)
41+
}
42+
}
43+
44+
// TODO: Maybe use global dataflow instead? Local dataflow would not detect Jenkins
45+
// fixes in https://github.com/jenkinsci/jenkins/commit/554f03782057c499c49bbb06575f0d28b5200edb
46+
predicate isSafeArgs4jParser(Expr e) {
47+
// Call to roughly `new CmdLineParser(..., properties.withAtSyntax(false))`
48+
exists(ClassInstanceExpr newParserExpr, MethodAccess withAtSyntaxCall, Method withAtSyntaxMethod |
49+
newParserExpr.getConstructedType() instanceof ClassCmdLineParser and
50+
withAtSyntaxCall.getMethod() = withAtSyntaxMethod and
51+
withAtSyntaxMethod.getDeclaringType() instanceof ClassParserProperties and
52+
withAtSyntaxMethod.hasName("withAtSyntax") and
53+
withAtSyntaxCall.getArgument(0).(CompileTimeConstantExpr).getBooleanValue() = false and
54+
DataFlow::localExprFlow(withAtSyntaxCall, newParserExpr.getArgument(1))
55+
|
56+
// Direct local flow
57+
DataFlow::localExprFlow(newParserExpr, e)
58+
or
59+
// Or flow through field
60+
exists(Field f |
61+
DataFlow::localExprFlow(newParserExpr, f.getAnAssignedValue()) and
62+
DataFlow::localExprFlow(f.getAnAccess(), e)
63+
)
64+
)
65+
}
66+
67+
class Args4jSink extends DataFlow::Node {
68+
Args4jSink() {
69+
exists(MethodAccess parseCall, Method parseMethod |
70+
parseCall.getMethod() = parseMethod and
71+
parseMethod.getDeclaringType() instanceof ClassCmdLineParser and
72+
parseMethod.hasName("parseArgument") and
73+
this.asExpr() = parseCall.getAnArgument() and
74+
not isSafeArgs4jParser(parseCall.getQualifier())
75+
)
76+
}
77+
}
78+
79+
/* -- JCommander -- */
80+
class ClassJCommander extends Class {
81+
ClassJCommander() { hasQualifiedName("com.beust.jcommander", "JCommander") }
82+
}
83+
84+
class ClassJCommanderBuilder extends Class {
85+
ClassJCommanderBuilder() { hasQualifiedName("com.beust.jcommander", "JCommander$Builder") }
86+
}
87+
88+
/** Value flow for `JCommander$Builder` methods */
89+
class JCommanderBuilderFlowStep extends AdditionalValueStep {
90+
override predicate step(DataFlow::Node node1, DataFlow::Node node2) {
91+
node1.getType() instanceof ClassJCommanderBuilder and
92+
node2.getType() instanceof ClassJCommanderBuilder and
93+
node2.asExpr().(MethodAccess).getQualifier() = node1.asExpr()
94+
}
95+
}
96+
97+
/** Flow for `JCommander$Builder.build()`; for simplicity treat this as value flow (instead of taint) */
98+
class JCommanderBuildFlowStep extends AdditionalValueStep {
99+
override predicate step(DataFlow::Node node1, DataFlow::Node node2) {
100+
node1.getType() instanceof ClassJCommanderBuilder and
101+
exists(MethodAccess buildCall |
102+
buildCall = node2.asExpr() and
103+
buildCall.getQualifier() = node1.asExpr() and
104+
buildCall.getMethod().hasName("build")
105+
)
106+
}
107+
}
108+
109+
predicate isSafeJCommanderParser(Expr e) {
110+
exists(MethodAccess expandAtSignCall, Method expandAtSignMethod, Expr affectedJCommander |
111+
// Call to `builder.expandAtSign(false)`
112+
expandAtSignCall.getMethod() = expandAtSignMethod and
113+
expandAtSignMethod.getDeclaringType() instanceof ClassJCommanderBuilder and
114+
expandAtSignMethod.hasName("expandAtSign") and
115+
expandAtSignCall.getArgument(0).(CompileTimeConstantExpr).getBooleanValue() = false and
116+
// Flow from result of the builder call
117+
affectedJCommander = expandAtSignCall
118+
or
119+
// Or call to `jCommander.setExpandAtSign(false)`
120+
expandAtSignCall.getMethod() = expandAtSignMethod and
121+
expandAtSignMethod.getDeclaringType() instanceof ClassJCommander and
122+
expandAtSignMethod.hasName("setExpandAtSign") and
123+
expandAtSignCall.getArgument(0).(CompileTimeConstantExpr).getBooleanValue() = false and
124+
// Flow from qualifier (= JCommander instance)
125+
affectedJCommander = expandAtSignCall.getQualifier()
126+
|
127+
// Direct local flow
128+
DataFlow::localExprFlow(affectedJCommander, e)
129+
or
130+
// Or flow through field
131+
exists(Field f |
132+
DataFlow::localExprFlow(affectedJCommander, f.getAnAssignedValue()) and
133+
DataFlow::localExprFlow(f.getAnAccess(), e)
134+
)
135+
)
136+
}
137+
138+
class JCommanderSink extends DataFlow::Node {
139+
JCommanderSink() {
140+
// Calling `JCommander` constructor which parses arguments
141+
exists(ClassInstanceExpr parseConstructorCall |
142+
parseConstructorCall.getConstructedType() instanceof ClassJCommander and
143+
this.asExpr() = parseConstructorCall.getAnArgument() and
144+
(
145+
this.getType() instanceof TypeString or
146+
this.getType().(Array).getElementType() instanceof TypeString
147+
)
148+
)
149+
or
150+
// Or calling `JCommander` parse method
151+
exists(MethodAccess parseCall, Method parseMethod |
152+
parseCall.getMethod() = parseMethod and
153+
parseMethod.getDeclaringType() instanceof ClassJCommander and
154+
parseMethod.hasName(["parse", "parseWithoutValidation"]) and
155+
this.asExpr() = parseCall.getAnArgument() and
156+
not isSafeJCommanderParser(parseCall.getQualifier())
157+
)
158+
or
159+
// Or calling `JCommander$Builder.args(...)`
160+
exists(MethodAccess builderArgsCall, Method argsMethod |
161+
builderArgsCall.getMethod() = argsMethod and
162+
argsMethod.getDeclaringType() instanceof ClassJCommanderBuilder and
163+
argsMethod.hasName("args") and
164+
this.asExpr() = builderArgsCall.getAnArgument() and
165+
not isSafeJCommanderParser(builderArgsCall.getQualifier())
166+
)
167+
}
168+
}
169+
170+
/* ----- */
171+
172+
module CommandParserConfig implements DataFlow::ConfigSig {
173+
predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource }
174+
175+
predicate isSink(DataFlow::Node sink) {
176+
sink instanceof Args4jSink or sink instanceof JCommanderSink
177+
}
178+
}
179+
180+
module CommandParserFlow = TaintTracking::Global<CommandParserConfig>;
181+
182+
import CommandParserFlow::PathGraph
183+
184+
from CommandParserFlow::PathNode source, CommandParserFlow::PathNode sink
185+
where CommandParserFlow::flowPath(source, sink)
186+
select sink.getNode(), source, sink,
187+
"Remote user input flows into command parser which expands file contents"

0 commit comments

Comments
 (0)