From 26087f6060e5ccd0f1cb7253c361de3dfee53314 Mon Sep 17 00:00:00 2001 From: aegilops <41705651+aegilops@users.noreply.github.com> Date: Fri, 31 Oct 2025 17:56:04 +0000 Subject: [PATCH 01/12] Added java-kotlin Sensitive Logging barriers (substrings) --- .../java/security/SensitiveLoggingQuery.qll | 112 +++++++++++++++++- 1 file changed, 107 insertions(+), 5 deletions(-) diff --git a/java/ql/lib/semmle/code/java/security/SensitiveLoggingQuery.qll b/java/ql/lib/semmle/code/java/security/SensitiveLoggingQuery.qll index 25454d80c717..855d05168cc6 100644 --- a/java/ql/lib/semmle/code/java/security/SensitiveLoggingQuery.qll +++ b/java/ql/lib/semmle/code/java/security/SensitiveLoggingQuery.qll @@ -6,10 +6,14 @@ import semmle.code.java.dataflow.TaintTracking import semmle.code.java.security.SensitiveActions import semmle.code.java.frameworks.android.Compose private import semmle.code.java.security.Sanitizers +import semmle.code.java.Constants /** A data flow source node for sensitive logging sources. */ abstract class SensitiveLoggerSource extends DataFlow::Node { } +/** A data flow barrier node for sensitive logging sanitizers. */ +abstract class SensitiveLoggerBarrier extends DataFlow::Node { } + /** A variable that may hold sensitive information, judging by its name. */ class VariableWithSensitiveName extends Variable { VariableWithSensitiveName() { @@ -40,17 +44,114 @@ private class TypeType extends RefType { } } +/** A sanitizer that may remove sensitive information from a string before logging. + * + * It allows for substring operations taking the first N (or last N, for Kotlin) characters, limited to 7 or fewer. + */ +private class SensitiveLoggerSanitizerCalled extends SensitiveLoggerBarrier { + SensitiveLoggerSanitizerCalled() { + exists(MethodCall mc, Method m, int limit | + limit = 7 and + mc.getMethod() = m and + ( + // substring in Java + ( + m.hasQualifiedName("java.lang", "String", "substring") or + m.hasQualifiedName("java.lang", "StringBuffer", "substring") or + m.hasQualifiedName("java.lang", "StringBuilder", "substring") + ) and + twoArgLimit(mc, limit, false) and + this.asExpr() = mc.getQualifier() + or + // Kotlin string operations, which use extension methods (so the string is the first argument) + ( + m.hasQualifiedName("kotlin.text", "StringsKt", "substring") and twoArgLimit(mc, limit, true) + or + m.hasQualifiedName("kotlin.text", "StringsKt", ["take", "takeLast"]) and + singleArgLimit(mc, limit, true) + ) and + this.asExpr() = mc.getArgument(0) + ) + ) + } +} + +bindingset[limit, isKotlin] +predicate singleArgLimit(MethodCall mc, int limit, boolean isKotlin) { + exists(int argIndex, int staticInt | + (if isKotlin = true then argIndex = 1 else argIndex = 0) and + ( + staticInt <= limit and + staticInt > 0 and + mc.getArgument(argIndex).getUnderlyingExpr().(CompileTimeConstantExpr).getIntValue() = staticInt + or exists(CompileTimeConstantExpr cte, DataFlow::Node source, DataFlow::Node sink | + source.asExpr() = cte and + cte.getIntValue() = staticInt and + sink.asExpr() = mc.getArgument(argIndex) and + IntegerToArgFlow::flow(source, sink) + ) + ) + ) +} + +bindingset[limit, isKotlin] +predicate twoArgLimit(MethodCall mc, int limit, boolean isKotlin) { + exists(int firstArgIndex, int secondArgIndex, int staticInt | + staticInt <= limit and + staticInt > 0 and + ( + (isKotlin = true and firstArgIndex = 1 and secondArgIndex = 2) + or + (isKotlin = false and firstArgIndex = 0 and secondArgIndex = 1) + ) + and + mc.getArgument(firstArgIndex).getUnderlyingExpr().(CompileTimeConstantExpr).getIntValue() = 0 and + ( + mc.getArgument(secondArgIndex).getUnderlyingExpr().(CompileTimeConstantExpr).getIntValue() = staticInt + or exists(CompileTimeConstantExpr cte, DataFlow::Node source, DataFlow::Node sink | + source.asExpr() = cte and + cte.getIntValue() = staticInt and + sink.asExpr() = mc.getArgument(secondArgIndex) and + IntegerToArgFlow::flow(source, sink) + ) + ) + ) +} + +module IntegerToArgConfig implements DataFlow::ConfigSig { + predicate isSource(DataFlow::Node source) { + source.asExpr().getUnderlyingExpr() instanceof CompileTimeConstantExpr and + source.asExpr().getType() instanceof IntegralType and + source.asExpr().(CompileTimeConstantExpr).getIntValue() > 0 + } + + predicate isSink(DataFlow::Node sink) { + exists(MethodCall mc | + sink.asExpr() = mc.getAnArgument() + and sink.asExpr().getType() instanceof IntegralType + ) + } + + predicate isBarrier(DataFlow::Node sanitizer) { none() } + + predicate isBarrierIn(DataFlow::Node node) { none() } +} + +private class GenericSanitizer extends SensitiveLoggerBarrier { + GenericSanitizer() { + this.asExpr() instanceof LiveLiteral or + this instanceof SimpleTypeSanitizer or + this.getType() instanceof TypeType + } +} + /** A data-flow configuration for identifying potentially-sensitive data flowing to a log output. */ module SensitiveLoggerConfig implements DataFlow::ConfigSig { predicate isSource(DataFlow::Node source) { source instanceof SensitiveLoggerSource } predicate isSink(DataFlow::Node sink) { sinkNode(sink, "log-injection") } - predicate isBarrier(DataFlow::Node sanitizer) { - sanitizer.asExpr() instanceof LiveLiteral or - sanitizer instanceof SimpleTypeSanitizer or - sanitizer.getType() instanceof TypeType - } + predicate isBarrier(DataFlow::Node sanitizer) { sanitizer instanceof SensitiveLoggerBarrier } predicate isBarrierIn(DataFlow::Node node) { isSource(node) } @@ -58,3 +159,4 @@ module SensitiveLoggerConfig implements DataFlow::ConfigSig { } module SensitiveLoggerFlow = TaintTracking::Global; +module IntegerToArgFlow = TaintTracking::Global; From d1eceee9d44463602e6d1167fa6ac3cf98b67572 Mon Sep 17 00:00:00 2001 From: aegilops <41705651+aegilops@users.noreply.github.com> Date: Fri, 31 Oct 2025 18:19:27 +0000 Subject: [PATCH 02/12] Fixed format/docs issues --- .../java/security/SensitiveLoggingQuery.qll | 41 +++++++++++-------- 1 file changed, 25 insertions(+), 16 deletions(-) diff --git a/java/ql/lib/semmle/code/java/security/SensitiveLoggingQuery.qll b/java/ql/lib/semmle/code/java/security/SensitiveLoggingQuery.qll index 855d05168cc6..4692427f1cde 100644 --- a/java/ql/lib/semmle/code/java/security/SensitiveLoggingQuery.qll +++ b/java/ql/lib/semmle/code/java/security/SensitiveLoggingQuery.qll @@ -44,8 +44,9 @@ private class TypeType extends RefType { } } -/** A sanitizer that may remove sensitive information from a string before logging. - * +/** + * A sanitizer that may remove sensitive information from a string before logging. + * * It allows for substring operations taking the first N (or last N, for Kotlin) characters, limited to 7 or fewer. */ private class SensitiveLoggerSanitizerCalled extends SensitiveLoggerBarrier { @@ -65,7 +66,8 @@ private class SensitiveLoggerSanitizerCalled extends SensitiveLoggerBarrier { or // Kotlin string operations, which use extension methods (so the string is the first argument) ( - m.hasQualifiedName("kotlin.text", "StringsKt", "substring") and twoArgLimit(mc, limit, true) + m.hasQualifiedName("kotlin.text", "StringsKt", "substring") and + twoArgLimit(mc, limit, true) or m.hasQualifiedName("kotlin.text", "StringsKt", ["take", "takeLast"]) and singleArgLimit(mc, limit, true) @@ -76,15 +78,18 @@ private class SensitiveLoggerSanitizerCalled extends SensitiveLoggerBarrier { } } +/** A predicate to check single-argument method calls for a constant integer below a set limit. */ bindingset[limit, isKotlin] -predicate singleArgLimit(MethodCall mc, int limit, boolean isKotlin) { +private predicate singleArgLimit(MethodCall mc, int limit, boolean isKotlin) { exists(int argIndex, int staticInt | (if isKotlin = true then argIndex = 1 else argIndex = 0) and ( staticInt <= limit and staticInt > 0 and - mc.getArgument(argIndex).getUnderlyingExpr().(CompileTimeConstantExpr).getIntValue() = staticInt - or exists(CompileTimeConstantExpr cte, DataFlow::Node source, DataFlow::Node sink | + mc.getArgument(argIndex).getUnderlyingExpr().(CompileTimeConstantExpr).getIntValue() = + staticInt + or + exists(CompileTimeConstantExpr cte, DataFlow::Node source, DataFlow::Node sink | source.asExpr() = cte and cte.getIntValue() = staticInt and sink.asExpr() = mc.getArgument(argIndex) and @@ -94,21 +99,23 @@ predicate singleArgLimit(MethodCall mc, int limit, boolean isKotlin) { ) } +/** A predicate to check two-argument method calls for zero and a constant integer below a set limit. */ bindingset[limit, isKotlin] -predicate twoArgLimit(MethodCall mc, int limit, boolean isKotlin) { +private predicate twoArgLimit(MethodCall mc, int limit, boolean isKotlin) { exists(int firstArgIndex, int secondArgIndex, int staticInt | staticInt <= limit and staticInt > 0 and ( - (isKotlin = true and firstArgIndex = 1 and secondArgIndex = 2) + isKotlin = true and firstArgIndex = 1 and secondArgIndex = 2 or - (isKotlin = false and firstArgIndex = 0 and secondArgIndex = 1) - ) - and + isKotlin = false and firstArgIndex = 0 and secondArgIndex = 1 + ) and mc.getArgument(firstArgIndex).getUnderlyingExpr().(CompileTimeConstantExpr).getIntValue() = 0 and ( - mc.getArgument(secondArgIndex).getUnderlyingExpr().(CompileTimeConstantExpr).getIntValue() = staticInt - or exists(CompileTimeConstantExpr cte, DataFlow::Node source, DataFlow::Node sink | + mc.getArgument(secondArgIndex).getUnderlyingExpr().(CompileTimeConstantExpr).getIntValue() = + staticInt + or + exists(CompileTimeConstantExpr cte, DataFlow::Node source, DataFlow::Node sink | source.asExpr() = cte and cte.getIntValue() = staticInt and sink.asExpr() = mc.getArgument(secondArgIndex) and @@ -118,7 +125,8 @@ predicate twoArgLimit(MethodCall mc, int limit, boolean isKotlin) { ) } -module IntegerToArgConfig implements DataFlow::ConfigSig { +/** A data-flow configuration for identifying flow from a constant integer to a use in a method argument. */ +private module IntegerToArgConfig implements DataFlow::ConfigSig { predicate isSource(DataFlow::Node source) { source.asExpr().getUnderlyingExpr() instanceof CompileTimeConstantExpr and source.asExpr().getType() instanceof IntegralType and @@ -127,8 +135,8 @@ module IntegerToArgConfig implements DataFlow::ConfigSig { predicate isSink(DataFlow::Node sink) { exists(MethodCall mc | - sink.asExpr() = mc.getAnArgument() - and sink.asExpr().getType() instanceof IntegralType + sink.asExpr() = mc.getAnArgument() and + sink.asExpr().getType() instanceof IntegralType ) } @@ -159,4 +167,5 @@ module SensitiveLoggerConfig implements DataFlow::ConfigSig { } module SensitiveLoggerFlow = TaintTracking::Global; + module IntegerToArgFlow = TaintTracking::Global; From fa703e3e6040b276b96cb708683dd26985dc0bf8 Mon Sep 17 00:00:00 2001 From: aegilops <41705651+aegilops@users.noreply.github.com> Date: Fri, 14 Nov 2025 16:53:46 +0000 Subject: [PATCH 03/12] Test cases for sensitive logging sanitizer --- java/ql/test/query-tests/security/CWE-532/Test.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/java/ql/test/query-tests/security/CWE-532/Test.java b/java/ql/test/query-tests/security/CWE-532/Test.java index cf983afc287d..77086bf31d58 100644 --- a/java/ql/test/query-tests/security/CWE-532/Test.java +++ b/java/ql/test/query-tests/security/CWE-532/Test.java @@ -9,5 +9,7 @@ void test(String password, String authToken, String username, String nullToken, logger.error("Auth failed for: " + username); // Safe logger.error("Auth failed for: " + nullToken); // Safe logger.error("Auth failed for: " + stringTokenizer); // Safe + logger.error("Auth failed for: " + authToken.substring(4) + "..."); // Safe + logger.error("Auth failed for: " + authToken.substring(0,4) + "..."); // Safe } } From 0c0fbc1457afb525885d18f0c48daebec0851318 Mon Sep 17 00:00:00 2001 From: aegilops <41705651+aegilops@users.noreply.github.com> Date: Fri, 14 Nov 2025 18:12:05 +0000 Subject: [PATCH 04/12] Fixed sensitive logging barriers for substring to allow single-arg use --- .../code/java/security/SensitiveLoggingQuery.qll | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/java/ql/lib/semmle/code/java/security/SensitiveLoggingQuery.qll b/java/ql/lib/semmle/code/java/security/SensitiveLoggingQuery.qll index 4692427f1cde..f8692902d026 100644 --- a/java/ql/lib/semmle/code/java/security/SensitiveLoggingQuery.qll +++ b/java/ql/lib/semmle/code/java/security/SensitiveLoggingQuery.qll @@ -61,13 +61,19 @@ private class SensitiveLoggerSanitizerCalled extends SensitiveLoggerBarrier { m.hasQualifiedName("java.lang", "StringBuffer", "substring") or m.hasQualifiedName("java.lang", "StringBuilder", "substring") ) and - twoArgLimit(mc, limit, false) and + ( + twoArgLimit(mc, limit, false) or + singleArgLimit(mc, limit, false) + ) and this.asExpr() = mc.getQualifier() or // Kotlin string operations, which use extension methods (so the string is the first argument) ( m.hasQualifiedName("kotlin.text", "StringsKt", "substring") and - twoArgLimit(mc, limit, true) + ( + twoArgLimit(mc, limit, true) or + singleArgLimit(mc, limit, true) + ) or m.hasQualifiedName("kotlin.text", "StringsKt", ["take", "takeLast"]) and singleArgLimit(mc, limit, true) From 528c451007838bc4b071204605d3c5a4ae737681 Mon Sep 17 00:00:00 2001 From: aegilops <41705651+aegilops@users.noreply.github.com> Date: Mon, 17 Nov 2025 11:02:59 +0000 Subject: [PATCH 05/12] Added change note, adjusted spacing in comment --- .../lib/semmle/code/java/security/SensitiveLoggingQuery.qll | 2 +- .../2025-11-17-sensitive-logging-new-sanitizers.md | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 java/ql/src/change-notes/2025-11-17-sensitive-logging-new-sanitizers.md diff --git a/java/ql/lib/semmle/code/java/security/SensitiveLoggingQuery.qll b/java/ql/lib/semmle/code/java/security/SensitiveLoggingQuery.qll index f8692902d026..243fc71ea325 100644 --- a/java/ql/lib/semmle/code/java/security/SensitiveLoggingQuery.qll +++ b/java/ql/lib/semmle/code/java/security/SensitiveLoggingQuery.qll @@ -47,7 +47,7 @@ private class TypeType extends RefType { /** * A sanitizer that may remove sensitive information from a string before logging. * - * It allows for substring operations taking the first N (or last N, for Kotlin) characters, limited to 7 or fewer. + * It allows for substring operations taking the first N (or last N, for Kotlin) characters, limited to 7 or fewer. */ private class SensitiveLoggerSanitizerCalled extends SensitiveLoggerBarrier { SensitiveLoggerSanitizerCalled() { diff --git a/java/ql/src/change-notes/2025-11-17-sensitive-logging-new-sanitizers.md b/java/ql/src/change-notes/2025-11-17-sensitive-logging-new-sanitizers.md new file mode 100644 index 000000000000..2e744e2ab2c2 --- /dev/null +++ b/java/ql/src/change-notes/2025-11-17-sensitive-logging-new-sanitizers.md @@ -0,0 +1,4 @@ +--- +category: minorAnalysis +--- +* Calls to `substring` (for Java), `take` (for Kotlin) and similar functions, when called with a fixed length less than or equal to 7, are now treated as sanitizers for the `java/sensitive-log` query. \ No newline at end of file From 62ee6d3a33f7245d1c8f67164d4ace9fd02931e3 Mon Sep 17 00:00:00 2001 From: aegilops <41705651+aegilops@users.noreply.github.com> Date: Thu, 20 Nov 2025 11:46:42 +0000 Subject: [PATCH 06/12] Made changes requested by reviewers - bounded() for range checking, style and better comments --- .../java/security/SensitiveLoggingQuery.qll | 111 +++++------------- ...-11-17-sensitive-logging-new-sanitizers.md | 2 +- .../query-tests/security/CWE-532/Test.java | 2 + 3 files changed, 35 insertions(+), 80 deletions(-) diff --git a/java/ql/lib/semmle/code/java/security/SensitiveLoggingQuery.qll b/java/ql/lib/semmle/code/java/security/SensitiveLoggingQuery.qll index 243fc71ea325..abd2da7ec216 100644 --- a/java/ql/lib/semmle/code/java/security/SensitiveLoggingQuery.qll +++ b/java/ql/lib/semmle/code/java/security/SensitiveLoggingQuery.qll @@ -6,7 +6,7 @@ import semmle.code.java.dataflow.TaintTracking import semmle.code.java.security.SensitiveActions import semmle.code.java.frameworks.android.Compose private import semmle.code.java.security.Sanitizers -import semmle.code.java.Constants +private import semmle.code.java.dataflow.RangeAnalysis /** A data flow source node for sensitive logging sources. */ abstract class SensitiveLoggerSource extends DataFlow::Node { } @@ -49,37 +49,36 @@ private class TypeType extends RefType { * * It allows for substring operations taking the first N (or last N, for Kotlin) characters, limited to 7 or fewer. */ -private class SensitiveLoggerSanitizerCalled extends SensitiveLoggerBarrier { - SensitiveLoggerSanitizerCalled() { +private class PrefixSuffixBarrier extends SensitiveLoggerBarrier { + PrefixSuffixBarrier() { exists(MethodCall mc, Method m, int limit | limit = 7 and - mc.getMethod() = m and + mc.getMethod() = m + | + // substring in Java ( - // substring in Java - ( - m.hasQualifiedName("java.lang", "String", "substring") or - m.hasQualifiedName("java.lang", "StringBuffer", "substring") or - m.hasQualifiedName("java.lang", "StringBuilder", "substring") - ) and - ( - twoArgLimit(mc, limit, false) or - singleArgLimit(mc, limit, false) - ) and - this.asExpr() = mc.getQualifier() - or - // Kotlin string operations, which use extension methods (so the string is the first argument) + m.hasQualifiedName("java.lang", "String", "substring") or + m.hasQualifiedName("java.lang", "StringBuffer", "substring") or + m.hasQualifiedName("java.lang", "StringBuilder", "substring") + ) and + ( + twoArgLimit(mc, limit, false) or + singleArgLimit(mc, limit, false) + ) and + this.asExpr() = mc.getQualifier() + or + // Kotlin string operations, which use extension methods (so the string is the first argument) + ( + m.hasQualifiedName("kotlin.text", "StringsKt", "substring") and ( - m.hasQualifiedName("kotlin.text", "StringsKt", "substring") and - ( - twoArgLimit(mc, limit, true) or - singleArgLimit(mc, limit, true) - ) - or - m.hasQualifiedName("kotlin.text", "StringsKt", ["take", "takeLast"]) and + twoArgLimit(mc, limit, true) or singleArgLimit(mc, limit, true) - ) and - this.asExpr() = mc.getArgument(0) - ) + ) + or + m.hasQualifiedName("kotlin.text", "StringsKt", ["take", "takeLast"]) and + singleArgLimit(mc, limit, true) + ) and + this.asExpr() = mc.getArgument(0) ) } } @@ -87,72 +86,28 @@ private class SensitiveLoggerSanitizerCalled extends SensitiveLoggerBarrier { /** A predicate to check single-argument method calls for a constant integer below a set limit. */ bindingset[limit, isKotlin] private predicate singleArgLimit(MethodCall mc, int limit, boolean isKotlin) { - exists(int argIndex, int staticInt | + exists(int argIndex | (if isKotlin = true then argIndex = 1 else argIndex = 0) and - ( - staticInt <= limit and - staticInt > 0 and - mc.getArgument(argIndex).getUnderlyingExpr().(CompileTimeConstantExpr).getIntValue() = - staticInt - or - exists(CompileTimeConstantExpr cte, DataFlow::Node source, DataFlow::Node sink | - source.asExpr() = cte and - cte.getIntValue() = staticInt and - sink.asExpr() = mc.getArgument(argIndex) and - IntegerToArgFlow::flow(source, sink) - ) - ) + bounded(mc.getArgument(argIndex), any(ZeroBound z), limit, true, _) ) } /** A predicate to check two-argument method calls for zero and a constant integer below a set limit. */ bindingset[limit, isKotlin] private predicate twoArgLimit(MethodCall mc, int limit, boolean isKotlin) { - exists(int firstArgIndex, int secondArgIndex, int staticInt | - staticInt <= limit and - staticInt > 0 and + exists(int firstArgIndex, int secondArgIndex | ( isKotlin = true and firstArgIndex = 1 and secondArgIndex = 2 or isKotlin = false and firstArgIndex = 0 and secondArgIndex = 1 ) and mc.getArgument(firstArgIndex).getUnderlyingExpr().(CompileTimeConstantExpr).getIntValue() = 0 and - ( - mc.getArgument(secondArgIndex).getUnderlyingExpr().(CompileTimeConstantExpr).getIntValue() = - staticInt - or - exists(CompileTimeConstantExpr cte, DataFlow::Node source, DataFlow::Node sink | - source.asExpr() = cte and - cte.getIntValue() = staticInt and - sink.asExpr() = mc.getArgument(secondArgIndex) and - IntegerToArgFlow::flow(source, sink) - ) - ) + bounded(mc.getArgument(secondArgIndex), any(ZeroBound z), limit, true, _) ) } -/** A data-flow configuration for identifying flow from a constant integer to a use in a method argument. */ -private module IntegerToArgConfig implements DataFlow::ConfigSig { - predicate isSource(DataFlow::Node source) { - source.asExpr().getUnderlyingExpr() instanceof CompileTimeConstantExpr and - source.asExpr().getType() instanceof IntegralType and - source.asExpr().(CompileTimeConstantExpr).getIntValue() > 0 - } - - predicate isSink(DataFlow::Node sink) { - exists(MethodCall mc | - sink.asExpr() = mc.getAnArgument() and - sink.asExpr().getType() instanceof IntegralType - ) - } - - predicate isBarrier(DataFlow::Node sanitizer) { none() } - - predicate isBarrierIn(DataFlow::Node node) { none() } -} - -private class GenericSanitizer extends SensitiveLoggerBarrier { - GenericSanitizer() { +private class DefaultSensitiveLoggerBarrier extends SensitiveLoggerBarrier { + DefaultSensitiveLoggerBarrier() { this.asExpr() instanceof LiveLiteral or this instanceof SimpleTypeSanitizer or this.getType() instanceof TypeType @@ -173,5 +128,3 @@ module SensitiveLoggerConfig implements DataFlow::ConfigSig { } module SensitiveLoggerFlow = TaintTracking::Global; - -module IntegerToArgFlow = TaintTracking::Global; diff --git a/java/ql/src/change-notes/2025-11-17-sensitive-logging-new-sanitizers.md b/java/ql/src/change-notes/2025-11-17-sensitive-logging-new-sanitizers.md index 2e744e2ab2c2..a3266e4d9e60 100644 --- a/java/ql/src/change-notes/2025-11-17-sensitive-logging-new-sanitizers.md +++ b/java/ql/src/change-notes/2025-11-17-sensitive-logging-new-sanitizers.md @@ -1,4 +1,4 @@ --- category: minorAnalysis --- -* Calls to `substring` (for Java), `take` (for Kotlin) and similar functions, when called with a fixed length less than or equal to 7, are now treated as sanitizers for the `java/sensitive-log` query. \ No newline at end of file +* Operations that extract only a fixed-length prefix or suffix of a string (for example, `substring` in Java or `take` in Kotlin), when limited to a length of at most 7 characters, are now treated as sanitizers for the `java/sensitive-log` query. \ No newline at end of file diff --git a/java/ql/test/query-tests/security/CWE-532/Test.java b/java/ql/test/query-tests/security/CWE-532/Test.java index 77086bf31d58..2eed3e79342d 100644 --- a/java/ql/test/query-tests/security/CWE-532/Test.java +++ b/java/ql/test/query-tests/security/CWE-532/Test.java @@ -11,5 +11,7 @@ void test(String password, String authToken, String username, String nullToken, logger.error("Auth failed for: " + stringTokenizer); // Safe logger.error("Auth failed for: " + authToken.substring(4) + "..."); // Safe logger.error("Auth failed for: " + authToken.substring(0,4) + "..."); // Safe + logger.error("Auth failed for: " + authToken.substring(1,5) + "..."); // $ Alert + logger.error("Auth failed for: " + authToken.substring(0,8) + "..."); // $ Alert } } From 29a5b27b139790907cb9c63478fd4d6cb28b091f Mon Sep 17 00:00:00 2001 From: aegilops <41705651+aegilops@users.noreply.github.com> Date: Thu, 20 Nov 2025 17:31:56 +0000 Subject: [PATCH 07/12] Removed bounds checking and only using literals - bounded() predicate did not work --- .../code/java/security/SensitiveLoggingQuery.qll | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/java/ql/lib/semmle/code/java/security/SensitiveLoggingQuery.qll b/java/ql/lib/semmle/code/java/security/SensitiveLoggingQuery.qll index abd2da7ec216..dee817705147 100644 --- a/java/ql/lib/semmle/code/java/security/SensitiveLoggingQuery.qll +++ b/java/ql/lib/semmle/code/java/security/SensitiveLoggingQuery.qll @@ -54,7 +54,7 @@ private class PrefixSuffixBarrier extends SensitiveLoggerBarrier { exists(MethodCall mc, Method m, int limit | limit = 7 and mc.getMethod() = m - | + | // substring in Java ( m.hasQualifiedName("java.lang", "String", "substring") or @@ -86,15 +86,17 @@ private class PrefixSuffixBarrier extends SensitiveLoggerBarrier { /** A predicate to check single-argument method calls for a constant integer below a set limit. */ bindingset[limit, isKotlin] private predicate singleArgLimit(MethodCall mc, int limit, boolean isKotlin) { - exists(int argIndex | - (if isKotlin = true then argIndex = 1 else argIndex = 0) and - bounded(mc.getArgument(argIndex), any(ZeroBound z), limit, true, _) + mc.getNumArgument() = 1 and + exists(int firstArgIndex | + (if isKotlin = true then firstArgIndex = 1 else firstArgIndex = 0) and + mc.getArgument(firstArgIndex).getUnderlyingExpr().(CompileTimeConstantExpr).getIntValue() <= limit ) } /** A predicate to check two-argument method calls for zero and a constant integer below a set limit. */ bindingset[limit, isKotlin] private predicate twoArgLimit(MethodCall mc, int limit, boolean isKotlin) { + mc.getNumArgument() = 2 and exists(int firstArgIndex, int secondArgIndex | ( isKotlin = true and firstArgIndex = 1 and secondArgIndex = 2 @@ -102,7 +104,7 @@ private predicate twoArgLimit(MethodCall mc, int limit, boolean isKotlin) { isKotlin = false and firstArgIndex = 0 and secondArgIndex = 1 ) and mc.getArgument(firstArgIndex).getUnderlyingExpr().(CompileTimeConstantExpr).getIntValue() = 0 and - bounded(mc.getArgument(secondArgIndex), any(ZeroBound z), limit, true, _) + mc.getArgument(secondArgIndex).getUnderlyingExpr().(CompileTimeConstantExpr).getIntValue() <= limit ) } From e904520779445223b7d51d8062960dc401232474 Mon Sep 17 00:00:00 2001 From: aegilops <41705651+aegilops@users.noreply.github.com> Date: Thu, 20 Nov 2025 17:34:42 +0000 Subject: [PATCH 08/12] Fixed formatting --- .../security/CWE-532/SensitiveLogInfo.expected | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/java/ql/test/query-tests/security/CWE-532/SensitiveLogInfo.expected b/java/ql/test/query-tests/security/CWE-532/SensitiveLogInfo.expected index e32ff5654e25..3827d2894ad4 100644 --- a/java/ql/test/query-tests/security/CWE-532/SensitiveLogInfo.expected +++ b/java/ql/test/query-tests/security/CWE-532/SensitiveLogInfo.expected @@ -1,15 +1,28 @@ #select | Test.java:7:21:7:53 | ... + ... | Test.java:7:46:7:53 | password : String | Test.java:7:21:7:53 | ... + ... | This $@ is written to a log file. | Test.java:7:46:7:53 | password | potentially sensitive information | | Test.java:8:22:8:52 | ... + ... | Test.java:8:44:8:52 | authToken : String | Test.java:8:22:8:52 | ... + ... | This $@ is written to a log file. | Test.java:8:44:8:52 | authToken | potentially sensitive information | +| Test.java:14:22:14:75 | ... + ... | Test.java:14:44:14:52 | authToken : String | Test.java:14:22:14:75 | ... + ... | This $@ is written to a log file. | Test.java:14:44:14:52 | authToken | potentially sensitive information | +| Test.java:15:22:15:75 | ... + ... | Test.java:15:44:15:52 | authToken : String | Test.java:15:22:15:75 | ... + ... | This $@ is written to a log file. | Test.java:15:44:15:52 | authToken | potentially sensitive information | edges | Test.java:7:46:7:53 | password : String | Test.java:7:21:7:53 | ... + ... | provenance | Sink:MaD:2 | | Test.java:8:44:8:52 | authToken : String | Test.java:8:22:8:52 | ... + ... | provenance | Sink:MaD:1 | +| Test.java:14:44:14:52 | authToken : String | Test.java:14:44:14:67 | substring(...) : String | provenance | MaD:3 | +| Test.java:14:44:14:67 | substring(...) : String | Test.java:14:22:14:75 | ... + ... | provenance | Sink:MaD:1 | +| Test.java:15:44:15:52 | authToken : String | Test.java:15:44:15:67 | substring(...) : String | provenance | MaD:3 | +| Test.java:15:44:15:67 | substring(...) : String | Test.java:15:22:15:75 | ... + ... | provenance | Sink:MaD:1 | models | 1 | Sink: org.apache.logging.log4j; Logger; true; error; (String); ; Argument[0]; log-injection; manual | | 2 | Sink: org.apache.logging.log4j; Logger; true; info; (String); ; Argument[0]; log-injection; manual | +| 3 | Summary: java.lang; String; false; substring; ; ; Argument[this]; ReturnValue; taint; manual | nodes | Test.java:7:21:7:53 | ... + ... | semmle.label | ... + ... | | Test.java:7:46:7:53 | password : String | semmle.label | password : String | | Test.java:8:22:8:52 | ... + ... | semmle.label | ... + ... | | Test.java:8:44:8:52 | authToken : String | semmle.label | authToken : String | +| Test.java:14:22:14:75 | ... + ... | semmle.label | ... + ... | +| Test.java:14:44:14:52 | authToken : String | semmle.label | authToken : String | +| Test.java:14:44:14:67 | substring(...) : String | semmle.label | substring(...) : String | +| Test.java:15:22:15:75 | ... + ... | semmle.label | ... + ... | +| Test.java:15:44:15:52 | authToken : String | semmle.label | authToken : String | +| Test.java:15:44:15:67 | substring(...) : String | semmle.label | substring(...) : String | subpaths From ce136684e64223f7d6a75df473132dcbe6c9c242 Mon Sep 17 00:00:00 2001 From: aegilops <41705651+aegilops@users.noreply.github.com> Date: Thu, 20 Nov 2025 17:39:32 +0000 Subject: [PATCH 09/12] Fixed formatting --- .../semmle/code/java/security/SensitiveLoggingQuery.qll | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/java/ql/lib/semmle/code/java/security/SensitiveLoggingQuery.qll b/java/ql/lib/semmle/code/java/security/SensitiveLoggingQuery.qll index dee817705147..b4eecdcdb4c1 100644 --- a/java/ql/lib/semmle/code/java/security/SensitiveLoggingQuery.qll +++ b/java/ql/lib/semmle/code/java/security/SensitiveLoggingQuery.qll @@ -54,7 +54,7 @@ private class PrefixSuffixBarrier extends SensitiveLoggerBarrier { exists(MethodCall mc, Method m, int limit | limit = 7 and mc.getMethod() = m - | + | // substring in Java ( m.hasQualifiedName("java.lang", "String", "substring") or @@ -89,7 +89,8 @@ private predicate singleArgLimit(MethodCall mc, int limit, boolean isKotlin) { mc.getNumArgument() = 1 and exists(int firstArgIndex | (if isKotlin = true then firstArgIndex = 1 else firstArgIndex = 0) and - mc.getArgument(firstArgIndex).getUnderlyingExpr().(CompileTimeConstantExpr).getIntValue() <= limit + mc.getArgument(firstArgIndex).getUnderlyingExpr().(CompileTimeConstantExpr).getIntValue() <= + limit ) } @@ -104,7 +105,8 @@ private predicate twoArgLimit(MethodCall mc, int limit, boolean isKotlin) { isKotlin = false and firstArgIndex = 0 and secondArgIndex = 1 ) and mc.getArgument(firstArgIndex).getUnderlyingExpr().(CompileTimeConstantExpr).getIntValue() = 0 and - mc.getArgument(secondArgIndex).getUnderlyingExpr().(CompileTimeConstantExpr).getIntValue() <= limit + mc.getArgument(secondArgIndex).getUnderlyingExpr().(CompileTimeConstantExpr).getIntValue() <= + limit ) } From ec381e4ec579728b9b2ce9cbe99f92b4cc45b414 Mon Sep 17 00:00:00 2001 From: Owen Mansel-Chan <62447351+owen-mc@users.noreply.github.com> Date: Fri, 21 Nov 2025 10:31:50 +0000 Subject: [PATCH 10/12] Use range analysis and improve tests --- .../java/security/SensitiveLoggingQuery.qll | 28 +++++++------ .../CWE-532/SensitiveLogInfo.expected | 40 +++++++++---------- .../query-tests/security/CWE-532/Test.java | 7 ++++ 3 files changed, 42 insertions(+), 33 deletions(-) diff --git a/java/ql/lib/semmle/code/java/security/SensitiveLoggingQuery.qll b/java/ql/lib/semmle/code/java/security/SensitiveLoggingQuery.qll index b4eecdcdb4c1..315f915b2e15 100644 --- a/java/ql/lib/semmle/code/java/security/SensitiveLoggingQuery.qll +++ b/java/ql/lib/semmle/code/java/security/SensitiveLoggingQuery.qll @@ -87,10 +87,11 @@ private class PrefixSuffixBarrier extends SensitiveLoggerBarrier { bindingset[limit, isKotlin] private predicate singleArgLimit(MethodCall mc, int limit, boolean isKotlin) { mc.getNumArgument() = 1 and - exists(int firstArgIndex | - (if isKotlin = true then firstArgIndex = 1 else firstArgIndex = 0) and - mc.getArgument(firstArgIndex).getUnderlyingExpr().(CompileTimeConstantExpr).getIntValue() <= - limit + exists(int firstArgIndex, int delta | + if isKotlin = true then firstArgIndex = 1 else firstArgIndex = 0 + | + bounded(mc.getArgument(firstArgIndex).getUnderlyingExpr(), any(ZeroBound z), delta, true, _) and + delta <= limit ) } @@ -98,15 +99,16 @@ private predicate singleArgLimit(MethodCall mc, int limit, boolean isKotlin) { bindingset[limit, isKotlin] private predicate twoArgLimit(MethodCall mc, int limit, boolean isKotlin) { mc.getNumArgument() = 2 and - exists(int firstArgIndex, int secondArgIndex | - ( - isKotlin = true and firstArgIndex = 1 and secondArgIndex = 2 - or - isKotlin = false and firstArgIndex = 0 and secondArgIndex = 1 - ) and - mc.getArgument(firstArgIndex).getUnderlyingExpr().(CompileTimeConstantExpr).getIntValue() = 0 and - mc.getArgument(secondArgIndex).getUnderlyingExpr().(CompileTimeConstantExpr).getIntValue() <= - limit + exists(int firstArgIndex, int secondArgIndex, int delta | + isKotlin = true and firstArgIndex = 1 and secondArgIndex = 2 + or + isKotlin = false and firstArgIndex = 0 and secondArgIndex = 1 + | + // mc.getArgument(firstArgIndex).getUnderlyingExpr().(CompileTimeConstantExpr).getIntValue() = 0 and + bounded(mc.getArgument(firstArgIndex).getUnderlyingExpr(), any(ZeroBound z), 0, true, _) and + bounded(mc.getArgument(firstArgIndex).getUnderlyingExpr(), any(ZeroBound z), 0, false, _) and + bounded(mc.getArgument(secondArgIndex).getUnderlyingExpr(), any(ZeroBound z), delta, true, _) and + delta <= limit ) } diff --git a/java/ql/test/query-tests/security/CWE-532/SensitiveLogInfo.expected b/java/ql/test/query-tests/security/CWE-532/SensitiveLogInfo.expected index 3827d2894ad4..54f1e9f8a5aa 100644 --- a/java/ql/test/query-tests/security/CWE-532/SensitiveLogInfo.expected +++ b/java/ql/test/query-tests/security/CWE-532/SensitiveLogInfo.expected @@ -1,28 +1,28 @@ #select -| Test.java:7:21:7:53 | ... + ... | Test.java:7:46:7:53 | password : String | Test.java:7:21:7:53 | ... + ... | This $@ is written to a log file. | Test.java:7:46:7:53 | password | potentially sensitive information | -| Test.java:8:22:8:52 | ... + ... | Test.java:8:44:8:52 | authToken : String | Test.java:8:22:8:52 | ... + ... | This $@ is written to a log file. | Test.java:8:44:8:52 | authToken | potentially sensitive information | -| Test.java:14:22:14:75 | ... + ... | Test.java:14:44:14:52 | authToken : String | Test.java:14:22:14:75 | ... + ... | This $@ is written to a log file. | Test.java:14:44:14:52 | authToken | potentially sensitive information | -| Test.java:15:22:15:75 | ... + ... | Test.java:15:44:15:52 | authToken : String | Test.java:15:22:15:75 | ... + ... | This $@ is written to a log file. | Test.java:15:44:15:52 | authToken | potentially sensitive information | +| Test.java:11:21:11:53 | ... + ... | Test.java:11:46:11:53 | password : String | Test.java:11:21:11:53 | ... + ... | This $@ is written to a log file. | Test.java:11:46:11:53 | password | potentially sensitive information | +| Test.java:12:22:12:52 | ... + ... | Test.java:12:44:12:52 | authToken : String | Test.java:12:22:12:52 | ... + ... | This $@ is written to a log file. | Test.java:12:44:12:52 | authToken | potentially sensitive information | +| Test.java:21:22:21:75 | ... + ... | Test.java:21:44:21:52 | authToken : String | Test.java:21:22:21:75 | ... + ... | This $@ is written to a log file. | Test.java:21:44:21:52 | authToken | potentially sensitive information | +| Test.java:22:22:22:75 | ... + ... | Test.java:22:44:22:52 | authToken : String | Test.java:22:22:22:75 | ... + ... | This $@ is written to a log file. | Test.java:22:44:22:52 | authToken | potentially sensitive information | edges -| Test.java:7:46:7:53 | password : String | Test.java:7:21:7:53 | ... + ... | provenance | Sink:MaD:2 | -| Test.java:8:44:8:52 | authToken : String | Test.java:8:22:8:52 | ... + ... | provenance | Sink:MaD:1 | -| Test.java:14:44:14:52 | authToken : String | Test.java:14:44:14:67 | substring(...) : String | provenance | MaD:3 | -| Test.java:14:44:14:67 | substring(...) : String | Test.java:14:22:14:75 | ... + ... | provenance | Sink:MaD:1 | -| Test.java:15:44:15:52 | authToken : String | Test.java:15:44:15:67 | substring(...) : String | provenance | MaD:3 | -| Test.java:15:44:15:67 | substring(...) : String | Test.java:15:22:15:75 | ... + ... | provenance | Sink:MaD:1 | +| Test.java:11:46:11:53 | password : String | Test.java:11:21:11:53 | ... + ... | provenance | Sink:MaD:2 | +| Test.java:12:44:12:52 | authToken : String | Test.java:12:22:12:52 | ... + ... | provenance | Sink:MaD:1 | +| Test.java:21:44:21:52 | authToken : String | Test.java:21:44:21:67 | substring(...) : String | provenance | MaD:3 | +| Test.java:21:44:21:67 | substring(...) : String | Test.java:21:22:21:75 | ... + ... | provenance | Sink:MaD:1 | +| Test.java:22:44:22:52 | authToken : String | Test.java:22:44:22:67 | substring(...) : String | provenance | MaD:3 | +| Test.java:22:44:22:67 | substring(...) : String | Test.java:22:22:22:75 | ... + ... | provenance | Sink:MaD:1 | models | 1 | Sink: org.apache.logging.log4j; Logger; true; error; (String); ; Argument[0]; log-injection; manual | | 2 | Sink: org.apache.logging.log4j; Logger; true; info; (String); ; Argument[0]; log-injection; manual | | 3 | Summary: java.lang; String; false; substring; ; ; Argument[this]; ReturnValue; taint; manual | nodes -| Test.java:7:21:7:53 | ... + ... | semmle.label | ... + ... | -| Test.java:7:46:7:53 | password : String | semmle.label | password : String | -| Test.java:8:22:8:52 | ... + ... | semmle.label | ... + ... | -| Test.java:8:44:8:52 | authToken : String | semmle.label | authToken : String | -| Test.java:14:22:14:75 | ... + ... | semmle.label | ... + ... | -| Test.java:14:44:14:52 | authToken : String | semmle.label | authToken : String | -| Test.java:14:44:14:67 | substring(...) : String | semmle.label | substring(...) : String | -| Test.java:15:22:15:75 | ... + ... | semmle.label | ... + ... | -| Test.java:15:44:15:52 | authToken : String | semmle.label | authToken : String | -| Test.java:15:44:15:67 | substring(...) : String | semmle.label | substring(...) : String | +| Test.java:11:21:11:53 | ... + ... | semmle.label | ... + ... | +| Test.java:11:46:11:53 | password : String | semmle.label | password : String | +| Test.java:12:22:12:52 | ... + ... | semmle.label | ... + ... | +| Test.java:12:44:12:52 | authToken : String | semmle.label | authToken : String | +| Test.java:21:22:21:75 | ... + ... | semmle.label | ... + ... | +| Test.java:21:44:21:52 | authToken : String | semmle.label | authToken : String | +| Test.java:21:44:21:67 | substring(...) : String | semmle.label | substring(...) : String | +| Test.java:22:22:22:75 | ... + ... | semmle.label | ... + ... | +| Test.java:22:44:22:52 | authToken : String | semmle.label | authToken : String | +| Test.java:22:44:22:67 | substring(...) : String | semmle.label | substring(...) : String | subpaths diff --git a/java/ql/test/query-tests/security/CWE-532/Test.java b/java/ql/test/query-tests/security/CWE-532/Test.java index 2eed3e79342d..0383f521ff37 100644 --- a/java/ql/test/query-tests/security/CWE-532/Test.java +++ b/java/ql/test/query-tests/security/CWE-532/Test.java @@ -3,6 +3,10 @@ class Test { void test(String password, String authToken, String username, String nullToken, String stringTokenizer) { Logger logger = null; + int zero = 0; + int four = 4; + short zeroS = 0; + long fourL = 4; logger.info("User's password is: " + password); // $ Alert logger.error("Auth failed for: " + authToken); // $ Alert @@ -10,7 +14,10 @@ void test(String password, String authToken, String username, String nullToken, logger.error("Auth failed for: " + nullToken); // Safe logger.error("Auth failed for: " + stringTokenizer); // Safe logger.error("Auth failed for: " + authToken.substring(4) + "..."); // Safe + logger.error("Auth failed for: " + authToken.substring(four) + "..."); // Safe logger.error("Auth failed for: " + authToken.substring(0,4) + "..."); // Safe + logger.error("Auth failed for: " + authToken.substring(zero,four) + "..."); // Safe + logger.error("Auth failed for: " + authToken.substring((int)zeroS,(int)fourL) + "..."); // Safe logger.error("Auth failed for: " + authToken.substring(1,5) + "..."); // $ Alert logger.error("Auth failed for: " + authToken.substring(0,8) + "..."); // $ Alert } From e37336d5504001dcf2f17bdc84769fe005e6f353 Mon Sep 17 00:00:00 2001 From: Owen Mansel-Chan <62447351+owen-mc@users.noreply.github.com> Date: Mon, 24 Nov 2025 14:10:20 +0000 Subject: [PATCH 11/12] No need for `getUnderlyingExpr` to look through casts --- .../code/java/security/SensitiveLoggingQuery.qll | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/java/ql/lib/semmle/code/java/security/SensitiveLoggingQuery.qll b/java/ql/lib/semmle/code/java/security/SensitiveLoggingQuery.qll index 315f915b2e15..7058b844cbdb 100644 --- a/java/ql/lib/semmle/code/java/security/SensitiveLoggingQuery.qll +++ b/java/ql/lib/semmle/code/java/security/SensitiveLoggingQuery.qll @@ -90,7 +90,7 @@ private predicate singleArgLimit(MethodCall mc, int limit, boolean isKotlin) { exists(int firstArgIndex, int delta | if isKotlin = true then firstArgIndex = 1 else firstArgIndex = 0 | - bounded(mc.getArgument(firstArgIndex).getUnderlyingExpr(), any(ZeroBound z), delta, true, _) and + bounded(mc.getArgument(firstArgIndex), any(ZeroBound z), delta, true, _) and delta <= limit ) } @@ -104,10 +104,10 @@ private predicate twoArgLimit(MethodCall mc, int limit, boolean isKotlin) { or isKotlin = false and firstArgIndex = 0 and secondArgIndex = 1 | - // mc.getArgument(firstArgIndex).getUnderlyingExpr().(CompileTimeConstantExpr).getIntValue() = 0 and - bounded(mc.getArgument(firstArgIndex).getUnderlyingExpr(), any(ZeroBound z), 0, true, _) and - bounded(mc.getArgument(firstArgIndex).getUnderlyingExpr(), any(ZeroBound z), 0, false, _) and - bounded(mc.getArgument(secondArgIndex).getUnderlyingExpr(), any(ZeroBound z), delta, true, _) and + // mc.getArgument(firstArgIndex).(CompileTimeConstantExpr).getIntValue() = 0 and + bounded(mc.getArgument(firstArgIndex), any(ZeroBound z), 0, true, _) and + bounded(mc.getArgument(firstArgIndex), any(ZeroBound z), 0, false, _) and + bounded(mc.getArgument(secondArgIndex), any(ZeroBound z), delta, true, _) and delta <= limit ) } From 1a59839f3c8ad37a4a0d92145c8da40daf210ac2 Mon Sep 17 00:00:00 2001 From: Owen Mansel-Chan <62447351+owen-mc@users.noreply.github.com> Date: Mon, 24 Nov 2025 14:10:54 +0000 Subject: [PATCH 12/12] Range library recognises long literals now --- java/ql/test/query-tests/security/CWE-532/Test.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/java/ql/test/query-tests/security/CWE-532/Test.java b/java/ql/test/query-tests/security/CWE-532/Test.java index 0383f521ff37..6521f7e2df79 100644 --- a/java/ql/test/query-tests/security/CWE-532/Test.java +++ b/java/ql/test/query-tests/security/CWE-532/Test.java @@ -6,7 +6,7 @@ void test(String password, String authToken, String username, String nullToken, int zero = 0; int four = 4; short zeroS = 0; - long fourL = 4; + long fourL = 4L; logger.info("User's password is: " + password); // $ Alert logger.error("Auth failed for: " + authToken); // $ Alert