Skip to content

Commit 26087f6

Browse files
committed
Added java-kotlin Sensitive Logging barriers (substrings)
1 parent 7ff696b commit 26087f6

File tree

1 file changed

+107
-5
lines changed

1 file changed

+107
-5
lines changed

java/ql/lib/semmle/code/java/security/SensitiveLoggingQuery.qll

Lines changed: 107 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,14 @@ import semmle.code.java.dataflow.TaintTracking
66
import semmle.code.java.security.SensitiveActions
77
import semmle.code.java.frameworks.android.Compose
88
private import semmle.code.java.security.Sanitizers
9+
import semmle.code.java.Constants
910

1011
/** A data flow source node for sensitive logging sources. */
1112
abstract class SensitiveLoggerSource extends DataFlow::Node { }
1213

14+
/** A data flow barrier node for sensitive logging sanitizers. */
15+
abstract class SensitiveLoggerBarrier extends DataFlow::Node { }
16+
1317
/** A variable that may hold sensitive information, judging by its name. */
1418
class VariableWithSensitiveName extends Variable {
1519
VariableWithSensitiveName() {
@@ -40,21 +44,119 @@ private class TypeType extends RefType {
4044
}
4145
}
4246

47+
/** A sanitizer that may remove sensitive information from a string before logging.
48+
*
49+
* It allows for substring operations taking the first N (or last N, for Kotlin) characters, limited to 7 or fewer.
50+
*/
51+
private class SensitiveLoggerSanitizerCalled extends SensitiveLoggerBarrier {
52+
SensitiveLoggerSanitizerCalled() {
53+
exists(MethodCall mc, Method m, int limit |
54+
limit = 7 and
55+
mc.getMethod() = m and
56+
(
57+
// substring in Java
58+
(
59+
m.hasQualifiedName("java.lang", "String", "substring") or
60+
m.hasQualifiedName("java.lang", "StringBuffer", "substring") or
61+
m.hasQualifiedName("java.lang", "StringBuilder", "substring")
62+
) and
63+
twoArgLimit(mc, limit, false) and
64+
this.asExpr() = mc.getQualifier()
65+
or
66+
// Kotlin string operations, which use extension methods (so the string is the first argument)
67+
(
68+
m.hasQualifiedName("kotlin.text", "StringsKt", "substring") and twoArgLimit(mc, limit, true)
69+
or
70+
m.hasQualifiedName("kotlin.text", "StringsKt", ["take", "takeLast"]) and
71+
singleArgLimit(mc, limit, true)
72+
) and
73+
this.asExpr() = mc.getArgument(0)
74+
)
75+
)
76+
}
77+
}
78+
79+
bindingset[limit, isKotlin]
80+
predicate singleArgLimit(MethodCall mc, int limit, boolean isKotlin) {
81+
exists(int argIndex, int staticInt |
82+
(if isKotlin = true then argIndex = 1 else argIndex = 0) and
83+
(
84+
staticInt <= limit and
85+
staticInt > 0 and
86+
mc.getArgument(argIndex).getUnderlyingExpr().(CompileTimeConstantExpr).getIntValue() = staticInt
87+
or exists(CompileTimeConstantExpr cte, DataFlow::Node source, DataFlow::Node sink |
88+
source.asExpr() = cte and
89+
cte.getIntValue() = staticInt and
90+
sink.asExpr() = mc.getArgument(argIndex) and
91+
IntegerToArgFlow::flow(source, sink)
92+
)
93+
)
94+
)
95+
}
96+
97+
bindingset[limit, isKotlin]
98+
predicate twoArgLimit(MethodCall mc, int limit, boolean isKotlin) {
99+
exists(int firstArgIndex, int secondArgIndex, int staticInt |
100+
staticInt <= limit and
101+
staticInt > 0 and
102+
(
103+
(isKotlin = true and firstArgIndex = 1 and secondArgIndex = 2)
104+
or
105+
(isKotlin = false and firstArgIndex = 0 and secondArgIndex = 1)
106+
)
107+
and
108+
mc.getArgument(firstArgIndex).getUnderlyingExpr().(CompileTimeConstantExpr).getIntValue() = 0 and
109+
(
110+
mc.getArgument(secondArgIndex).getUnderlyingExpr().(CompileTimeConstantExpr).getIntValue() = staticInt
111+
or exists(CompileTimeConstantExpr cte, DataFlow::Node source, DataFlow::Node sink |
112+
source.asExpr() = cte and
113+
cte.getIntValue() = staticInt and
114+
sink.asExpr() = mc.getArgument(secondArgIndex) and
115+
IntegerToArgFlow::flow(source, sink)
116+
)
117+
)
118+
)
119+
}
120+
121+
module IntegerToArgConfig implements DataFlow::ConfigSig {
122+
predicate isSource(DataFlow::Node source) {
123+
source.asExpr().getUnderlyingExpr() instanceof CompileTimeConstantExpr and
124+
source.asExpr().getType() instanceof IntegralType and
125+
source.asExpr().(CompileTimeConstantExpr).getIntValue() > 0
126+
}
127+
128+
predicate isSink(DataFlow::Node sink) {
129+
exists(MethodCall mc |
130+
sink.asExpr() = mc.getAnArgument()
131+
and sink.asExpr().getType() instanceof IntegralType
132+
)
133+
}
134+
135+
predicate isBarrier(DataFlow::Node sanitizer) { none() }
136+
137+
predicate isBarrierIn(DataFlow::Node node) { none() }
138+
}
139+
140+
private class GenericSanitizer extends SensitiveLoggerBarrier {
141+
GenericSanitizer() {
142+
this.asExpr() instanceof LiveLiteral or
143+
this instanceof SimpleTypeSanitizer or
144+
this.getType() instanceof TypeType
145+
}
146+
}
147+
43148
/** A data-flow configuration for identifying potentially-sensitive data flowing to a log output. */
44149
module SensitiveLoggerConfig implements DataFlow::ConfigSig {
45150
predicate isSource(DataFlow::Node source) { source instanceof SensitiveLoggerSource }
46151

47152
predicate isSink(DataFlow::Node sink) { sinkNode(sink, "log-injection") }
48153

49-
predicate isBarrier(DataFlow::Node sanitizer) {
50-
sanitizer.asExpr() instanceof LiveLiteral or
51-
sanitizer instanceof SimpleTypeSanitizer or
52-
sanitizer.getType() instanceof TypeType
53-
}
154+
predicate isBarrier(DataFlow::Node sanitizer) { sanitizer instanceof SensitiveLoggerBarrier }
54155

55156
predicate isBarrierIn(DataFlow::Node node) { isSource(node) }
56157

57158
predicate observeDiffInformedIncrementalMode() { any() }
58159
}
59160

60161
module SensitiveLoggerFlow = TaintTracking::Global<SensitiveLoggerConfig>;
162+
module IntegerToArgFlow = TaintTracking::Global<IntegerToArgConfig>;

0 commit comments

Comments
 (0)