From f0ca76102cc6ea8ba10715caafe37f89eab93cc3 Mon Sep 17 00:00:00 2001
From: Moshe Kaplan
Date: Fri, 24 Mar 2023 15:26:46 -0400
Subject: [PATCH 001/984] Correct menu title name of "Open Workspace"
Correct menu title name of "Open Workspace" to "Open Workspace from File", as that is the current menu title on VS Code (at least as of 1.76.2)
---
.../setting-up-codeql-in-visual-studio-code.rst | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/codeql/codeql-for-visual-studio-code/setting-up-codeql-in-visual-studio-code.rst b/docs/codeql/codeql-for-visual-studio-code/setting-up-codeql-in-visual-studio-code.rst
index 6ed374381d8..98990c7f706 100644
--- a/docs/codeql/codeql-for-visual-studio-code/setting-up-codeql-in-visual-studio-code.rst
+++ b/docs/codeql/codeql-for-visual-studio-code/setting-up-codeql-in-visual-studio-code.rst
@@ -72,7 +72,7 @@ To use the starter workspace:
* Make sure you include the submodules, either by using ``git clone --recursive``, or using by ``git submodule update --init --remote`` after cloning.
* Use ``git submodule update --remote`` regularly to keep the submodules up to date.
-#. In VS Code, use the **File** > **Open Workspace** option to open the ``vscode-codeql-starter.code-workspace`` file from your checkout of the workspace repository.
+#. In VS Code, use the **File** > **Open Workspace from File** option to open the ``vscode-codeql-starter.code-workspace`` file from your checkout of the workspace repository.
.. _existing-workspace:
From 53dbfcb3aa6dc75871474e1f9538e2f582ea7a43 Mon Sep 17 00:00:00 2001
From: Asger F
Date: Tue, 26 Sep 2023 10:29:19 +0200
Subject: [PATCH 002/984] C++: use in/out barriers with flow state
---
cpp/ql/src/Security/CWE/CWE-078/ExecTainted.ql | 4 ++--
.../Security/CWE/CWE-193/ConstantSizeArrayOffByOne.ql | 4 ++--
2 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/cpp/ql/src/Security/CWE/CWE-078/ExecTainted.ql b/cpp/ql/src/Security/CWE/CWE-078/ExecTainted.ql
index 0686c4a707c..f56064a1220 100644
--- a/cpp/ql/src/Security/CWE/CWE-078/ExecTainted.ql
+++ b/cpp/ql/src/Security/CWE/CWE-078/ExecTainted.ql
@@ -134,8 +134,8 @@ module ExecTaintConfig implements DataFlow::StateConfigSig {
predicate isBarrier(DataFlow::Node node) { isBarrierImpl(node) }
- predicate isBarrierOut(DataFlow::Node node) {
- isSink(node, _) // Prevent duplicates along a call chain, since `shellCommand` will include wrappers
+ predicate isBarrierOut(DataFlow::Node node, FlowState state) {
+ isSink(node, state) // Prevent duplicates along a call chain, since `shellCommand` will include wrappers
}
}
diff --git a/cpp/ql/src/experimental/Security/CWE/CWE-193/ConstantSizeArrayOffByOne.ql b/cpp/ql/src/experimental/Security/CWE/CWE-193/ConstantSizeArrayOffByOne.ql
index c38a012b27b..018974419a5 100644
--- a/cpp/ql/src/experimental/Security/CWE/CWE-193/ConstantSizeArrayOffByOne.ql
+++ b/cpp/ql/src/experimental/Security/CWE/CWE-193/ConstantSizeArrayOffByOne.ql
@@ -168,9 +168,9 @@ module ArrayAddressToDerefConfig implements DataFlow::StateConfigSig {
)
}
- predicate isBarrierIn(DataFlow::Node node) { isSource(node, _) }
+ predicate isBarrierIn(DataFlow::Node node, FlowState state) { isSource(node, state) }
- predicate isBarrierOut(DataFlow::Node node) { isSink(node, _) }
+ predicate isBarrierOut(DataFlow::Node node, FlowState state) { isSink(node, state) }
predicate isAdditionalFlowStep(
DataFlow::Node node1, FlowState state1, DataFlow::Node node2, FlowState state2
From d10a6b4ca0df39a5b5c5800745e598292fe0e681 Mon Sep 17 00:00:00 2001
From: Kristen Newbury
Date: Thu, 3 Oct 2024 13:33:30 -0400
Subject: [PATCH 003/984] Remove unnecessary query
---
.../ql/src/Language Abuse/EmptyStatement.java | 8 ----
.../src/Language Abuse/EmptyStatement.qhelp | 39 -------------------
java/ql/src/Language Abuse/EmptyStatement.ql | 19 ---------
3 files changed, 66 deletions(-)
delete mode 100644 java/ql/src/Language Abuse/EmptyStatement.java
delete mode 100644 java/ql/src/Language Abuse/EmptyStatement.qhelp
delete mode 100644 java/ql/src/Language Abuse/EmptyStatement.ql
diff --git a/java/ql/src/Language Abuse/EmptyStatement.java b/java/ql/src/Language Abuse/EmptyStatement.java
deleted file mode 100644
index 4f9b462a38f..00000000000
--- a/java/ql/src/Language Abuse/EmptyStatement.java
+++ /dev/null
@@ -1,8 +0,0 @@
-public class Cart {
- // AVOID: Empty statement
- List items = new ArrayList();;
- public void applyDiscount(float discount) {
- // AVOID: Empty statement as loop body
- for (int i = 0; i < items.size(); items.get(i++).applyDiscount(discount));
- }
-}
\ No newline at end of file
diff --git a/java/ql/src/Language Abuse/EmptyStatement.qhelp b/java/ql/src/Language Abuse/EmptyStatement.qhelp
deleted file mode 100644
index 700bd488dfc..00000000000
--- a/java/ql/src/Language Abuse/EmptyStatement.qhelp
+++ /dev/null
@@ -1,39 +0,0 @@
-
-
-
-
-
-
An empty statement is a single semicolon ; that does not
-terminate another statement. Such a statement hinders readability and has no effect on its own.
-
-
-
-
-
Avoid empty statements. If a loop is intended to have an empty body, it is better
-to mark that fact explicitly by using a pair of braces {} containing an explanatory comment
-for the body, rather than a single semicolon.
-
-
-
-
-
In the following example, there is an empty statement on line 3, where an additional semicolon is
-used. On line 6, the for statement has an empty body because the condition is
-immediately followed by a semicolon. In this case, it is better to include a pair of braces {} containing
-an explanatory comment for the body instead.
-
-
-
-
-
-
diff --git a/java/ql/src/Language Abuse/EmptyStatement.ql b/java/ql/src/Language Abuse/EmptyStatement.ql
deleted file mode 100644
index 36f61b862ac..00000000000
--- a/java/ql/src/Language Abuse/EmptyStatement.ql
+++ /dev/null
@@ -1,19 +0,0 @@
-/**
- * @name Empty statement
- * @description An empty statement hinders readability.
- * @kind problem
- * @problem.severity recommendation
- * @precision low
- * @id java/empty-statement
- * @tags maintainability
- * useless-code
- */
-
-import java
-
-from EmptyStmt empty, string action
-where
- if exists(LoopStmt l | l.getBody() = empty)
- then action = "turned into '{}'"
- else action = "deleted"
-select empty, "This empty statement should be " + action + "."
From e98db7fd20f7c64cb68ed315523bc8bc0594aac8 Mon Sep 17 00:00:00 2001
From: Kristen Newbury
Date: Thu, 3 Oct 2024 17:28:59 -0400
Subject: [PATCH 004/984] Add changenote for query removal change
---
java/ql/src/change-notes/2024-10-03-remove-java-query.md | 4 ++++
1 file changed, 4 insertions(+)
create mode 100644 java/ql/src/change-notes/2024-10-03-remove-java-query.md
diff --git a/java/ql/src/change-notes/2024-10-03-remove-java-query.md b/java/ql/src/change-notes/2024-10-03-remove-java-query.md
new file mode 100644
index 00000000000..efa7fa3504b
--- /dev/null
+++ b/java/ql/src/change-notes/2024-10-03-remove-java-query.md
@@ -0,0 +1,4 @@
+---
+category: removedQuery
+---
+* Removed the `java/empty-statement` query that was subsumed by the `java/empty-block` query.
\ No newline at end of file
From df18891a2f7840a3d3a12caf41ff4dc43a049d11 Mon Sep 17 00:00:00 2001
From: Kristen Newbury
Date: Thu, 3 Oct 2024 17:36:42 -0400
Subject: [PATCH 005/984] Fix changenote for query removal change
---
java/ql/src/change-notes/2024-10-03-remove-java-query.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/java/ql/src/change-notes/2024-10-03-remove-java-query.md b/java/ql/src/change-notes/2024-10-03-remove-java-query.md
index efa7fa3504b..e9f3e4910cb 100644
--- a/java/ql/src/change-notes/2024-10-03-remove-java-query.md
+++ b/java/ql/src/change-notes/2024-10-03-remove-java-query.md
@@ -1,4 +1,4 @@
---
-category: removedQuery
+category: minorAnalysis
---
* Removed the `java/empty-statement` query that was subsumed by the `java/empty-block` query.
\ No newline at end of file
From 150debdd8dc5d0ff368ae6d25578b63c8c617179 Mon Sep 17 00:00:00 2001
From: 2h0ng <60600792+superboy-zjc@users.noreply.github.com>
Date: Sun, 9 Feb 2025 15:29:37 -0500
Subject: [PATCH 006/984] Fix the broken reference
---
docs/codeql/ql-language-reference/aliases.rst | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/codeql/ql-language-reference/aliases.rst b/docs/codeql/ql-language-reference/aliases.rst
index efbbef65cda..ebecbdf0a2b 100644
--- a/docs/codeql/ql-language-reference/aliases.rst
+++ b/docs/codeql/ql-language-reference/aliases.rst
@@ -137,6 +137,6 @@ During :ref:`name resolution `, ambiguity between aliases from
for the same module/type/predicate is allowed, but ambiguity between between aliases from distinct **strong**
alias definitions is invalid QL.
Likewise, for the purpose of applicative instantiation of :ref:`parameterised modules `
-and `:ref:`parameterised module signatures `, aliases from **weak** alias
+and :ref:`parameterised module signatures `, aliases from **weak** alias
definitions for instantiation arguments do not result in separate instantiations, but aliases from **strong**
alias definitions for instantiation arguments do.
From 26f3b40d3591ec07806bdf1438df6e95030bd2c3 Mon Sep 17 00:00:00 2001
From: Vasco-jofra <11303847+Vasco-jofra@users.noreply.github.com>
Date: Sat, 14 Jun 2025 00:13:03 +0200
Subject: [PATCH 007/984] Add lodash GroupBy as taint step
---
.../javascript/frameworks/LodashUnderscore.qll | 12 ++++++++++++
1 file changed, 12 insertions(+)
diff --git a/javascript/ql/lib/semmle/javascript/frameworks/LodashUnderscore.qll b/javascript/ql/lib/semmle/javascript/frameworks/LodashUnderscore.qll
index 7c2e6aa37a5..20258622737 100644
--- a/javascript/ql/lib/semmle/javascript/frameworks/LodashUnderscore.qll
+++ b/javascript/ql/lib/semmle/javascript/frameworks/LodashUnderscore.qll
@@ -341,6 +341,18 @@ module LodashUnderscore {
preservesValue = true
}
}
+
+ private class LodashGroupBy extends DataFlow::SummarizedCallable {
+ LodashGroupBy() { this = "_.groupBy" }
+
+ override DataFlow::CallNode getACall() { result = member("groupBy").getACall() }
+
+ override predicate propagatesFlow(string input, string output, boolean preservesValue) {
+ input = "Argument[0]" and
+ output = ["Argument[1].Parameter[0]", "ReturnValue"] and
+ preservesValue = false
+ }
+ }
}
/**
From 8c4dbca23c895b28fab9f256ce0c2b042b7174f6 Mon Sep 17 00:00:00 2001
From: Vasco-jofra <11303847+Vasco-jofra@users.noreply.github.com>
Date: Sun, 15 Jun 2025 17:59:49 +0200
Subject: [PATCH 008/984] Improve data flow in the async library
---
.../dataflow/internal/FlowSummaryPrivate.qll | 2 +
.../javascript/frameworks/AsyncPackage.qll | 147 +++++++++++++-----
.../AsyncPackage/AsyncTaintTracking.expected | 32 ++--
.../frameworks/AsyncPackage/map.js | 13 ++
.../frameworks/AsyncPackage/waterfall.js | 10 +-
5 files changed, 158 insertions(+), 46 deletions(-)
diff --git a/javascript/ql/lib/semmle/javascript/dataflow/internal/FlowSummaryPrivate.qll b/javascript/ql/lib/semmle/javascript/dataflow/internal/FlowSummaryPrivate.qll
index 31f5f16bbfb..6315b34b0a4 100644
--- a/javascript/ql/lib/semmle/javascript/dataflow/internal/FlowSummaryPrivate.qll
+++ b/javascript/ql/lib/semmle/javascript/dataflow/internal/FlowSummaryPrivate.qll
@@ -94,6 +94,8 @@ private string encodeContentAux(ContentSet cs, string arg) {
cs = ContentSet::iteratorElement() and result = "IteratorElement"
or
cs = ContentSet::iteratorError() and result = "IteratorError"
+ or
+ cs = ContentSet::anyProperty() and result = "AnyMember"
)
or
cs = getPromiseContent(arg) and
diff --git a/javascript/ql/lib/semmle/javascript/frameworks/AsyncPackage.qll b/javascript/ql/lib/semmle/javascript/frameworks/AsyncPackage.qll
index 4dc60d44765..eacc69585ed 100644
--- a/javascript/ql/lib/semmle/javascript/frameworks/AsyncPackage.qll
+++ b/javascript/ql/lib/semmle/javascript/frameworks/AsyncPackage.qll
@@ -15,14 +15,15 @@ module AsyncPackage {
}
/**
- * Gets a reference to the given member or one of its `Limit` or `Series` variants.
+ * Gets `Limit` or `Series` name variants for a given member name.
*
- * For example, `memberVariant("map")` finds references to `map`, `mapLimit`, and `mapSeries`.
+ * For example, `memberNameVariant("map")` returns `map`, `mapLimit`, and `mapSeries`.
*/
- DataFlow::SourceNode memberVariant(string name) {
- result = member(name) or
- result = member(name + "Limit") or
- result = member(name + "Series")
+ bindingset[name]
+ string memberNameVariant(string name) {
+ result = name or
+ result = name + "Limit" or
+ result = name + "Series"
}
/**
@@ -101,22 +102,47 @@ module AsyncPackage {
*/
class IterationCall extends DataFlow::InvokeNode {
string name;
+ int iteratorCallbackIndex;
+ int finalCallbackIndex;
IterationCall() {
- this = memberVariant(name).getACall() and
- name =
- [
- "concat", "detect", "each", "eachOf", "forEach", "forEachOf", "every", "filter",
- "groupBy", "map", "mapValues", "reduce", "reduceRight", "reject", "some", "sortBy",
- "transform"
- ]
+ (
+ (
+ name =
+ memberNameVariant([
+ "concat", "detect", "each", "eachOf", "forEach", "forEachOf", "every", "filter",
+ "groupBy", "map", "mapValues", "reject", "some", "sortBy",
+ ]) and
+ if name.matches("%Limit")
+ then (
+ iteratorCallbackIndex = 2 and finalCallbackIndex = 3
+ ) else (
+ iteratorCallbackIndex = 1 and finalCallbackIndex = 2
+ )
+ )
+ or
+ name = ["reduce", "reduceRight", "transform"] and
+ iteratorCallbackIndex = 2 and
+ finalCallbackIndex = 3
+ ) and
+ this = member(name).getACall()
}
/**
- * Gets the name of the iteration call, without the `Limit` or `Series` suffix.
+ * Gets the name of the iteration call
*/
string getName() { result = name }
+ /**
+ * Gets the iterator callback index
+ */
+ int getIteratorCallbackIndex() { result = iteratorCallbackIndex }
+
+ /**
+ * Gets the final callback index
+ */
+ int getFinalCallbackIndex() { result = finalCallbackIndex }
+
/**
* Gets the node holding the collection being iterated over.
*/
@@ -125,26 +151,73 @@ module AsyncPackage {
/**
* Gets the node holding the function being called for each element in the collection.
*/
- DataFlow::Node getIteratorCallback() { result = this.getArgument(this.getNumArgument() - 2) }
+ DataFlow::FunctionNode getIteratorCallback() {
+ result = this.getCallback(iteratorCallbackIndex)
+ }
/**
- * Gets the node holding the function being invoked after iteration is complete.
+ * Gets the node holding the function being invoked after iteration is complete. (may not exist)
*/
- DataFlow::Node getFinalCallback() { result = this.getArgument(this.getNumArgument() - 1) }
+ DataFlow::FunctionNode getFinalCallback() { result = this.getCallback(finalCallbackIndex) }
}
/**
- * A taint step from the collection into the iterator callback of an iteration call.
+ * An IterationCall with its iterator callback at index 1
+ */
+ private class IterationCallCallbacksFirstArg extends IterationCall {
+ IterationCallCallbacksFirstArg() { this.getIteratorCallbackIndex() = 1 }
+ }
+
+ /**
+ * An IterationCall with its iterator callback at index 2
+ */
+ private class IterationCallCallbacksSecondArg extends IterationCall {
+ IterationCallCallbacksSecondArg() { this.getIteratorCallbackIndex() = 2 }
+ }
+
+ /**
+ * The model with the iteratorCallbackIndex abstracted
+ */
+ bindingset[iteratorCallbackIndex]
+ private predicate iterationCallPropagatesFlow(
+ string input, string output, boolean preservesValue, int iteratorCallbackIndex
+ ) {
+ preservesValue = true and
+ input = "Argument[0]." + ["ArrayElement", "SetElement", "IteratorElement", "AnyMember"] and
+ output = "Argument[" + iteratorCallbackIndex + "].Parameter[0]"
+ }
+
+ /**
+ * A taint step from the collection into the iterator callback (at index 1) of an iteration call.
*
* For example: `data -> item` in `async.each(data, (item, cb) => {})`.
*/
- private class IterationInputTaintStep extends TaintTracking::SharedTaintStep {
- override predicate step(DataFlow::Node pred, DataFlow::Node succ) {
- exists(DataFlow::FunctionNode iteratee, IterationCall call |
- iteratee = call.getIteratorCallback() and // Require a closure to avoid spurious call/return mismatch.
- pred = call.getCollection() and // TODO: needs a flow summary to ensure ArrayElement content is unfolded
- succ = iteratee.getParameter(0)
- )
+ class IterationCallCallbacksFirstArgFlowSummary extends DataFlow::SummarizedCallable {
+ IterationCallCallbacksFirstArgFlowSummary() { this = "async.[IterationCallCallbacksFirstArg]" }
+
+ override DataFlow::InvokeNode getACallSimple() {
+ result instanceof IterationCallCallbacksFirstArg
+ }
+
+ override predicate propagatesFlow(string input, string output, boolean preservesValue) {
+ iterationCallPropagatesFlow(input, output, preservesValue, 1)
+ }
+ }
+
+ /**
+ * A taint step from the collection into the iterator callback (at index 2) of an iteration call.
+ *
+ * For example: `data -> item` in `async.eachLimit(data, 1, (item, cb) => {})`.
+ */
+ class IterationCallCallbacksSecondArgFlowSummary extends DataFlow::SummarizedCallable {
+ IterationCallCallbacksSecondArgFlowSummary() { this = "async.[IterationCallCallbackSecondArg]" }
+
+ override DataFlow::InvokeNode getACallSimple() {
+ result instanceof IterationCallCallbacksSecondArg
+ }
+
+ override predicate propagatesFlow(string input, string output, boolean preservesValue) {
+ iterationCallPropagatesFlow(input, output, preservesValue, 2)
}
}
@@ -152,14 +225,14 @@ module AsyncPackage {
* A taint step from the return value of an iterator callback to the result of the iteration
* call.
*
- * For example: `item + taint()` -> result` in `async.map(data, (item, cb) => cb(null, item + taint()), (err, result) => {})`.
+ * For example: `item + taint() -> result` in `async.map(data, (item, cb) => cb(null, item + taint()), (err, result) => {})`.
*/
private class IterationOutputTaintStep extends TaintTracking::SharedTaintStep {
override predicate step(DataFlow::Node pred, DataFlow::Node succ) {
exists(
DataFlow::FunctionNode iteratee, DataFlow::FunctionNode final, int i, IterationCall call
|
- iteratee = call.getIteratorCallback().getALocalSource() and
+ iteratee = call.getIteratorCallback() and
final = call.getFinalCallback() and // Require a closure to avoid spurious call/return mismatch.
pred = getLastParameter(iteratee).getACall().getArgument(i) and
succ = final.getParameter(i) and
@@ -175,14 +248,18 @@ module AsyncPackage {
*
* For example: `data -> result` in `async.sortBy(data, orderingFn, (err, result) => {})`.
*/
- private class IterationPreserveTaintStep extends TaintTracking::SharedTaintStep {
- override predicate step(DataFlow::Node pred, DataFlow::Node succ) {
- exists(DataFlow::FunctionNode final, IterationCall call |
- final = call.getFinalCallback() and // Require a closure to avoid spurious call/return mismatch.
- pred = call.getCollection() and
- succ = final.getParameter(1) and
- call.getName() = "sortBy"
- )
+ class IterationPreserveTaintStepFlowSummary extends DataFlow::SummarizedCallable {
+ IterationPreserveTaintStepFlowSummary() { this = "async.sortBy" }
+
+ override DataFlow::InvokeNode getACallSimple() {
+ result instanceof IterationCall and
+ result.(IterationCall).getName() = "sortBy"
+ }
+
+ override predicate propagatesFlow(string input, string output, boolean preservesValue) {
+ preservesValue = false and
+ input = "Argument[0]." + ["ArrayElement", "SetElement", "IteratorElement", "AnyMember"] and
+ output = "Argument[2].Parameter[1]"
}
}
}
diff --git a/javascript/ql/test/library-tests/frameworks/AsyncPackage/AsyncTaintTracking.expected b/javascript/ql/test/library-tests/frameworks/AsyncPackage/AsyncTaintTracking.expected
index 168f5ec5ace..95ee8fe452b 100644
--- a/javascript/ql/test/library-tests/frameworks/AsyncPackage/AsyncTaintTracking.expected
+++ b/javascript/ql/test/library-tests/frameworks/AsyncPackage/AsyncTaintTracking.expected
@@ -1,12 +1,24 @@
legacyDataFlowDifference
-| each.js:11:9:11:16 | source() | each.js:13:12:13:15 | item | only flow with OLD data flow library |
-| map.js:10:13:10:20 | source() | map.js:12:14:12:17 | item | only flow with OLD data flow library |
-| map.js:26:13:26:20 | source() | map.js:28:27:28:32 | result | only flow with OLD data flow library |
-| sortBy.js:10:22:10:29 | source() | sortBy.js:12:27:12:32 | result | only flow with OLD data flow library |
+| each.js:11:9:11:16 | source() | each.js:13:12:13:15 | item | only flow with NEW data flow library |
+| map.js:14:13:14:20 | source() | map.js:16:14:16:17 | item | only flow with NEW data flow library |
+| map.js:30:13:30:20 | source() | map.js:32:27:32:32 | result | only flow with NEW data flow library |
+| map.js:40:13:40:20 | source() | map.js:11:10:11:10 | x | only flow with NEW data flow library |
+| map.js:42:12:42:19 | source() | map.js:11:10:11:10 | x | only flow with NEW data flow library |
+| map.js:44:16:44:23 | source() | map.js:11:10:11:10 | x | only flow with NEW data flow library |
+| map.js:46:18:46:25 | source() | map.js:11:10:11:10 | x | only flow with NEW data flow library |
+| sortBy.js:10:22:10:29 | source() | sortBy.js:12:27:12:32 | result | only flow with NEW data flow library |
#select
-| map.js:20:19:20:26 | source() | map.js:23:27:23:32 | result |
-| waterfall.js:8:30:8:37 | source() | waterfall.js:11:12:11:16 | taint |
-| waterfall.js:8:30:8:37 | source() | waterfall.js:20:10:20:14 | taint |
-| waterfall.js:28:18:28:25 | source() | waterfall.js:39:10:39:12 | err |
-| waterfall.js:46:22:46:29 | source() | waterfall.js:49:12:49:16 | taint |
-| waterfall.js:46:22:46:29 | source() | waterfall.js:55:10:55:14 | taint |
+| each.js:11:9:11:16 | source() | each.js:13:12:13:15 | item |
+| map.js:14:13:14:20 | source() | map.js:16:14:16:17 | item |
+| map.js:24:19:24:26 | source() | map.js:27:27:27:32 | result |
+| map.js:30:13:30:20 | source() | map.js:32:27:32:32 | result |
+| map.js:40:13:40:20 | source() | map.js:11:10:11:10 | x |
+| map.js:42:12:42:19 | source() | map.js:11:10:11:10 | x |
+| map.js:44:16:44:23 | source() | map.js:11:10:11:10 | x |
+| map.js:46:18:46:25 | source() | map.js:11:10:11:10 | x |
+| sortBy.js:10:22:10:29 | source() | sortBy.js:12:27:12:32 | result |
+| waterfall.js:16:30:16:37 | source() | waterfall.js:19:12:19:16 | taint |
+| waterfall.js:16:30:16:37 | source() | waterfall.js:28:10:28:14 | taint |
+| waterfall.js:36:18:36:25 | source() | waterfall.js:47:10:47:12 | err |
+| waterfall.js:54:22:54:29 | source() | waterfall.js:57:12:57:16 | taint |
+| waterfall.js:54:22:54:29 | source() | waterfall.js:63:10:63:14 | taint |
diff --git a/javascript/ql/test/library-tests/frameworks/AsyncPackage/map.js b/javascript/ql/test/library-tests/frameworks/AsyncPackage/map.js
index ed7e64b01fa..b1e9ecc883b 100644
--- a/javascript/ql/test/library-tests/frameworks/AsyncPackage/map.js
+++ b/javascript/ql/test/library-tests/frameworks/AsyncPackage/map.js
@@ -7,6 +7,10 @@ function sink(x) {
console.log(x)
}
+function call_sink(x) {
+ sink(x)
+}
+
async_.map([source()],
(item, cb) => {
sink(item), // NOT OK
@@ -32,3 +36,12 @@ async_.map(['safe'],
(item, cb) => cb(null, item),
(err, result) => sink(result) // OK
);
+
+async_.map([source()], call_sink) // NOT OK
+
+async_.map(source().prop, call_sink) // NOT OK
+
+async_.map({a: source()}, call_sink) // NOT OK
+
+async_.mapLimit([source()], 1, call_sink) // NOT OK
+
diff --git a/javascript/ql/test/library-tests/frameworks/AsyncPackage/waterfall.js b/javascript/ql/test/library-tests/frameworks/AsyncPackage/waterfall.js
index 439ac48674a..8554d048d98 100644
--- a/javascript/ql/test/library-tests/frameworks/AsyncPackage/waterfall.js
+++ b/javascript/ql/test/library-tests/frameworks/AsyncPackage/waterfall.js
@@ -1,7 +1,15 @@
let async_ = require('async');
let waterfall = require('a-sync-waterfall');
-var source, sink, somethingWrong;
+function source() {
+ return 'TAINT'
+}
+
+function sink(x) {
+ console.log(x)
+}
+
+var somethingWrong;
async_.waterfall([
function(callback) {
From 575da5c31c8909a99a64dc20571bb7c23aa22635 Mon Sep 17 00:00:00 2001
From: Vasco-jofra <11303847+Vasco-jofra@users.noreply.github.com>
Date: Thu, 26 Jun 2025 10:10:52 +0200
Subject: [PATCH 009/984] Merge SummarizedCallable into single class
---
.../javascript/frameworks/AsyncPackage.qll | 77 ++++++-------------
1 file changed, 24 insertions(+), 53 deletions(-)
diff --git a/javascript/ql/lib/semmle/javascript/frameworks/AsyncPackage.qll b/javascript/ql/lib/semmle/javascript/frameworks/AsyncPackage.qll
index eacc69585ed..db2487ce46a 100644
--- a/javascript/ql/lib/semmle/javascript/frameworks/AsyncPackage.qll
+++ b/javascript/ql/lib/semmle/javascript/frameworks/AsyncPackage.qll
@@ -14,13 +14,24 @@ module AsyncPackage {
result = DataFlow::moduleMember("async-es", name)
}
+ /**
+ * Gets a reference to the given member or one of its `Limit` or `Series` variants.
+ *
+ * For example, `memberVariant("map")` finds references to `map`, `mapLimit`, and `mapSeries`.
+ */
+ DataFlow::SourceNode memberVariant(string name) {
+ result = member(name) or
+ result = member(name + "Limit") or
+ result = member(name + "Series")
+ }
+
/**
* Gets `Limit` or `Series` name variants for a given member name.
*
* For example, `memberNameVariant("map")` returns `map`, `mapLimit`, and `mapSeries`.
*/
bindingset[name]
- string memberNameVariant(string name) {
+ private string memberNameVariant(string name) {
result = name or
result = name + "Limit" or
result = name + "Series"
@@ -161,63 +172,23 @@ module AsyncPackage {
DataFlow::FunctionNode getFinalCallback() { result = this.getCallback(finalCallbackIndex) }
}
- /**
- * An IterationCall with its iterator callback at index 1
- */
- private class IterationCallCallbacksFirstArg extends IterationCall {
- IterationCallCallbacksFirstArg() { this.getIteratorCallbackIndex() = 1 }
- }
+ private class IterationCallFlowSummary extends DataFlow::SummarizedCallable {
+ private int callbackArgIndex;
- /**
- * An IterationCall with its iterator callback at index 2
- */
- private class IterationCallCallbacksSecondArg extends IterationCall {
- IterationCallCallbacksSecondArg() { this.getIteratorCallbackIndex() = 2 }
- }
-
- /**
- * The model with the iteratorCallbackIndex abstracted
- */
- bindingset[iteratorCallbackIndex]
- private predicate iterationCallPropagatesFlow(
- string input, string output, boolean preservesValue, int iteratorCallbackIndex
- ) {
- preservesValue = true and
- input = "Argument[0]." + ["ArrayElement", "SetElement", "IteratorElement", "AnyMember"] and
- output = "Argument[" + iteratorCallbackIndex + "].Parameter[0]"
- }
-
- /**
- * A taint step from the collection into the iterator callback (at index 1) of an iteration call.
- *
- * For example: `data -> item` in `async.each(data, (item, cb) => {})`.
- */
- class IterationCallCallbacksFirstArgFlowSummary extends DataFlow::SummarizedCallable {
- IterationCallCallbacksFirstArgFlowSummary() { this = "async.[IterationCallCallbacksFirstArg]" }
+ IterationCallFlowSummary() {
+ this = "async.IteratorCall(callbackArgIndex=" + callbackArgIndex + ")" and
+ callbackArgIndex in [1 .. 3]
+ }
override DataFlow::InvokeNode getACallSimple() {
- result instanceof IterationCallCallbacksFirstArg
+ result instanceof IterationCall and
+ result.(IterationCall).getIteratorCallbackIndex() = callbackArgIndex
}
override predicate propagatesFlow(string input, string output, boolean preservesValue) {
- iterationCallPropagatesFlow(input, output, preservesValue, 1)
- }
- }
-
- /**
- * A taint step from the collection into the iterator callback (at index 2) of an iteration call.
- *
- * For example: `data -> item` in `async.eachLimit(data, 1, (item, cb) => {})`.
- */
- class IterationCallCallbacksSecondArgFlowSummary extends DataFlow::SummarizedCallable {
- IterationCallCallbacksSecondArgFlowSummary() { this = "async.[IterationCallCallbackSecondArg]" }
-
- override DataFlow::InvokeNode getACallSimple() {
- result instanceof IterationCallCallbacksSecondArg
- }
-
- override predicate propagatesFlow(string input, string output, boolean preservesValue) {
- iterationCallPropagatesFlow(input, output, preservesValue, 2)
+ preservesValue = true and
+ input = "Argument[0]." + ["ArrayElement", "SetElement", "IteratorElement", "AnyMember"] and
+ output = "Argument[" + callbackArgIndex + "].Parameter[0]"
}
}
@@ -248,7 +219,7 @@ module AsyncPackage {
*
* For example: `data -> result` in `async.sortBy(data, orderingFn, (err, result) => {})`.
*/
- class IterationPreserveTaintStepFlowSummary extends DataFlow::SummarizedCallable {
+ private class IterationPreserveTaintStepFlowSummary extends DataFlow::SummarizedCallable {
IterationPreserveTaintStepFlowSummary() { this = "async.sortBy" }
override DataFlow::InvokeNode getACallSimple() {
From fccdc30ac544ca6af17c132b69814543102d969f Mon Sep 17 00:00:00 2001
From: Joe Farebrother
Date: Tue, 8 Jul 2025 11:10:38 +0100
Subject: [PATCH 010/984] Modernize incomplete ordering query
---
python/ql/src/Classes/IncompleteOrdering.ql | 88 +++++++++------------
1 file changed, 36 insertions(+), 52 deletions(-)
diff --git a/python/ql/src/Classes/IncompleteOrdering.ql b/python/ql/src/Classes/IncompleteOrdering.ql
index d6cd1230ece..bbb6ca5cf6d 100644
--- a/python/ql/src/Classes/IncompleteOrdering.ql
+++ b/python/ql/src/Classes/IncompleteOrdering.ql
@@ -2,7 +2,8 @@
* @name Incomplete ordering
* @description Class defines one or more ordering method but does not define all 4 ordering comparison methods
* @kind problem
- * @tags reliability
+ * @tags quality
+ * reliability
* correctness
* @problem.severity warning
* @sub-severity low
@@ -11,63 +12,46 @@
*/
import python
+import semmle.python.dataflow.new.internal.DataFlowDispatch
+import semmle.python.ApiGraphs
-predicate total_ordering(Class cls) {
- exists(Attribute a | a = cls.getADecorator() | a.getName() = "total_ordering")
+predicate totalOrdering(Class cls) {
+ cls.getADecorator() =
+ API::moduleImport("functools").getMember("total_ordering").asSource().asExpr()
+}
+
+Function getMethod(Class cls, string name) {
+ result = cls.getAMethod() and
+ result.getName() = name
+}
+
+predicate definesStrictOrdering(Class cls, Function meth) {
+ meth = getMethod(cls, "__lt__")
or
- exists(Name n | n = cls.getADecorator() | n.getId() = "total_ordering")
+ not exists(getMethod(cls, "__lt__")) and
+ meth = getMethod(cls, "__gt__")
}
-string ordering_name(int n) {
- result = "__lt__" and n = 1
+predicate definesNonStrictOrdering(Class cls, Function meth) {
+ meth = getMethod(cls, "__le__")
or
- result = "__le__" and n = 2
+ not exists(getMethod(cls, "__le__")) and
+ meth = getMethod(cls, "__ge__")
+}
+
+predicate missingComparison(Class cls, Function defined, string missing) {
+ definesStrictOrdering(cls, defined) and
+ not definesNonStrictOrdering(getADirectSuperclass*(cls), _) and
+ missing = "__le__ or __ge__"
or
- result = "__gt__" and n = 3
- or
- result = "__ge__" and n = 4
+ definesNonStrictOrdering(cls, defined) and
+ not definesStrictOrdering(getADirectSuperclass*(cls), _) and
+ missing = "__lt__ or __gt__"
}
-predicate overrides_ordering_method(ClassValue c, string name) {
- name = ordering_name(_) and
- (
- c.declaresAttribute(name)
- or
- exists(ClassValue sup | sup = c.getASuperType() and not sup = Value::named("object") |
- sup.declaresAttribute(name)
- )
- )
-}
-
-string unimplemented_ordering(ClassValue c, int n) {
- not c = Value::named("object") and
- not overrides_ordering_method(c, result) and
- result = ordering_name(n)
-}
-
-string unimplemented_ordering_methods(ClassValue c, int n) {
- n = 0 and result = "" and exists(unimplemented_ordering(c, _))
- or
- exists(string prefix, int nm1 | n = nm1 + 1 and prefix = unimplemented_ordering_methods(c, nm1) |
- prefix = "" and result = unimplemented_ordering(c, n)
- or
- result = prefix and not exists(unimplemented_ordering(c, n)) and n < 5
- or
- prefix != "" and result = prefix + " or " + unimplemented_ordering(c, n)
- )
-}
-
-Value ordering_method(ClassValue c, string name) {
- /* If class doesn't declare a method then don't blame this class (the superclass will be blamed). */
- name = ordering_name(_) and result = c.declaredAttribute(name)
-}
-
-from ClassValue c, Value ordering, string name
+from Class cls, Function defined, string missing
where
- not c.failedInference(_) and
- not total_ordering(c.getScope()) and
- ordering = ordering_method(c, name) and
- exists(unimplemented_ordering(c, _))
-select c,
- "Class " + c.getName() + " implements $@, but does not implement " +
- unimplemented_ordering_methods(c, 4) + ".", ordering, name
+ not totalOrdering(cls) and
+ missingComparison(cls, defined, missing)
+select cls, "This class implements $@, but does not implement an " + missing + " method.", defined,
+ defined.getName()
From e71af8fd6d2b834a1de6629a82896900c79b1c11 Mon Sep 17 00:00:00 2001
From: Joe Farebrother
Date: Tue, 8 Jul 2025 11:14:19 +0100
Subject: [PATCH 011/984] Move to subfolder
---
python/ql/src/Classes/{ => Comparisons}/IncompleteOrdering.qhelp | 0
python/ql/src/Classes/{ => Comparisons}/IncompleteOrdering.ql | 0
.../src/Classes/{ => Comparisons/examples}/IncompleteOrdering.py | 0
3 files changed, 0 insertions(+), 0 deletions(-)
rename python/ql/src/Classes/{ => Comparisons}/IncompleteOrdering.qhelp (100%)
rename python/ql/src/Classes/{ => Comparisons}/IncompleteOrdering.ql (100%)
rename python/ql/src/Classes/{ => Comparisons/examples}/IncompleteOrdering.py (100%)
diff --git a/python/ql/src/Classes/IncompleteOrdering.qhelp b/python/ql/src/Classes/Comparisons/IncompleteOrdering.qhelp
similarity index 100%
rename from python/ql/src/Classes/IncompleteOrdering.qhelp
rename to python/ql/src/Classes/Comparisons/IncompleteOrdering.qhelp
diff --git a/python/ql/src/Classes/IncompleteOrdering.ql b/python/ql/src/Classes/Comparisons/IncompleteOrdering.ql
similarity index 100%
rename from python/ql/src/Classes/IncompleteOrdering.ql
rename to python/ql/src/Classes/Comparisons/IncompleteOrdering.ql
diff --git a/python/ql/src/Classes/IncompleteOrdering.py b/python/ql/src/Classes/Comparisons/examples/IncompleteOrdering.py
similarity index 100%
rename from python/ql/src/Classes/IncompleteOrdering.py
rename to python/ql/src/Classes/Comparisons/examples/IncompleteOrdering.py
From 4c5c4e06c3b0c0d80cf930a2910fe5a668bf21c4 Mon Sep 17 00:00:00 2001
From: Joe Farebrother
Date: Tue, 8 Jul 2025 11:33:47 +0100
Subject: [PATCH 012/984] Move inconsistentEquality and equals-hash-mismatch to
subfolder
---
python/ql/src/Classes/{ => Comparisons}/EqualsOrHash.qhelp | 0
python/ql/src/Classes/{ => Comparisons}/EqualsOrHash.ql | 0
python/ql/src/Classes/{ => Comparisons}/EqualsOrNotEquals.qhelp | 0
python/ql/src/Classes/{ => Comparisons}/EqualsOrNotEquals.ql | 0
python/ql/src/Classes/{ => Comparisons/examples}/EqualsOrHash.py | 0
.../src/Classes/{ => Comparisons/examples}/EqualsOrNotEquals.py | 0
6 files changed, 0 insertions(+), 0 deletions(-)
rename python/ql/src/Classes/{ => Comparisons}/EqualsOrHash.qhelp (100%)
rename python/ql/src/Classes/{ => Comparisons}/EqualsOrHash.ql (100%)
rename python/ql/src/Classes/{ => Comparisons}/EqualsOrNotEquals.qhelp (100%)
rename python/ql/src/Classes/{ => Comparisons}/EqualsOrNotEquals.ql (100%)
rename python/ql/src/Classes/{ => Comparisons/examples}/EqualsOrHash.py (100%)
rename python/ql/src/Classes/{ => Comparisons/examples}/EqualsOrNotEquals.py (100%)
diff --git a/python/ql/src/Classes/EqualsOrHash.qhelp b/python/ql/src/Classes/Comparisons/EqualsOrHash.qhelp
similarity index 100%
rename from python/ql/src/Classes/EqualsOrHash.qhelp
rename to python/ql/src/Classes/Comparisons/EqualsOrHash.qhelp
diff --git a/python/ql/src/Classes/EqualsOrHash.ql b/python/ql/src/Classes/Comparisons/EqualsOrHash.ql
similarity index 100%
rename from python/ql/src/Classes/EqualsOrHash.ql
rename to python/ql/src/Classes/Comparisons/EqualsOrHash.ql
diff --git a/python/ql/src/Classes/EqualsOrNotEquals.qhelp b/python/ql/src/Classes/Comparisons/EqualsOrNotEquals.qhelp
similarity index 100%
rename from python/ql/src/Classes/EqualsOrNotEquals.qhelp
rename to python/ql/src/Classes/Comparisons/EqualsOrNotEquals.qhelp
diff --git a/python/ql/src/Classes/EqualsOrNotEquals.ql b/python/ql/src/Classes/Comparisons/EqualsOrNotEquals.ql
similarity index 100%
rename from python/ql/src/Classes/EqualsOrNotEquals.ql
rename to python/ql/src/Classes/Comparisons/EqualsOrNotEquals.ql
diff --git a/python/ql/src/Classes/EqualsOrHash.py b/python/ql/src/Classes/Comparisons/examples/EqualsOrHash.py
similarity index 100%
rename from python/ql/src/Classes/EqualsOrHash.py
rename to python/ql/src/Classes/Comparisons/examples/EqualsOrHash.py
diff --git a/python/ql/src/Classes/EqualsOrNotEquals.py b/python/ql/src/Classes/Comparisons/examples/EqualsOrNotEquals.py
similarity index 100%
rename from python/ql/src/Classes/EqualsOrNotEquals.py
rename to python/ql/src/Classes/Comparisons/examples/EqualsOrNotEquals.py
From eb1b5a35d790d851bbbd469915a0288f6b01ad4f Mon Sep 17 00:00:00 2001
From: Joe Farebrother
Date: Tue, 8 Jul 2025 15:33:59 +0100
Subject: [PATCH 013/984] Modernize inconsistent equality
---
python/ql/lib/semmle/python/Class.qll | 6 ++
.../src/Classes/Comparisons/Comparisons.qll | 10 ++++
.../Classes/Comparisons/EqualsOrNotEquals.ql | 56 ++++++++-----------
.../Classes/Comparisons/IncompleteOrdering.ql | 25 +++------
.../Comparisons/examples/EqualsOrNotEquals.py | 24 ++++++++
python/ql/src/Classes/Equality.qll | 25 +++++++--
6 files changed, 92 insertions(+), 54 deletions(-)
create mode 100644 python/ql/src/Classes/Comparisons/Comparisons.qll
diff --git a/python/ql/lib/semmle/python/Class.qll b/python/ql/lib/semmle/python/Class.qll
index 52c6c5aa389..58a6504b547 100644
--- a/python/ql/lib/semmle/python/Class.qll
+++ b/python/ql/lib/semmle/python/Class.qll
@@ -91,6 +91,12 @@ class Class extends Class_, Scope, AstNode {
/** Gets a method defined in this class */
Function getAMethod() { result.getScope() = this }
+ /** Gets the method defined in this class with the specified name, if any. */
+ Function getMethod(string name) {
+ result = this.getAMethod() and
+ result.getName() = name
+ }
+
override Location getLocation() { py_scope_location(result, this) }
/** Gets the scope (module, class or function) in which this class is defined */
diff --git a/python/ql/src/Classes/Comparisons/Comparisons.qll b/python/ql/src/Classes/Comparisons/Comparisons.qll
new file mode 100644
index 00000000000..b835b07ef44
--- /dev/null
+++ b/python/ql/src/Classes/Comparisons/Comparisons.qll
@@ -0,0 +1,10 @@
+/** Helper definitions for reasoning about comparison methods. */
+
+import python
+import semmle.python.ApiGraphs
+
+/** Holds if `cls` has the `functools.total_ordering` decorator. */
+predicate totalOrdering(Class cls) {
+ cls.getADecorator() =
+ API::moduleImport("functools").getMember("total_ordering").asSource().asExpr()
+}
diff --git a/python/ql/src/Classes/Comparisons/EqualsOrNotEquals.ql b/python/ql/src/Classes/Comparisons/EqualsOrNotEquals.ql
index adac5a20e87..feeada86682 100644
--- a/python/ql/src/Classes/Comparisons/EqualsOrNotEquals.ql
+++ b/python/ql/src/Classes/Comparisons/EqualsOrNotEquals.ql
@@ -2,7 +2,8 @@
* @name Inconsistent equality and inequality
* @description Defining only an equality method or an inequality method for a class violates the object model.
* @kind problem
- * @tags reliability
+ * @tags quality
+ * reliability
* correctness
* @problem.severity warning
* @sub-severity high
@@ -11,38 +12,29 @@
*/
import python
-import Equality
+import Comparisons
+import semmle.python.dataflow.new.internal.DataFlowDispatch
+import Classes.Equality
-string equals_or_ne() { result = "__eq__" or result = "__ne__" }
-
-predicate total_ordering(Class cls) {
- exists(Attribute a | a = cls.getADecorator() | a.getName() = "total_ordering")
+predicate missingEquality(Class cls, Function defined, string missing) {
+ defined = cls.getMethod("__ne__") and
+ not exists(cls.getMethod("__eq__")) and
+ missing = "__eq__"
or
- exists(Name n | n = cls.getADecorator() | n.getId() = "total_ordering")
+ // In python 3, __ne__ automatically delegates to __eq__ if its not defined in the hierarchy
+ // However if it is defined in a superclass (and isn't a delegation method) then it will use the superclass method (which may be incorrect)
+ defined = cls.getMethod("__eq__") and
+ not exists(cls.getMethod("__ne__")) and
+ exists(Function neMeth |
+ neMeth = getADirectSuperclass+(cls).getMethod("__ne__") and
+ not neMeth instanceof DelegatingEqualityMethod
+ ) and
+ missing = "__ne__"
}
-CallableValue implemented_method(ClassValue c, string name) {
- result = c.declaredAttribute(name) and name = equals_or_ne()
-}
-
-string unimplemented_method(ClassValue c) {
- not c.declaresAttribute(result) and result = equals_or_ne()
-}
-
-predicate violates_equality_contract(
- ClassValue c, string present, string missing, CallableValue method
-) {
- missing = unimplemented_method(c) and
- method = implemented_method(c, present) and
- not c.failedInference(_) and
- not total_ordering(c.getScope()) and
- /* Python 3 automatically implements __ne__ if __eq__ is defined, but not vice-versa */
- not (major_version() = 3 and present = "__eq__" and missing = "__ne__") and
- not method.getScope() instanceof DelegatingEqualityMethod and
- not c.lookup(missing).(CallableValue).getScope() instanceof DelegatingEqualityMethod
-}
-
-from ClassValue c, string present, string missing, CallableValue method
-where violates_equality_contract(c, present, missing, method)
-select method, "Class $@ implements " + present + " but does not implement " + missing + ".", c,
- c.getName()
+from Class cls, Function defined, string missing
+where
+ not totalOrdering(cls) and
+ missingEquality(cls, defined, missing)
+select cls, "This class implements $@, but does not implement " + missing + ".", defined,
+ defined.getName()
diff --git a/python/ql/src/Classes/Comparisons/IncompleteOrdering.ql b/python/ql/src/Classes/Comparisons/IncompleteOrdering.ql
index bbb6ca5cf6d..882321cc3f5 100644
--- a/python/ql/src/Classes/Comparisons/IncompleteOrdering.ql
+++ b/python/ql/src/Classes/Comparisons/IncompleteOrdering.ql
@@ -14,29 +14,20 @@
import python
import semmle.python.dataflow.new.internal.DataFlowDispatch
import semmle.python.ApiGraphs
-
-predicate totalOrdering(Class cls) {
- cls.getADecorator() =
- API::moduleImport("functools").getMember("total_ordering").asSource().asExpr()
-}
-
-Function getMethod(Class cls, string name) {
- result = cls.getAMethod() and
- result.getName() = name
-}
+import Comparisons
predicate definesStrictOrdering(Class cls, Function meth) {
- meth = getMethod(cls, "__lt__")
+ meth = cls.getMethod("__lt__")
or
- not exists(getMethod(cls, "__lt__")) and
- meth = getMethod(cls, "__gt__")
+ not exists(cls.getMethod("__lt__")) and
+ meth = cls.getMethod("__gt__")
}
predicate definesNonStrictOrdering(Class cls, Function meth) {
- meth = getMethod(cls, "__le__")
+ meth = cls.getMethod("__le__")
or
- not exists(getMethod(cls, "__le__")) and
- meth = getMethod(cls, "__ge__")
+ not exists(cls.getMethod("__le__")) and
+ meth = cls.getMethod("__ge__")
}
predicate missingComparison(Class cls, Function defined, string missing) {
@@ -53,5 +44,5 @@ from Class cls, Function defined, string missing
where
not totalOrdering(cls) and
missingComparison(cls, defined, missing)
-select cls, "This class implements $@, but does not implement an " + missing + " method.", defined,
+select cls, "This class implements $@, but does not implement " + missing + ".", defined,
defined.getName()
diff --git a/python/ql/src/Classes/Comparisons/examples/EqualsOrNotEquals.py b/python/ql/src/Classes/Comparisons/examples/EqualsOrNotEquals.py
index 7e1ece7685c..32bc26d4737 100644
--- a/python/ql/src/Classes/Comparisons/examples/EqualsOrNotEquals.py
+++ b/python/ql/src/Classes/Comparisons/examples/EqualsOrNotEquals.py
@@ -30,3 +30,27 @@ class PointUpdated(object):
def __ne__(self, other): # Improved: equality and inequality method defined (hash method still missing)
return not self == other
+
+
+class A:
+ def __init__(self, a):
+ self.a = a
+
+ def __eq__(self, other):
+ print("A eq")
+ return self.a == other.a
+
+ def __ne__(self, other):
+ print("A ne")
+ return self.a != other.a
+
+class B(A):
+ def __init__(self, a, b):
+ self.a = a
+ self.b = b
+
+ def __eq__(self, other):
+ print("B eq")
+ return self.a == other.a and self.b == other.b
+
+print(B(1,2) != B(1,3))
diff --git a/python/ql/src/Classes/Equality.qll b/python/ql/src/Classes/Equality.qll
index 347f5057c38..08162399e3e 100644
--- a/python/ql/src/Classes/Equality.qll
+++ b/python/ql/src/Classes/Equality.qll
@@ -1,4 +1,7 @@
+/** Utility definitions for reasoning about equality methods. */
+
import python
+import semmle.python.dataflow.new.DataFlow
private Attribute dictAccess(LocalVariable var) {
result.getName() = "__dict__" and
@@ -59,16 +62,28 @@ class IdentityEqMethod extends Function {
/** An (in)equality method that delegates to its complement */
class DelegatingEqualityMethod extends Function {
DelegatingEqualityMethod() {
- exists(Return ret, UnaryExpr not_, Compare comp, Cmpop op, Parameter p0, Parameter p1 |
+ exists(Return ret, UnaryExpr not_, Expr comp, Parameter p0, Parameter p1 |
ret.getScope() = this and
ret.getValue() = not_ and
not_.getOp() instanceof Not and
- not_.getOperand() = comp and
- comp.compares(p0.getVariable().getAnAccess(), op, p1.getVariable().getAnAccess())
+ not_.getOperand() = comp
|
- this.getName() = "__eq__" and op instanceof NotEq
+ exists(Cmpop op |
+ comp.(Compare).compares(p0.getVariable().getAnAccess(), op, p1.getVariable().getAnAccess())
+ |
+ this.getName() = "__eq__" and op instanceof NotEq
+ or
+ this.getName() = "__ne__" and op instanceof Eq
+ )
or
- this.getName() = "__ne__" and op instanceof Eq
+ exists(DataFlow::MethodCallNode call, string name |
+ call.calls(DataFlow::exprNode(p0.getVariable().getAnAccess()), name) and
+ call.getArg(0).asExpr() = p1.getVariable().getAnAccess()
+ |
+ this.getName() = "__eq__" and name = "__ne__"
+ or
+ this.getName() = "__ne__" and name = "__eq__"
+ )
)
}
}
From a687b60af987f948ef7df79b8d2825f930512ca4 Mon Sep 17 00:00:00 2001
From: Joe Farebrother
Date: Wed, 9 Jul 2025 13:32:13 +0100
Subject: [PATCH 014/984] Modernise equals-hash-mismatch
---
.../src/Classes/Comparisons/EqualsOrHash.ql | 53 +++----------------
1 file changed, 8 insertions(+), 45 deletions(-)
diff --git a/python/ql/src/Classes/Comparisons/EqualsOrHash.ql b/python/ql/src/Classes/Comparisons/EqualsOrHash.ql
index 4c8cf2c1169..4e73cef92fd 100644
--- a/python/ql/src/Classes/Comparisons/EqualsOrHash.ql
+++ b/python/ql/src/Classes/Comparisons/EqualsOrHash.ql
@@ -14,50 +14,13 @@
import python
-CallableValue defines_equality(ClassValue c, string name) {
- (
- name = "__eq__"
- or
- major_version() = 2 and name = "__cmp__"
- ) and
- result = c.declaredAttribute(name)
+predicate missingEquality(Class cls, Function defined) {
+ defined = cls.getMethod("__hash__") and
+ not exists(cls.getMethod("__eq__"))
+ // In python 3, the case of defined eq without hash automatically makes the class unhashable (even if a superclass defined hash)
+ // So this is not an issue.
}
-CallableValue implemented_method(ClassValue c, string name) {
- result = defines_equality(c, name)
- or
- result = c.declaredAttribute("__hash__") and name = "__hash__"
-}
-
-string unimplemented_method(ClassValue c) {
- not exists(defines_equality(c, _)) and
- (
- result = "__eq__" and major_version() = 3
- or
- major_version() = 2 and result = "__eq__ or __cmp__"
- )
- or
- /* Python 3 automatically makes classes unhashable if __eq__ is defined, but __hash__ is not */
- not c.declaresAttribute(result) and result = "__hash__" and major_version() = 2
-}
-
-/** Holds if this class is unhashable */
-predicate unhashable(ClassValue cls) {
- cls.lookup("__hash__") = Value::named("None")
- or
- cls.lookup("__hash__").(CallableValue).neverReturns()
-}
-
-predicate violates_hash_contract(ClassValue c, string present, string missing, Value method) {
- not unhashable(c) and
- missing = unimplemented_method(c) and
- method = implemented_method(c, present) and
- not c.failedInference(_)
-}
-
-from ClassValue c, string present, string missing, CallableValue method
-where
- violates_hash_contract(c, present, missing, method) and
- exists(c.getScope()) // Suppress results that aren't from source
-select method, "Class $@ implements " + present + " but does not define " + missing + ".", c,
- c.getName()
+from Class cls, Function defined
+where missingEquality(cls, defined)
+select cls, "This class implements $@, but does not implement __eq__.", defined, defined.getName()
From 8fb9bdd0afb985b3d7e566db40177f4452286e6e Mon Sep 17 00:00:00 2001
From: Joe Farebrother
Date: Wed, 9 Jul 2025 15:25:21 +0100
Subject: [PATCH 015/984] move equals attr test to equals attr folder
---
.../equals-attr/DefineEqualsWhenAddingAttributes.expected | 1 +
.../Classes/{equals-hash => equals-attr}/attr_eq_test.py | 0
.../Classes/equals-hash/DefineEqualsWhenAddingFields.expected | 1 -
.../Classes/equals-hash/DefineEqualsWhenAddingFields.qlref | 1 -
4 files changed, 1 insertion(+), 2 deletions(-)
rename python/ql/test/query-tests/Classes/{equals-hash => equals-attr}/attr_eq_test.py (100%)
delete mode 100644 python/ql/test/query-tests/Classes/equals-hash/DefineEqualsWhenAddingFields.expected
delete mode 100644 python/ql/test/query-tests/Classes/equals-hash/DefineEqualsWhenAddingFields.qlref
diff --git a/python/ql/test/query-tests/Classes/equals-attr/DefineEqualsWhenAddingAttributes.expected b/python/ql/test/query-tests/Classes/equals-attr/DefineEqualsWhenAddingAttributes.expected
index e69de29bb2d..2f5a5a249f5 100644
--- a/python/ql/test/query-tests/Classes/equals-attr/DefineEqualsWhenAddingAttributes.expected
+++ b/python/ql/test/query-tests/Classes/equals-attr/DefineEqualsWhenAddingAttributes.expected
@@ -0,0 +1 @@
+| attr_eq_test.py:21:1:21:27 | class BadColorPoint | The class 'BadColorPoint' does not override $@, but adds the new attribute $@. | attr_eq_test.py:10:5:10:28 | Function Point.__eq__ | '__eq__' | attr_eq_test.py:25:9:25:19 | Attribute | _color |
diff --git a/python/ql/test/query-tests/Classes/equals-hash/attr_eq_test.py b/python/ql/test/query-tests/Classes/equals-attr/attr_eq_test.py
similarity index 100%
rename from python/ql/test/query-tests/Classes/equals-hash/attr_eq_test.py
rename to python/ql/test/query-tests/Classes/equals-attr/attr_eq_test.py
diff --git a/python/ql/test/query-tests/Classes/equals-hash/DefineEqualsWhenAddingFields.expected b/python/ql/test/query-tests/Classes/equals-hash/DefineEqualsWhenAddingFields.expected
deleted file mode 100644
index 2f5a5a249f5..00000000000
--- a/python/ql/test/query-tests/Classes/equals-hash/DefineEqualsWhenAddingFields.expected
+++ /dev/null
@@ -1 +0,0 @@
-| attr_eq_test.py:21:1:21:27 | class BadColorPoint | The class 'BadColorPoint' does not override $@, but adds the new attribute $@. | attr_eq_test.py:10:5:10:28 | Function Point.__eq__ | '__eq__' | attr_eq_test.py:25:9:25:19 | Attribute | _color |
diff --git a/python/ql/test/query-tests/Classes/equals-hash/DefineEqualsWhenAddingFields.qlref b/python/ql/test/query-tests/Classes/equals-hash/DefineEqualsWhenAddingFields.qlref
deleted file mode 100644
index e542a6176ad..00000000000
--- a/python/ql/test/query-tests/Classes/equals-hash/DefineEqualsWhenAddingFields.qlref
+++ /dev/null
@@ -1 +0,0 @@
-Classes/DefineEqualsWhenAddingAttributes.ql
\ No newline at end of file
From 083d258585b0a226763b5301867c213b66456d25 Mon Sep 17 00:00:00 2001
From: Joe Farebrother
Date: Fri, 11 Jul 2025 15:10:45 +0100
Subject: [PATCH 016/984] Add/update unit tests
---
.../src/Classes/Comparisons/Comparisons.qll | 6 +-
.../Classes/equals-hash/EqualsOrHash.expected | 2 +
.../Classes/equals-hash/EqualsOrHash.qlref | 2 +
.../Classes/equals-hash/equalsHash.py | 19 +++
.../EqualsOrNotEquals.expected | 2 +
.../equals-not-equals/EqualsOrNotEquals.py | 147 ++++++++++++++++++
.../equals-not-equals/EqualsOrNotEquals.qlref | 2 +
.../IncompleteOrdering.expected | 3 +-
.../IncompleteOrdering.qlref | 3 +-
.../incomplete_ordering.py | 30 +++-
10 files changed, 208 insertions(+), 8 deletions(-)
create mode 100644 python/ql/test/query-tests/Classes/equals-hash/EqualsOrHash.expected
create mode 100644 python/ql/test/query-tests/Classes/equals-hash/EqualsOrHash.qlref
create mode 100644 python/ql/test/query-tests/Classes/equals-hash/equalsHash.py
create mode 100644 python/ql/test/query-tests/Classes/equals-not-equals/EqualsOrNotEquals.expected
create mode 100644 python/ql/test/query-tests/Classes/equals-not-equals/EqualsOrNotEquals.py
create mode 100644 python/ql/test/query-tests/Classes/equals-not-equals/EqualsOrNotEquals.qlref
diff --git a/python/ql/src/Classes/Comparisons/Comparisons.qll b/python/ql/src/Classes/Comparisons/Comparisons.qll
index b835b07ef44..5c049410c69 100644
--- a/python/ql/src/Classes/Comparisons/Comparisons.qll
+++ b/python/ql/src/Classes/Comparisons/Comparisons.qll
@@ -5,6 +5,8 @@ import semmle.python.ApiGraphs
/** Holds if `cls` has the `functools.total_ordering` decorator. */
predicate totalOrdering(Class cls) {
- cls.getADecorator() =
- API::moduleImport("functools").getMember("total_ordering").asSource().asExpr()
+ API::moduleImport("functools")
+ .getMember("total_ordering")
+ .asSource()
+ .flowsTo(DataFlow::exprNode(cls.getADecorator()))
}
diff --git a/python/ql/test/query-tests/Classes/equals-hash/EqualsOrHash.expected b/python/ql/test/query-tests/Classes/equals-hash/EqualsOrHash.expected
new file mode 100644
index 00000000000..bd584939b43
--- /dev/null
+++ b/python/ql/test/query-tests/Classes/equals-hash/EqualsOrHash.expected
@@ -0,0 +1,2 @@
+| equalsHash.py:13:1:13:8 | Class C | This class implements $@, but does not implement __eq__. | equalsHash.py:14:5:14:23 | Function __hash__ | __hash__ |
+| equalsHash.py:17:1:17:11 | Class D | This class implements $@, but does not implement __eq__. | equalsHash.py:18:5:18:23 | Function __hash__ | __hash__ |
diff --git a/python/ql/test/query-tests/Classes/equals-hash/EqualsOrHash.qlref b/python/ql/test/query-tests/Classes/equals-hash/EqualsOrHash.qlref
new file mode 100644
index 00000000000..e531bbc62e3
--- /dev/null
+++ b/python/ql/test/query-tests/Classes/equals-hash/EqualsOrHash.qlref
@@ -0,0 +1,2 @@
+query: Classes/Comparisons/EqualsOrHash.ql
+postprocess: utils/test/InlineExpectationsTestQuery.ql
\ No newline at end of file
diff --git a/python/ql/test/query-tests/Classes/equals-hash/equalsHash.py b/python/ql/test/query-tests/Classes/equals-hash/equalsHash.py
new file mode 100644
index 00000000000..6b3ec5d2b02
--- /dev/null
+++ b/python/ql/test/query-tests/Classes/equals-hash/equalsHash.py
@@ -0,0 +1,19 @@
+class A:
+ def __eq__(self, other):
+ return True
+
+ def __hash__(self, other):
+ return 7
+
+# B is automatically non-hashable - so eq without hash never needs to alert
+class B:
+ def __eq__(self, other):
+ return True
+
+class C: # $ Alert
+ def __hash__(self):
+ return 5
+
+class D(A): # $ Alert
+ def __hash__(self):
+ return 4
\ No newline at end of file
diff --git a/python/ql/test/query-tests/Classes/equals-not-equals/EqualsOrNotEquals.expected b/python/ql/test/query-tests/Classes/equals-not-equals/EqualsOrNotEquals.expected
new file mode 100644
index 00000000000..ceec3c1cef9
--- /dev/null
+++ b/python/ql/test/query-tests/Classes/equals-not-equals/EqualsOrNotEquals.expected
@@ -0,0 +1,2 @@
+| EqualsOrNotEquals.py:14:1:14:8 | Class B | This class implements $@, but does not implement __eq__. | EqualsOrNotEquals.py:19:5:19:28 | Function __ne__ | __ne__ |
+| EqualsOrNotEquals.py:37:1:37:11 | Class D | This class implements $@, but does not implement __ne__. | EqualsOrNotEquals.py:43:5:43:28 | Function __eq__ | __eq__ |
diff --git a/python/ql/test/query-tests/Classes/equals-not-equals/EqualsOrNotEquals.py b/python/ql/test/query-tests/Classes/equals-not-equals/EqualsOrNotEquals.py
new file mode 100644
index 00000000000..2052118e749
--- /dev/null
+++ b/python/ql/test/query-tests/Classes/equals-not-equals/EqualsOrNotEquals.py
@@ -0,0 +1,147 @@
+class A:
+ def __init__(self, a):
+ self.a = a
+
+ # OK: __ne__ if not defined delegates to eq automatically
+ def __eq__(self, other):
+ return self.a == other.a
+
+assert (A(1) == A(1))
+assert not (A(1) == A(2))
+assert not (A(1) != A(1))
+assert (A(1) != A(2))
+
+class B: # $ Alert
+ def __init__(self, b):
+ self.b = b
+
+ # BAD: eq defaults to `is`
+ def __ne__(self, other):
+ return self.b != other.b
+
+assert not (B(1) == B(1)) # potentially unexpected
+assert not (B(2) == B(2))
+assert not (B(1) != B(1))
+assert (B(1) != B(2))
+
+class C:
+ def __init__(self, c):
+ self.c = c
+
+ def __eq__(self, other):
+ return self.c == other.c
+
+ def __ne__(self, other):
+ return self.c != other.c
+
+class D(C): # $ Alert
+ def __init__(self, c, d):
+ super().__init__(c)
+ self.d = d
+
+ # BAD: ne is not defined, but the superclass ne is used instead of delegating, which may be incorrect
+ def __eq__(self, other):
+ return self.c == other.c and self.d == other.d
+
+assert (D(1,2) == D(1,2))
+assert not (D(1,2) == D(1,3))
+assert (D(1,2) != D(3,2))
+assert not (D(1,2) != D(1,3)) # Potentially unexpected
+
+class E:
+ def __init__(self, e):
+ self.e = e
+
+ def __eq__(self, other):
+ return self.e == other.e
+
+ def __ne__(self, other):
+ return not self.__eq__(other)
+
+class F(E):
+ def __init__(self, e, f):
+ super().__init__(e)
+ self.f = f
+
+ # OK: superclass ne delegates to eq
+ def __eq__(self, other):
+ return self.e == other.e and self.f == other.f
+
+assert (F(1,2) == F(1,2))
+assert not (F(1,2) == F(1,3))
+assert (F(1,2) != F(3,2))
+assert (F(1,2) != F(1,3))
+
+# Variations
+
+class E2:
+ def __init__(self, e):
+ self.e = e
+
+ def __eq__(self, other):
+ return self.e == other.e
+
+ def __ne__(self, other):
+ return not self == other
+
+class F2(E2):
+ def __init__(self, e, f):
+ super().__init__(e)
+ self.f = f
+
+ # OK: superclass ne delegates to eq
+ def __eq__(self, other):
+ return self.e == other.e and self.f == other.f
+
+assert (F2(1,2) == F2(1,2))
+assert not (F2(1,2) == F2(1,3))
+assert (F2(1,2) != F2(3,2))
+assert (F2(1,2) != F2(1,3))
+
+class E3:
+ def __init__(self, e):
+ self.e = e
+
+ def __eq__(self, other):
+ return self.e == other.e
+
+ def __ne__(self, other):
+ return not other.__eq__(self)
+
+class F3(E3):
+ def __init__(self, e, f):
+ super().__init__(e)
+ self.f = f
+
+ # OK: superclass ne delegates to eq
+ def __eq__(self, other):
+ return self.e == other.e and self.f == other.f
+
+assert (F3(1,2) == F3(1,2))
+assert not (F3(1,2) == F3(1,3))
+assert (F3(1,2) != F3(3,2))
+assert (F3(1,2) != F3(1,3))
+
+class E4:
+ def __init__(self, e):
+ self.e = e
+
+ def __eq__(self, other):
+ return self.e == other.e
+
+ def __ne__(self, other):
+ return not other == self
+
+class F4(E4):
+ def __init__(self, e, f):
+ super().__init__(e)
+ self.f = f
+
+ # OK: superclass ne delegates to eq
+ def __eq__(self, other):
+ return self.e == other.e and self.f == other.f
+
+assert (F4(1,2) == F4(1,2))
+assert not (F4(1,2) == F4(1,3))
+assert (F4(1,2) != F4(3,2))
+assert (F4(1,2) != F4(1,3))
\ No newline at end of file
diff --git a/python/ql/test/query-tests/Classes/equals-not-equals/EqualsOrNotEquals.qlref b/python/ql/test/query-tests/Classes/equals-not-equals/EqualsOrNotEquals.qlref
new file mode 100644
index 00000000000..9b1e8646c0e
--- /dev/null
+++ b/python/ql/test/query-tests/Classes/equals-not-equals/EqualsOrNotEquals.qlref
@@ -0,0 +1,2 @@
+query: Classes/Comparisons/EqualsOrNotEquals.ql
+postprocess: utils/test/InlineExpectationsTestQuery.ql
\ No newline at end of file
diff --git a/python/ql/test/query-tests/Classes/incomplete-ordering/IncompleteOrdering.expected b/python/ql/test/query-tests/Classes/incomplete-ordering/IncompleteOrdering.expected
index d376a002335..94df0ad1d32 100644
--- a/python/ql/test/query-tests/Classes/incomplete-ordering/IncompleteOrdering.expected
+++ b/python/ql/test/query-tests/Classes/incomplete-ordering/IncompleteOrdering.expected
@@ -1 +1,2 @@
-| incomplete_ordering.py:3:1:3:26 | class PartOrdered | Class PartOrdered implements $@, but does not implement __le__ or __gt__ or __ge__. | incomplete_ordering.py:13:5:13:28 | Function PartOrdered.__lt__ | __lt__ |
+| incomplete_ordering.py:3:1:3:26 | Class LtWithoutLe | This class implements $@, but does not implement __le__ or __ge__. | incomplete_ordering.py:13:5:13:28 | Function __lt__ | __lt__ |
+| incomplete_ordering.py:28:1:28:17 | Class LendGeNoLt | This class implements $@, but does not implement __lt__ or __gt__. | incomplete_ordering.py:29:5:29:28 | Function __le__ | __le__ |
diff --git a/python/ql/test/query-tests/Classes/incomplete-ordering/IncompleteOrdering.qlref b/python/ql/test/query-tests/Classes/incomplete-ordering/IncompleteOrdering.qlref
index 3387dad807a..cb15c6a47ba 100644
--- a/python/ql/test/query-tests/Classes/incomplete-ordering/IncompleteOrdering.qlref
+++ b/python/ql/test/query-tests/Classes/incomplete-ordering/IncompleteOrdering.qlref
@@ -1 +1,2 @@
-Classes/IncompleteOrdering.ql
\ No newline at end of file
+query: Classes/Comparisons/IncompleteOrdering.ql
+postprocess: utils/test/InlineExpectationsTestQuery.ql
\ No newline at end of file
diff --git a/python/ql/test/query-tests/Classes/incomplete-ordering/incomplete_ordering.py b/python/ql/test/query-tests/Classes/incomplete-ordering/incomplete_ordering.py
index 3c7514d7a83..2645819c43b 100644
--- a/python/ql/test/query-tests/Classes/incomplete-ordering/incomplete_ordering.py
+++ b/python/ql/test/query-tests/Classes/incomplete-ordering/incomplete_ordering.py
@@ -1,6 +1,6 @@
#Incomplete ordering
-class PartOrdered(object):
+class LtWithoutLe(object): # $ Alert
def __eq__(self, other):
return self is other
@@ -13,6 +13,28 @@ class PartOrdered(object):
def __lt__(self, other):
return False
-#Don't blame a sub-class for super-class's sins.
-class DerivedPartOrdered(PartOrdered):
- pass
\ No newline at end of file
+# Don't alert on subclass
+class LtWithoutLeSub(LtWithoutLe):
+ pass
+
+class LeSub(LtWithoutLe):
+ def __le__(self, other):
+ return self < other or self == other
+
+class GeSub(LtWithoutLe):
+ def __ge__(self, other):
+ return self > other or self == other
+
+class LendGeNoLt: # $ Alert
+ def __le__(self, other):
+ return True
+
+ def __ge__(self, other):
+ return other <= self
+
+from functools import total_ordering
+
+@total_ordering
+class Total:
+ def __le__(self, other):
+ return True
\ No newline at end of file
From 843a6c8012471c9966bbe8cbb2e6e18c0118fb3e Mon Sep 17 00:00:00 2001
From: Joe Farebrother
Date: Fri, 11 Jul 2025 15:12:59 +0100
Subject: [PATCH 017/984] Remove total order check from equals not equals
(doesn't make sense there; total order doesn't define eq or ne methods at
all)
---
python/ql/src/Classes/Comparisons/Comparisons.qll | 12 ------------
.../ql/src/Classes/Comparisons/EqualsOrNotEquals.ql | 5 +----
.../ql/src/Classes/Comparisons/IncompleteOrdering.ql | 9 ++++++++-
3 files changed, 9 insertions(+), 17 deletions(-)
delete mode 100644 python/ql/src/Classes/Comparisons/Comparisons.qll
diff --git a/python/ql/src/Classes/Comparisons/Comparisons.qll b/python/ql/src/Classes/Comparisons/Comparisons.qll
deleted file mode 100644
index 5c049410c69..00000000000
--- a/python/ql/src/Classes/Comparisons/Comparisons.qll
+++ /dev/null
@@ -1,12 +0,0 @@
-/** Helper definitions for reasoning about comparison methods. */
-
-import python
-import semmle.python.ApiGraphs
-
-/** Holds if `cls` has the `functools.total_ordering` decorator. */
-predicate totalOrdering(Class cls) {
- API::moduleImport("functools")
- .getMember("total_ordering")
- .asSource()
- .flowsTo(DataFlow::exprNode(cls.getADecorator()))
-}
diff --git a/python/ql/src/Classes/Comparisons/EqualsOrNotEquals.ql b/python/ql/src/Classes/Comparisons/EqualsOrNotEquals.ql
index feeada86682..25aafea6db2 100644
--- a/python/ql/src/Classes/Comparisons/EqualsOrNotEquals.ql
+++ b/python/ql/src/Classes/Comparisons/EqualsOrNotEquals.ql
@@ -12,7 +12,6 @@
*/
import python
-import Comparisons
import semmle.python.dataflow.new.internal.DataFlowDispatch
import Classes.Equality
@@ -33,8 +32,6 @@ predicate missingEquality(Class cls, Function defined, string missing) {
}
from Class cls, Function defined, string missing
-where
- not totalOrdering(cls) and
- missingEquality(cls, defined, missing)
+where missingEquality(cls, defined, missing)
select cls, "This class implements $@, but does not implement " + missing + ".", defined,
defined.getName()
diff --git a/python/ql/src/Classes/Comparisons/IncompleteOrdering.ql b/python/ql/src/Classes/Comparisons/IncompleteOrdering.ql
index 882321cc3f5..2a09b281058 100644
--- a/python/ql/src/Classes/Comparisons/IncompleteOrdering.ql
+++ b/python/ql/src/Classes/Comparisons/IncompleteOrdering.ql
@@ -14,7 +14,14 @@
import python
import semmle.python.dataflow.new.internal.DataFlowDispatch
import semmle.python.ApiGraphs
-import Comparisons
+
+/** Holds if `cls` has the `functools.total_ordering` decorator. */
+predicate totalOrdering(Class cls) {
+ API::moduleImport("functools")
+ .getMember("total_ordering")
+ .asSource()
+ .flowsTo(DataFlow::exprNode(cls.getADecorator()))
+}
predicate definesStrictOrdering(Class cls, Function meth) {
meth = cls.getMethod("__lt__")
From 58f503de38cbd8e2cd9dc07a209a6fdfb4fb4376 Mon Sep 17 00:00:00 2001
From: Joe Farebrother
Date: Fri, 11 Jul 2025 23:08:50 +0100
Subject: [PATCH 018/984] Update docs for incomplete ordering + inconsistent
hashing
---
.../Classes/Comparisons/EqualsOrHash.qhelp | 38 ++++++------
.../src/Classes/Comparisons/EqualsOrHash.ql | 2 +-
.../Classes/Comparisons/EqualsOrNotEquals.ql | 2 +-
.../Comparisons/IncompleteOrdering.qhelp | 30 +++++-----
.../Classes/Comparisons/IncompleteOrdering.ql | 2 +-
.../Comparisons/examples/EqualsOrHash.py | 58 +++----------------
.../Comparisons/examples/EqualsOrNotEquals.py | 23 --------
.../examples/IncompleteOrdering.py | 6 +-
8 files changed, 48 insertions(+), 113 deletions(-)
diff --git a/python/ql/src/Classes/Comparisons/EqualsOrHash.qhelp b/python/ql/src/Classes/Comparisons/EqualsOrHash.qhelp
index 28579a095f7..562ad7be1bd 100644
--- a/python/ql/src/Classes/Comparisons/EqualsOrHash.qhelp
+++ b/python/ql/src/Classes/Comparisons/EqualsOrHash.qhelp
@@ -4,42 +4,40 @@
-
In order to conform to the object model, classes that define their own equality method should also
-define their own hash method, or be unhashable. If the hash method is not defined then the hash of the
-super class is used. This is unlikely to result in the expected behavior.
+
A hashable class has an __eq__ method, and a __hash__ method that agrees with equality.
+When a hash method is defined, an equality method should also be defined; otherwise object identity is used for equality comparisons
+which may not be intended.
+
-
A class can be made unhashable by setting its __hash__ attribute to None.
-
-
In Python 3, if you define a class-level equality method and omit a __hash__ method
-then the class is automatically marked as unhashable.
+
Note that defining an __eq__ method without defining a __hash__ method automatically makes the class unhashable in Python 3.
+(even if a superclass defines a hash method).
-
When you define an __eq__ method for a class, remember to implement a __hash__ method or set
-__hash__ = None.
+
+If a __hash__ method is defined, ensure a compatible __eq__ method is also defined.
+
+
+
+To explicitly declare a class as unhashable, set __hash__ = None, rather than defining a __hash__ method that always
+raises an exception. Otherwise, the class would be incorrectly identified as hashable by an isinstance(obj, collections.abc.Hashable) call.
+
-
In the following example the Point class defines an equality method but
-no hash method. If hash is called on this class then the hash method defined for object
-is used. This is unlikely to give the required behavior. The PointUpdated class
-is better as it defines both an equality and a hash method.
-If Point was not to be used in dicts or sets, then it could be defined as
-UnhashablePoint below.
+
In the following example, the A class defines an hash method but
+no equality method. Equality will be determined by object identity, which may not be the expected behaviour.
-
-To comply fully with the object model this class should also define an inequality method (identified
-by a separate rule).
diff --git a/python/ql/src/Classes/Comparisons/EqualsOrHash.ql b/python/ql/src/Classes/Comparisons/EqualsOrHash.ql
index 4e73cef92fd..54393cf1573 100644
--- a/python/ql/src/Classes/Comparisons/EqualsOrHash.ql
+++ b/python/ql/src/Classes/Comparisons/EqualsOrHash.ql
@@ -1,6 +1,6 @@
/**
* @name Inconsistent equality and hashing
- * @description Defining equality for a class without also defining hashability (or vice-versa) violates the object model.
+ * @description Defining a hash operation without defining equality may be a mistake.
* @kind problem
* @tags quality
* reliability
diff --git a/python/ql/src/Classes/Comparisons/EqualsOrNotEquals.ql b/python/ql/src/Classes/Comparisons/EqualsOrNotEquals.ql
index 25aafea6db2..ea025f39c2f 100644
--- a/python/ql/src/Classes/Comparisons/EqualsOrNotEquals.ql
+++ b/python/ql/src/Classes/Comparisons/EqualsOrNotEquals.ql
@@ -1,6 +1,6 @@
/**
* @name Inconsistent equality and inequality
- * @description Defining only an equality method or an inequality method for a class violates the object model.
+ * @description Class definitions of equality and inequality operators may be inconsistent.
* @kind problem
* @tags quality
* reliability
diff --git a/python/ql/src/Classes/Comparisons/IncompleteOrdering.qhelp b/python/ql/src/Classes/Comparisons/IncompleteOrdering.qhelp
index 7983985ccee..abb4faef59c 100644
--- a/python/ql/src/Classes/Comparisons/IncompleteOrdering.qhelp
+++ b/python/ql/src/Classes/Comparisons/IncompleteOrdering.qhelp
@@ -3,32 +3,34 @@
"qhelp.dtd">
-
A class that implements an ordering operator
-(__lt__, __gt__, __le__ or __ge__) should implement
-all four in order that ordering between two objects is consistent and obeys the usual mathematical rules.
-If the ordering is inconsistent with default equality, then __eq__ and __ne__
-should also be implemented.
+
A class that implements the rich comparison operators
+(__lt__, __gt__, __le__, or __ge__) should ensure that all four
+comparison operations <, <=, >, and >= function as expected, consistent
+with expected mathematical rules.
+In Python 3, this is ensured by implementing one of __lt__ or __gt__, and one of __le__ or __ge__.
+If the ordering is not consistent with default equality, then __eq__ should also be implemented.
-
Ensure that all four ordering comparisons are implemented as well as __eq__ and
-__ne__ if required.
+
Ensure that at least one of __lt__ or __gt__ and at least one of __le__ or __ge__ is defined.
+
-
It is not necessary to manually implement all four comparisons,
-the functools.total_ordering class decorator can be used.
+
+The functools.total_ordering class decorator can be used to automatically implement all four comparison methods from a single one,
+which is typically the cleanest way to ensure all necessary comparison methods are implemented consistently.
-
In this example only the __lt__ operator has been implemented which could lead to
-inconsistent behavior. __gt__, __le__, __ge__, and in this case,
-__eq__ and __ne__ should be implemented.
-
+
In the following example, only the __lt__ operator has been implemented, which would lead to unexpected
+errors if the <= or >= operators are used on A instances.
+The __le__ method should also be defined, as well as __eq__ in this case.
diff --git a/python/ql/src/Classes/Comparisons/IncompleteOrdering.ql b/python/ql/src/Classes/Comparisons/IncompleteOrdering.ql
index 2a09b281058..e35f0c1a715 100644
--- a/python/ql/src/Classes/Comparisons/IncompleteOrdering.ql
+++ b/python/ql/src/Classes/Comparisons/IncompleteOrdering.ql
@@ -1,6 +1,6 @@
/**
* @name Incomplete ordering
- * @description Class defines one or more ordering method but does not define all 4 ordering comparison methods
+ * @description Class defines ordering comparison methods, but does not define both strict and nonstrict ordering methods, to ensure all four comparison operators behave as expected.
* @kind problem
* @tags quality
* reliability
diff --git a/python/ql/src/Classes/Comparisons/examples/EqualsOrHash.py b/python/ql/src/Classes/Comparisons/examples/EqualsOrHash.py
index e89c75b30ad..601ce2b18d0 100644
--- a/python/ql/src/Classes/Comparisons/examples/EqualsOrHash.py
+++ b/python/ql/src/Classes/Comparisons/examples/EqualsOrHash.py
@@ -1,52 +1,8 @@
-# Incorrect: equality method defined but class contains no hash method
-class Point(object):
-
- def __init__(self, x, y):
- self._x = x
- self._y = y
-
- def __repr__(self):
- return 'Point(%r, %r)' % (self._x, self._y)
-
- def __eq__(self, other):
- if not isinstance(other, Point):
- return False
- return self._x == other._x and self._y == other._y
-
-
-# Improved: equality and hash method defined (inequality method still missing)
-class PointUpdated(object):
-
- def __init__(self, x, y):
- self._x = x
- self._y = y
-
- def __repr__(self):
- return 'Point(%r, %r)' % (self._x, self._y)
-
- def __eq__(self, other):
- if not isinstance(other, Point):
- return False
- return self._x == other._x and self._y == other._y
-
- def __hash__(self):
- return hash(self._x) ^ hash(self._y)
-
-# Improved: equality method defined and class instances made unhashable
-class UnhashablePoint(object):
-
- def __init__(self, x, y):
- self._x = x
- self._y = y
-
- def __repr__(self):
- return 'Point(%r, %r)' % (self._x, self._y)
-
- def __eq__(self, other):
- if not isinstance(other, Point):
- return False
- return self._x == other._x and self._y == other._y
-
- #Tell the interpreter that instances of this class cannot be hashed
- __hash__ = None
+class A:
+ def __init__(self, a, b):
+ self.a = a
+ self.b = b
+ # No equality method is defined
+ def __hash__(self):
+ return hash((self.a, self.b))
diff --git a/python/ql/src/Classes/Comparisons/examples/EqualsOrNotEquals.py b/python/ql/src/Classes/Comparisons/examples/EqualsOrNotEquals.py
index 32bc26d4737..080c9b8f6e4 100644
--- a/python/ql/src/Classes/Comparisons/examples/EqualsOrNotEquals.py
+++ b/python/ql/src/Classes/Comparisons/examples/EqualsOrNotEquals.py
@@ -31,26 +31,3 @@ class PointUpdated(object):
return not self == other
-
-class A:
- def __init__(self, a):
- self.a = a
-
- def __eq__(self, other):
- print("A eq")
- return self.a == other.a
-
- def __ne__(self, other):
- print("A ne")
- return self.a != other.a
-
-class B(A):
- def __init__(self, a, b):
- self.a = a
- self.b = b
-
- def __eq__(self, other):
- print("B eq")
- return self.a == other.a and self.b == other.b
-
-print(B(1,2) != B(1,3))
diff --git a/python/ql/src/Classes/Comparisons/examples/IncompleteOrdering.py b/python/ql/src/Classes/Comparisons/examples/IncompleteOrdering.py
index 78b306880b0..7ea0f0f82a7 100644
--- a/python/ql/src/Classes/Comparisons/examples/IncompleteOrdering.py
+++ b/python/ql/src/Classes/Comparisons/examples/IncompleteOrdering.py
@@ -1,6 +1,8 @@
-class IncompleteOrdering(object):
+class A:
def __init__(self, i):
self.i = i
+ # BAD: le is not defined, so `A(1) <= A(2) would result in an error.`
def __lt__(self, other):
- return self.i < other.i
\ No newline at end of file
+ return self.i < other.i
+
\ No newline at end of file
From ea48fcca8f55b76ed0383182734363b385c9b4cf Mon Sep 17 00:00:00 2001
From: Joe Farebrother
Date: Mon, 14 Jul 2025 10:49:28 +0100
Subject: [PATCH 019/984] Update doc for equalsNotEquals
---
.../Comparisons/EqualsOrNotEquals.qhelp | 42 ++++++++++++-------
.../Comparisons/IncompleteOrdering.qhelp | 3 +-
.../Comparisons/examples/EqualsOrNotEquals.py | 33 ---------------
.../examples/EqualsOrNotEquals1.py | 15 +++++++
.../examples/EqualsOrNotEquals2.py | 21 ++++++++++
5 files changed, 66 insertions(+), 48 deletions(-)
delete mode 100644 python/ql/src/Classes/Comparisons/examples/EqualsOrNotEquals.py
create mode 100644 python/ql/src/Classes/Comparisons/examples/EqualsOrNotEquals1.py
create mode 100644 python/ql/src/Classes/Comparisons/examples/EqualsOrNotEquals2.py
diff --git a/python/ql/src/Classes/Comparisons/EqualsOrNotEquals.qhelp b/python/ql/src/Classes/Comparisons/EqualsOrNotEquals.qhelp
index c49f3d2529e..49e825d7ef4 100644
--- a/python/ql/src/Classes/Comparisons/EqualsOrNotEquals.qhelp
+++ b/python/ql/src/Classes/Comparisons/EqualsOrNotEquals.qhelp
@@ -4,33 +4,47 @@
-
In order to conform to the object model, classes should define either no equality methods, or both
-an equality and an inequality method. If only one of __eq__ or __ne__ is
-defined then the method from the super class is used. This is unlikely to result in the expected
-behavior.
+
In order to ensure the == and != operators behave consistently as expected (i.e. they should be negations of each other), care should be taken when implementing the
+__eq__ and __ne__ special methods.
+
+
In Python 3, if the __eq__ method is defined in a class while the __ne__ is not,
+then the != operator will automatically delegate to the __eq__ method in the expected way.
+
+
+
However, if the __ne__ method is defined without a corresponding __eq__ method,
+ the == operator will still default to object identity (equivalent to the is operator), while the !=
+ operator will use the __ne__ method, which may be inconsistent.
+
+
Additionally, if the __ne__ method is defined on a superclass, and the subclass defines its own __eq__ method without overriding
+the superclass __ne__ method, the != operator will use this superclass __ne__ method, rather than automatically delegating
+to __eq__, which may be incorrect.
-
When you define an equality or an inequality method for a class, remember to implement both an
-__eq__ method and an __ne__ method.
+
Ensure that when an __ne__ method is defined, the __eq__ method is also defined, and their results are consistent.
+In most cases, the __ne__ method does not need to be defined at all, as the default behavior is to delegate to __eq__ and negate the result.
-
In the following example the PointOriginal class defines an equality method but
-no inequality method. If this class is tested for inequality then a type error will be raised. The
-PointUpdated class is better as it defines both an equality and an inequality method. To
-comply fully with the object model this class should also define a hash method (identified by
-a separate rule).
+
In the following example, A defines a __ne__ method, but not an __eq__ method.
+This leads to inconsistent results between equality and inequality operators.
+
-
+
+
+
In the following example, C defines an __eq__ method, but its __ne__ implementation is inherited from B,
+which is not consistent with the equality operation.
+
diff --git a/python/ql/src/Classes/Comparisons/IncompleteOrdering.qhelp b/python/ql/src/Classes/Comparisons/IncompleteOrdering.qhelp
index abb4faef59c..6bffaed7b87 100644
--- a/python/ql/src/Classes/Comparisons/IncompleteOrdering.qhelp
+++ b/python/ql/src/Classes/Comparisons/IncompleteOrdering.qhelp
@@ -17,7 +17,8 @@ If the ordering is not consistent with default equality, then __eq__
-The functools.total_ordering class decorator can be used to automatically implement all four comparison methods from a single one,
+The functools.total_ordering class decorator can be used to automatically implement all four comparison methods from a
+single one,
which is typically the cleanest way to ensure all necessary comparison methods are implemented consistently.
diff --git a/python/ql/src/Classes/Comparisons/examples/EqualsOrNotEquals.py b/python/ql/src/Classes/Comparisons/examples/EqualsOrNotEquals.py
deleted file mode 100644
index 080c9b8f6e4..00000000000
--- a/python/ql/src/Classes/Comparisons/examples/EqualsOrNotEquals.py
+++ /dev/null
@@ -1,33 +0,0 @@
-class PointOriginal(object):
-
- def __init__(self, x, y):
- self._x, x
- self._y = y
-
- def __repr__(self):
- return 'Point(%r, %r)' % (self._x, self._y)
-
- def __eq__(self, other): # Incorrect: equality is defined but inequality is not
- if not isinstance(other, Point):
- return False
- return self._x == other._x and self._y == other._y
-
-
-class PointUpdated(object):
-
- def __init__(self, x, y):
- self._x, x
- self._y = y
-
- def __repr__(self):
- return 'Point(%r, %r)' % (self._x, self._y)
-
- def __eq__(self, other):
- if not isinstance(other, Point):
- return False
- return self._x == other._x and self._y == other._y
-
- def __ne__(self, other): # Improved: equality and inequality method defined (hash method still missing)
- return not self == other
-
-
diff --git a/python/ql/src/Classes/Comparisons/examples/EqualsOrNotEquals1.py b/python/ql/src/Classes/Comparisons/examples/EqualsOrNotEquals1.py
new file mode 100644
index 00000000000..2f749ebeb9e
--- /dev/null
+++ b/python/ql/src/Classes/Comparisons/examples/EqualsOrNotEquals1.py
@@ -0,0 +1,15 @@
+class A:
+ def __init__(self, a):
+ self.a = a
+
+ # BAD: ne is defined, but not eq.
+ def __ne__(self, other):
+ if not isinstance(other, A):
+ return NotImplemented
+ return self.a != other.a
+
+x = A(1)
+y = A(1)
+
+print(x == y) # Prints False (potentially unexpected - object identity is used)
+print(x != y) # Prints False
diff --git a/python/ql/src/Classes/Comparisons/examples/EqualsOrNotEquals2.py b/python/ql/src/Classes/Comparisons/examples/EqualsOrNotEquals2.py
new file mode 100644
index 00000000000..051108be9c5
--- /dev/null
+++ b/python/ql/src/Classes/Comparisons/examples/EqualsOrNotEquals2.py
@@ -0,0 +1,21 @@
+class B:
+ def __init__(self, b):
+ self.b = b
+
+ def __eq__(self, other):
+ return self.b == other.b
+
+ def __ne__(self, other):
+ return self.b != other.b
+
+class C(B):
+ def __init__(self, b, c):
+ super().init(b)
+ self.c = c
+
+ # BAD: eq is defined, but != will use superclass ne method, which is not consistent
+ def __eq__(self, other):
+ return self.b == other.b and self.c == other.c
+
+print(C(1,2) == C(1,3)) # Prints False
+print(C(1,2) != C(1,3)) # Prints False (potentially unexpected)
\ No newline at end of file
From 61af4e451484502a6ff651f3735b4196c2ce944b Mon Sep 17 00:00:00 2001
From: Joe Farebrother
Date: Mon, 14 Jul 2025 11:00:05 +0100
Subject: [PATCH 020/984] Add changenote and update integraion test output
---
.../query-suite/python-code-quality-extended.qls.expected | 4 +++-
.../query-suite/python-code-quality.qls.expected | 4 +++-
python/ql/src/change-notes/2025-07-14-comparisons.md | 4 ++++
3 files changed, 10 insertions(+), 2 deletions(-)
create mode 100644 python/ql/src/change-notes/2025-07-14-comparisons.md
diff --git a/python/ql/integration-tests/query-suite/python-code-quality-extended.qls.expected b/python/ql/integration-tests/query-suite/python-code-quality-extended.qls.expected
index 960972c508c..cbc32fbd4ca 100644
--- a/python/ql/integration-tests/query-suite/python-code-quality-extended.qls.expected
+++ b/python/ql/integration-tests/query-suite/python-code-quality-extended.qls.expected
@@ -1,6 +1,8 @@
+ql/python/ql/src/Classes/Comparisons/EqualsOrHash.ql
+ql/python/ql/src/Classes/Comparisons/EqualsOrNotEquals.ql
+ql/python/ql/src/Classes/Comparisons/IncompleteOrdering.ql
ql/python/ql/src/Classes/ConflictingAttributesInBaseClasses.ql
ql/python/ql/src/Classes/DefineEqualsWhenAddingAttributes.ql
-ql/python/ql/src/Classes/EqualsOrHash.ql
ql/python/ql/src/Classes/InconsistentMRO.ql
ql/python/ql/src/Classes/InitCallsSubclass/InitCallsSubclassMethod.ql
ql/python/ql/src/Classes/MissingCallToDel.ql
diff --git a/python/ql/integration-tests/query-suite/python-code-quality.qls.expected b/python/ql/integration-tests/query-suite/python-code-quality.qls.expected
index 960972c508c..cbc32fbd4ca 100644
--- a/python/ql/integration-tests/query-suite/python-code-quality.qls.expected
+++ b/python/ql/integration-tests/query-suite/python-code-quality.qls.expected
@@ -1,6 +1,8 @@
+ql/python/ql/src/Classes/Comparisons/EqualsOrHash.ql
+ql/python/ql/src/Classes/Comparisons/EqualsOrNotEquals.ql
+ql/python/ql/src/Classes/Comparisons/IncompleteOrdering.ql
ql/python/ql/src/Classes/ConflictingAttributesInBaseClasses.ql
ql/python/ql/src/Classes/DefineEqualsWhenAddingAttributes.ql
-ql/python/ql/src/Classes/EqualsOrHash.ql
ql/python/ql/src/Classes/InconsistentMRO.ql
ql/python/ql/src/Classes/InitCallsSubclass/InitCallsSubclassMethod.ql
ql/python/ql/src/Classes/MissingCallToDel.ql
diff --git a/python/ql/src/change-notes/2025-07-14-comparisons.md b/python/ql/src/change-notes/2025-07-14-comparisons.md
new file mode 100644
index 00000000000..a8a2bdacf31
--- /dev/null
+++ b/python/ql/src/change-notes/2025-07-14-comparisons.md
@@ -0,0 +1,4 @@
+---
+category: minorAnalysis
+---
+* The queries `py/incomplete-ordering`, `py/inconsistent-equality`, and `py/equals-hash-mismatch` have been modernized; no longer relying on outdated libraries, improved documentation, and no longer producing alerts for problems specific to Python 2.
\ No newline at end of file
From f784bb0a35ed785abad01968b999844db2d20732 Mon Sep 17 00:00:00 2001
From: Joe Farebrother
Date: Mon, 14 Jul 2025 14:26:49 +0100
Subject: [PATCH 021/984] Fix qldoc errors + typos
---
python/ql/src/Classes/Comparisons/EqualsOrNotEquals.qhelp | 4 +++-
.../ql/src/Classes/Comparisons/examples/EqualsOrNotEquals2.py | 2 +-
.../ql/src/Classes/Comparisons/examples/IncompleteOrdering.py | 2 +-
python/ql/test/query-tests/Classes/equals-hash/equalsHash.py | 2 +-
4 files changed, 6 insertions(+), 4 deletions(-)
diff --git a/python/ql/src/Classes/Comparisons/EqualsOrNotEquals.qhelp b/python/ql/src/Classes/Comparisons/EqualsOrNotEquals.qhelp
index 49e825d7ef4..74f20d9f0c5 100644
--- a/python/ql/src/Classes/Comparisons/EqualsOrNotEquals.qhelp
+++ b/python/ql/src/Classes/Comparisons/EqualsOrNotEquals.qhelp
@@ -14,10 +14,12 @@ then the != operator will automatically delegate to the __eq_
However, if the __ne__ method is defined without a corresponding __eq__ method,
the == operator will still default to object identity (equivalent to the is operator), while the !=
operator will use the __ne__ method, which may be inconsistent.
+
-
Additionally, if the __ne__ method is defined on a superclass, and the subclass defines its own __eq__
method without overriding
+
Additionally, if the __ne__ method is defined on a superclass, and the subclass defines its own __eq__ method without overriding
the superclass __ne__ method, the != operator will use this superclass __ne__ method, rather than automatically delegating
to __eq__, which may be incorrect.
+
Spring Boot is a popular framework that facilitates the development of stand-alone applications
+and micro services. Spring Boot Actuator helps to expose production-ready support features against
+Spring Boot applications.
+
+
Endpoints of Spring Boot Actuator allow to monitor and interact with a Spring Boot application.
+Exposing unprotected actuator endpoints through configuration files can lead to information disclosure
+or even remote code execution vulnerability.
+
+
Rather than programmatically permitting endpoint requests or enforcing access control, frequently
+developers simply leave management endpoints publicly accessible in the application configuration file
+application.properties without enforcing access control through Spring Security.
+
+
+
+
Declare the Spring Boot Starter Security module in XML configuration or programmatically enforce
+security checks on management endpoints using Spring Security. Otherwise accessing management endpoints
+on a different HTTP port other than the port that the web application is listening on also helps to
+improve the security.
+
+
+
+
The following examples show both 'BAD' and 'GOOD' configurations. In the 'BAD' configuration,
+no security module is declared and sensitive management endpoints are exposed. In the 'GOOD' configuration,
+security is enforced and only endpoints requiring exposure are exposed.
+
+
diff --git a/java/ql/src/Security/CWE/CWE-200/InsecureSpringActuatorConfig/InsecureSpringActuatorConfig.ql b/java/ql/src/Security/CWE/CWE-200/InsecureSpringActuatorConfig/InsecureSpringActuatorConfig.ql
new file mode 100644
index 00000000000..b21aa82e8ba
--- /dev/null
+++ b/java/ql/src/Security/CWE/CWE-200/InsecureSpringActuatorConfig/InsecureSpringActuatorConfig.ql
@@ -0,0 +1,121 @@
+/**
+ * @name Insecure Spring Boot Actuator Configuration
+ * @description Exposed Spring Boot Actuator through configuration files without declarative or procedural
+ * security enforcement leads to information leak or even remote code execution.
+ * @kind problem
+ * @problem.severity error
+ * @precision high
+ * @id java/insecure-spring-actuator-config
+ * @tags security
+ * experimental
+ * external/cwe/cwe-016
+ */
+
+/*
+ * Note this query requires properties files to be indexed before it can produce results.
+ * If creating your own database with the CodeQL CLI, you should run
+ * `codeql database index-files --language=properties ...`
+ * If using lgtm.com, you should add `properties_files: true` to the index block of your
+ * lgtm.yml file (see https://lgtm.com/help/lgtm/java-extraction)
+ */
+
+import java
+import semmle.code.configfiles.ConfigFiles
+import semmle.code.xml.MavenPom
+
+/** The parent node of the `org.springframework.boot` group. */
+class SpringBootParent extends Parent {
+ SpringBootParent() { this.getGroup().getValue() = "org.springframework.boot" }
+}
+
+/** Class of Spring Boot dependencies. */
+class SpringBootPom extends Pom {
+ SpringBootPom() { this.getParentElement() instanceof SpringBootParent }
+
+ /** Holds if the Spring Boot Actuator module `spring-boot-starter-actuator` is used in the project. */
+ predicate isSpringBootActuatorUsed() {
+ this.getADependency().getArtifact().getValue() = "spring-boot-starter-actuator"
+ }
+
+ /**
+ * Holds if the Spring Boot Security module is used in the project, which brings in other security
+ * related libraries.
+ */
+ predicate isSpringBootSecurityUsed() {
+ this.getADependency().getArtifact().getValue() = "spring-boot-starter-security"
+ }
+}
+
+/** The properties file `application.properties`. */
+class ApplicationProperties extends ConfigPair {
+ ApplicationProperties() { this.getFile().getBaseName() = "application.properties" }
+}
+
+/** The configuration property `management.security.enabled`. */
+class ManagementSecurityConfig extends ApplicationProperties {
+ ManagementSecurityConfig() { this.getNameElement().getName() = "management.security.enabled" }
+
+ /** Gets the whitespace-trimmed value of this property. */
+ string getValue() { result = this.getValueElement().getValue().trim() }
+
+ /** Holds if `management.security.enabled` is set to `false`. */
+ predicate hasSecurityDisabled() { this.getValue() = "false" }
+
+ /** Holds if `management.security.enabled` is set to `true`. */
+ predicate hasSecurityEnabled() { this.getValue() = "true" }
+}
+
+/** The configuration property `management.endpoints.web.exposure.include`. */
+class ManagementEndPointInclude extends ApplicationProperties {
+ ManagementEndPointInclude() {
+ this.getNameElement().getName() = "management.endpoints.web.exposure.include"
+ }
+
+ /** Gets the whitespace-trimmed value of this property. */
+ string getValue() { result = this.getValueElement().getValue().trim() }
+}
+
+/**
+ * Holds if `ApplicationProperties` ap of a repository managed by `SpringBootPom` pom
+ * has a vulnerable configuration of Spring Boot Actuator management endpoints.
+ */
+predicate hasConfidentialEndPointExposed(SpringBootPom pom, ApplicationProperties ap) {
+ pom.isSpringBootActuatorUsed() and
+ not pom.isSpringBootSecurityUsed() and
+ ap.getFile()
+ .getParentContainer()
+ .getAbsolutePath()
+ .matches(pom.getFile().getParentContainer().getAbsolutePath() + "%") and // in the same sub-directory
+ exists(string springBootVersion | springBootVersion = pom.getParentElement().getVersionString() |
+ springBootVersion.regexpMatch("1\\.[0-4].*") and // version 1.0, 1.1, ..., 1.4
+ not exists(ManagementSecurityConfig me |
+ me.hasSecurityEnabled() and me.getFile() = ap.getFile()
+ )
+ or
+ springBootVersion.matches("1.5%") and // version 1.5
+ exists(ManagementSecurityConfig me | me.hasSecurityDisabled() and me.getFile() = ap.getFile())
+ or
+ springBootVersion.matches("2.%") and //version 2.x
+ exists(ManagementEndPointInclude mi |
+ mi.getFile() = ap.getFile() and
+ (
+ mi.getValue() = "*" // all endpoints are enabled
+ or
+ mi.getValue()
+ .matches([
+ "%dump%", "%trace%", "%logfile%", "%shutdown%", "%startup%", "%mappings%", "%env%",
+ "%beans%", "%sessions%"
+ ]) // confidential endpoints to check although all endpoints apart from '/health' and '/info' are considered sensitive by Spring
+ )
+ )
+ )
+}
+
+deprecated query predicate problems(Dependency d, string message) {
+ exists(SpringBootPom pom |
+ hasConfidentialEndPointExposed(pom, _) and
+ d = pom.getADependency() and
+ d.getArtifact().getValue() = "spring-boot-starter-actuator"
+ ) and
+ message = "Insecure configuration of Spring Boot Actuator exposes sensitive endpoints."
+}
diff --git a/java/ql/src/Security/CWE/CWE-200/InsecureSpringActuatorConfig/application.properties b/java/ql/src/Security/CWE/CWE-200/InsecureSpringActuatorConfig/application.properties
new file mode 100644
index 00000000000..441d752508c
--- /dev/null
+++ b/java/ql/src/Security/CWE/CWE-200/InsecureSpringActuatorConfig/application.properties
@@ -0,0 +1,22 @@
+#management.endpoints.web.base-path=/admin
+
+
+#### BAD: All management endpoints are accessible ####
+# vulnerable configuration (spring boot 1.0 - 1.4): exposes actuators by default
+
+# vulnerable configuration (spring boot 1.5+): requires value false to expose sensitive actuators
+management.security.enabled=false
+
+# vulnerable configuration (spring boot 2+): exposes health and info only by default, here overridden to expose everything
+management.endpoints.web.exposure.include=*
+
+
+#### GOOD: All management endpoints have access control ####
+# safe configuration (spring boot 1.0 - 1.4): exposes actuators by default
+management.security.enabled=true
+
+# safe configuration (spring boot 1.5+): requires value false to expose sensitive actuators
+management.security.enabled=true
+
+# safe configuration (spring boot 2+): exposes health and info only by default, here overridden to expose one additional endpoint which we assume is intentional and safe.
+management.endpoints.web.exposure.include=beans,info,health
diff --git a/java/ql/src/Security/CWE/CWE-200/InsecureSpringActuatorConfig/pom_bad.xml b/java/ql/src/Security/CWE/CWE-200/InsecureSpringActuatorConfig/pom_bad.xml
new file mode 100644
index 00000000000..6bca2829ac4
--- /dev/null
+++ b/java/ql/src/Security/CWE/CWE-200/InsecureSpringActuatorConfig/pom_bad.xml
@@ -0,0 +1,50 @@
+
+
+ 4.0.0
+
+ spring-boot-actuator-app
+ spring-boot-actuator-app
+ 1.0-SNAPSHOT
+
+
+ UTF-8
+ 1.8
+ 1.8
+
+
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 2.3.8.RELEASE
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+ org.springframework.boot
+ spring-boot-starter-actuator
+
+
+ org.springframework.boot
+ spring-boot-devtools
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-test
+
+
+
+
diff --git a/java/ql/src/Security/CWE/CWE-200/InsecureSpringActuatorConfig/pom_good.xml b/java/ql/src/Security/CWE/CWE-200/InsecureSpringActuatorConfig/pom_good.xml
new file mode 100644
index 00000000000..03bc257f5bd
--- /dev/null
+++ b/java/ql/src/Security/CWE/CWE-200/InsecureSpringActuatorConfig/pom_good.xml
@@ -0,0 +1,50 @@
+
+
+ 4.0.0
+
+ spring-boot-actuator-app
+ spring-boot-actuator-app
+ 1.0-SNAPSHOT
+
+
+ UTF-8
+ 1.8
+ 1.8
+
+
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 2.3.8.RELEASE
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+ org.springframework.boot
+ spring-boot-starter-actuator
+
+
+ org.springframework.boot
+ spring-boot-devtools
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-security
+
+
+
+ org.springframework.boot
+ spring-boot-test
+
+
+
+
diff --git a/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/InsecureSpringActuatorConfig.expected b/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/InsecureSpringActuatorConfig.expected
new file mode 100644
index 00000000000..48630293985
--- /dev/null
+++ b/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/InsecureSpringActuatorConfig.expected
@@ -0,0 +1 @@
+| pom.xml:29:9:32:22 | dependency | Insecure configuration of Spring Boot Actuator exposes sensitive endpoints. |
diff --git a/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/InsecureSpringActuatorConfig.qlref b/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/InsecureSpringActuatorConfig.qlref
new file mode 100644
index 00000000000..ada54d34dc1
--- /dev/null
+++ b/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/InsecureSpringActuatorConfig.qlref
@@ -0,0 +1 @@
+experimental/Security/CWE/CWE-016/InsecureSpringActuatorConfig.ql
diff --git a/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/SensitiveInfo.java b/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/SensitiveInfo.java
new file mode 100644
index 00000000000..a3ff69c1b81
--- /dev/null
+++ b/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/SensitiveInfo.java
@@ -0,0 +1,13 @@
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RequestMapping;
+
+@Controller
+public class SensitiveInfo {
+ @RequestMapping
+ public void handleLogin(@RequestParam String username, @RequestParam String password) throws Exception {
+ if (!username.equals("") && password.equals("")) {
+ //Blank processing
+ }
+ }
+}
\ No newline at end of file
diff --git a/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/application.properties b/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/application.properties
new file mode 100644
index 00000000000..797906a3ca3
--- /dev/null
+++ b/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/application.properties
@@ -0,0 +1,14 @@
+#management.endpoints.web.base-path=/admin
+
+# vulnerable configuration (spring boot 1.0 - 1.4): exposes actuators by default
+
+# vulnerable configuration (spring boot 1.5+): requires value false to expose sensitive actuators
+management.security.enabled=false
+
+# vulnerable configuration (spring boot 2+): exposes health and info only by default, here overridden to expose everything
+management.endpoints.web.exposure.include=*
+management.endpoints.web.exposure.exclude=beans
+
+management.endpoint.shutdown.enabled=true
+
+management.endpoint.health.show-details=when_authorized
\ No newline at end of file
diff --git a/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/options b/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/options
new file mode 100644
index 00000000000..2ce7a4743cd
--- /dev/null
+++ b/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/options
@@ -0,0 +1 @@
+//semmle-extractor-options: --javac-args -cp ${testdir}/../../../../stubs/springframework-5.8.x
diff --git a/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/pom.xml b/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/pom.xml
new file mode 100644
index 00000000000..a9d5fa920c8
--- /dev/null
+++ b/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/pom.xml
@@ -0,0 +1,47 @@
+
+
+ 4.0.0
+
+ spring-boot-actuator-app
+ spring-boot-actuator-app
+ 1.0-SNAPSHOT
+
+
+ UTF-8
+ 1.8
+ 1.8
+
+
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 2.3.8.RELEASE
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+ org.springframework.boot
+ spring-boot-starter-actuator
+
+
+ org.springframework.boot
+ spring-boot-devtools
+
+
+
+ org.springframework.boot
+ spring-boot-test
+
+
+
+
\ No newline at end of file
From 0dbddbdf0f5787d8ea92bc6f6132447a110b5b91 Mon Sep 17 00:00:00 2001
From: Jami Cogswell
Date: Wed, 9 Jul 2025 16:46:30 -0400
Subject: [PATCH 036/984] Java: remove experimental files
---
.../InsecureSpringActuatorConfig.qhelp | 47 -------
.../CWE-016/InsecureSpringActuatorConfig.ql | 121 ------------------
.../CWE/CWE-016/application.properties | 22 ----
.../Security/CWE/CWE-016/pom_bad.xml | 50 --------
.../Security/CWE/CWE-016/pom_good.xml | 50 --------
.../InsecureSpringActuatorConfig.expected | 1 -
.../InsecureSpringActuatorConfig.qlref | 1 -
.../security/CWE-016/SensitiveInfo.java | 13 --
.../security/CWE-016/application.properties | 14 --
.../query-tests/security/CWE-016/options | 1 -
.../query-tests/security/CWE-016/pom.xml | 47 -------
11 files changed, 367 deletions(-)
delete mode 100644 java/ql/src/experimental/Security/CWE/CWE-016/InsecureSpringActuatorConfig.qhelp
delete mode 100644 java/ql/src/experimental/Security/CWE/CWE-016/InsecureSpringActuatorConfig.ql
delete mode 100644 java/ql/src/experimental/Security/CWE/CWE-016/application.properties
delete mode 100644 java/ql/src/experimental/Security/CWE/CWE-016/pom_bad.xml
delete mode 100644 java/ql/src/experimental/Security/CWE/CWE-016/pom_good.xml
delete mode 100644 java/ql/test/experimental/query-tests/security/CWE-016/InsecureSpringActuatorConfig.expected
delete mode 100644 java/ql/test/experimental/query-tests/security/CWE-016/InsecureSpringActuatorConfig.qlref
delete mode 100644 java/ql/test/experimental/query-tests/security/CWE-016/SensitiveInfo.java
delete mode 100644 java/ql/test/experimental/query-tests/security/CWE-016/application.properties
delete mode 100644 java/ql/test/experimental/query-tests/security/CWE-016/options
delete mode 100644 java/ql/test/experimental/query-tests/security/CWE-016/pom.xml
diff --git a/java/ql/src/experimental/Security/CWE/CWE-016/InsecureSpringActuatorConfig.qhelp b/java/ql/src/experimental/Security/CWE/CWE-016/InsecureSpringActuatorConfig.qhelp
deleted file mode 100644
index e201156728a..00000000000
--- a/java/ql/src/experimental/Security/CWE/CWE-016/InsecureSpringActuatorConfig.qhelp
+++ /dev/null
@@ -1,47 +0,0 @@
-
-
-
-
Spring Boot is a popular framework that facilitates the development of stand-alone applications
-and micro services. Spring Boot Actuator helps to expose production-ready support features against
-Spring Boot applications.
-
-
Endpoints of Spring Boot Actuator allow to monitor and interact with a Spring Boot application.
-Exposing unprotected actuator endpoints through configuration files can lead to information disclosure
-or even remote code execution vulnerability.
-
-
Rather than programmatically permitting endpoint requests or enforcing access control, frequently
-developers simply leave management endpoints publicly accessible in the application configuration file
-application.properties without enforcing access control through Spring Security.
-
-
-
-
Declare the Spring Boot Starter Security module in XML configuration or programmatically enforce
-security checks on management endpoints using Spring Security. Otherwise accessing management endpoints
-on a different HTTP port other than the port that the web application is listening on also helps to
-improve the security.
-
-
-
-
The following examples show both 'BAD' and 'GOOD' configurations. In the 'BAD' configuration,
-no security module is declared and sensitive management endpoints are exposed. In the 'GOOD' configuration,
-security is enforced and only endpoints requiring exposure are exposed.
-
-
diff --git a/java/ql/src/experimental/Security/CWE/CWE-016/InsecureSpringActuatorConfig.ql b/java/ql/src/experimental/Security/CWE/CWE-016/InsecureSpringActuatorConfig.ql
deleted file mode 100644
index b21aa82e8ba..00000000000
--- a/java/ql/src/experimental/Security/CWE/CWE-016/InsecureSpringActuatorConfig.ql
+++ /dev/null
@@ -1,121 +0,0 @@
-/**
- * @name Insecure Spring Boot Actuator Configuration
- * @description Exposed Spring Boot Actuator through configuration files without declarative or procedural
- * security enforcement leads to information leak or even remote code execution.
- * @kind problem
- * @problem.severity error
- * @precision high
- * @id java/insecure-spring-actuator-config
- * @tags security
- * experimental
- * external/cwe/cwe-016
- */
-
-/*
- * Note this query requires properties files to be indexed before it can produce results.
- * If creating your own database with the CodeQL CLI, you should run
- * `codeql database index-files --language=properties ...`
- * If using lgtm.com, you should add `properties_files: true` to the index block of your
- * lgtm.yml file (see https://lgtm.com/help/lgtm/java-extraction)
- */
-
-import java
-import semmle.code.configfiles.ConfigFiles
-import semmle.code.xml.MavenPom
-
-/** The parent node of the `org.springframework.boot` group. */
-class SpringBootParent extends Parent {
- SpringBootParent() { this.getGroup().getValue() = "org.springframework.boot" }
-}
-
-/** Class of Spring Boot dependencies. */
-class SpringBootPom extends Pom {
- SpringBootPom() { this.getParentElement() instanceof SpringBootParent }
-
- /** Holds if the Spring Boot Actuator module `spring-boot-starter-actuator` is used in the project. */
- predicate isSpringBootActuatorUsed() {
- this.getADependency().getArtifact().getValue() = "spring-boot-starter-actuator"
- }
-
- /**
- * Holds if the Spring Boot Security module is used in the project, which brings in other security
- * related libraries.
- */
- predicate isSpringBootSecurityUsed() {
- this.getADependency().getArtifact().getValue() = "spring-boot-starter-security"
- }
-}
-
-/** The properties file `application.properties`. */
-class ApplicationProperties extends ConfigPair {
- ApplicationProperties() { this.getFile().getBaseName() = "application.properties" }
-}
-
-/** The configuration property `management.security.enabled`. */
-class ManagementSecurityConfig extends ApplicationProperties {
- ManagementSecurityConfig() { this.getNameElement().getName() = "management.security.enabled" }
-
- /** Gets the whitespace-trimmed value of this property. */
- string getValue() { result = this.getValueElement().getValue().trim() }
-
- /** Holds if `management.security.enabled` is set to `false`. */
- predicate hasSecurityDisabled() { this.getValue() = "false" }
-
- /** Holds if `management.security.enabled` is set to `true`. */
- predicate hasSecurityEnabled() { this.getValue() = "true" }
-}
-
-/** The configuration property `management.endpoints.web.exposure.include`. */
-class ManagementEndPointInclude extends ApplicationProperties {
- ManagementEndPointInclude() {
- this.getNameElement().getName() = "management.endpoints.web.exposure.include"
- }
-
- /** Gets the whitespace-trimmed value of this property. */
- string getValue() { result = this.getValueElement().getValue().trim() }
-}
-
-/**
- * Holds if `ApplicationProperties` ap of a repository managed by `SpringBootPom` pom
- * has a vulnerable configuration of Spring Boot Actuator management endpoints.
- */
-predicate hasConfidentialEndPointExposed(SpringBootPom pom, ApplicationProperties ap) {
- pom.isSpringBootActuatorUsed() and
- not pom.isSpringBootSecurityUsed() and
- ap.getFile()
- .getParentContainer()
- .getAbsolutePath()
- .matches(pom.getFile().getParentContainer().getAbsolutePath() + "%") and // in the same sub-directory
- exists(string springBootVersion | springBootVersion = pom.getParentElement().getVersionString() |
- springBootVersion.regexpMatch("1\\.[0-4].*") and // version 1.0, 1.1, ..., 1.4
- not exists(ManagementSecurityConfig me |
- me.hasSecurityEnabled() and me.getFile() = ap.getFile()
- )
- or
- springBootVersion.matches("1.5%") and // version 1.5
- exists(ManagementSecurityConfig me | me.hasSecurityDisabled() and me.getFile() = ap.getFile())
- or
- springBootVersion.matches("2.%") and //version 2.x
- exists(ManagementEndPointInclude mi |
- mi.getFile() = ap.getFile() and
- (
- mi.getValue() = "*" // all endpoints are enabled
- or
- mi.getValue()
- .matches([
- "%dump%", "%trace%", "%logfile%", "%shutdown%", "%startup%", "%mappings%", "%env%",
- "%beans%", "%sessions%"
- ]) // confidential endpoints to check although all endpoints apart from '/health' and '/info' are considered sensitive by Spring
- )
- )
- )
-}
-
-deprecated query predicate problems(Dependency d, string message) {
- exists(SpringBootPom pom |
- hasConfidentialEndPointExposed(pom, _) and
- d = pom.getADependency() and
- d.getArtifact().getValue() = "spring-boot-starter-actuator"
- ) and
- message = "Insecure configuration of Spring Boot Actuator exposes sensitive endpoints."
-}
diff --git a/java/ql/src/experimental/Security/CWE/CWE-016/application.properties b/java/ql/src/experimental/Security/CWE/CWE-016/application.properties
deleted file mode 100644
index 4f5defdd948..00000000000
--- a/java/ql/src/experimental/Security/CWE/CWE-016/application.properties
+++ /dev/null
@@ -1,22 +0,0 @@
-#management.endpoints.web.base-path=/admin
-
-
-#### BAD: All management endpoints are accessible ####
-# vulnerable configuration (spring boot 1.0 - 1.4): exposes actuators by default
-
-# vulnerable configuration (spring boot 1.5+): requires value false to expose sensitive actuators
-management.security.enabled=false
-
-# vulnerable configuration (spring boot 2+): exposes health and info only by default, here overridden to expose everything
-management.endpoints.web.exposure.include=*
-
-
-#### GOOD: All management endpoints have access control ####
-# safe configuration (spring boot 1.0 - 1.4): exposes actuators by default
-management.security.enabled=true
-
-# safe configuration (spring boot 1.5+): requires value false to expose sensitive actuators
-management.security.enabled=true
-
-# safe configuration (spring boot 2+): exposes health and info only by default, here overridden to expose one additional endpoint which we assume is intentional and safe.
-management.endpoints.web.exposure.include=beans,info,health
diff --git a/java/ql/src/experimental/Security/CWE/CWE-016/pom_bad.xml b/java/ql/src/experimental/Security/CWE/CWE-016/pom_bad.xml
deleted file mode 100644
index 9dd5c9c188b..00000000000
--- a/java/ql/src/experimental/Security/CWE/CWE-016/pom_bad.xml
+++ /dev/null
@@ -1,50 +0,0 @@
-
-
- 4.0.0
-
- spring-boot-actuator-app
- spring-boot-actuator-app
- 1.0-SNAPSHOT
-
-
- UTF-8
- 1.8
- 1.8
-
-
-
- org.springframework.boot
- spring-boot-starter-parent
- 2.3.8.RELEASE
-
-
-
-
-
- org.springframework.boot
- spring-boot-starter-web
-
-
- org.springframework.boot
- spring-boot-starter-actuator
-
-
- org.springframework.boot
- spring-boot-devtools
-
-
-
-
-
-
- org.springframework.boot
- spring-boot-test
-
-
-
-
\ No newline at end of file
diff --git a/java/ql/src/experimental/Security/CWE/CWE-016/pom_good.xml b/java/ql/src/experimental/Security/CWE/CWE-016/pom_good.xml
deleted file mode 100644
index 89f577f21e5..00000000000
--- a/java/ql/src/experimental/Security/CWE/CWE-016/pom_good.xml
+++ /dev/null
@@ -1,50 +0,0 @@
-
-
- 4.0.0
-
- spring-boot-actuator-app
- spring-boot-actuator-app
- 1.0-SNAPSHOT
-
-
- UTF-8
- 1.8
- 1.8
-
-
-
- org.springframework.boot
- spring-boot-starter-parent
- 2.3.8.RELEASE
-
-
-
-
-
- org.springframework.boot
- spring-boot-starter-web
-
-
- org.springframework.boot
- spring-boot-starter-actuator
-
-
- org.springframework.boot
- spring-boot-devtools
-
-
-
-
- org.springframework.boot
- spring-boot-starter-security
-
-
-
- org.springframework.boot
- spring-boot-test
-
-
-
-
\ No newline at end of file
diff --git a/java/ql/test/experimental/query-tests/security/CWE-016/InsecureSpringActuatorConfig.expected b/java/ql/test/experimental/query-tests/security/CWE-016/InsecureSpringActuatorConfig.expected
deleted file mode 100644
index 48630293985..00000000000
--- a/java/ql/test/experimental/query-tests/security/CWE-016/InsecureSpringActuatorConfig.expected
+++ /dev/null
@@ -1 +0,0 @@
-| pom.xml:29:9:32:22 | dependency | Insecure configuration of Spring Boot Actuator exposes sensitive endpoints. |
diff --git a/java/ql/test/experimental/query-tests/security/CWE-016/InsecureSpringActuatorConfig.qlref b/java/ql/test/experimental/query-tests/security/CWE-016/InsecureSpringActuatorConfig.qlref
deleted file mode 100644
index 9cd12d5e4fb..00000000000
--- a/java/ql/test/experimental/query-tests/security/CWE-016/InsecureSpringActuatorConfig.qlref
+++ /dev/null
@@ -1 +0,0 @@
-experimental/Security/CWE/CWE-016/InsecureSpringActuatorConfig.ql
\ No newline at end of file
diff --git a/java/ql/test/experimental/query-tests/security/CWE-016/SensitiveInfo.java b/java/ql/test/experimental/query-tests/security/CWE-016/SensitiveInfo.java
deleted file mode 100644
index a3ff69c1b81..00000000000
--- a/java/ql/test/experimental/query-tests/security/CWE-016/SensitiveInfo.java
+++ /dev/null
@@ -1,13 +0,0 @@
-import org.springframework.stereotype.Controller;
-import org.springframework.web.bind.annotation.RequestParam;
-import org.springframework.web.bind.annotation.RequestMapping;
-
-@Controller
-public class SensitiveInfo {
- @RequestMapping
- public void handleLogin(@RequestParam String username, @RequestParam String password) throws Exception {
- if (!username.equals("") && password.equals("")) {
- //Blank processing
- }
- }
-}
\ No newline at end of file
diff --git a/java/ql/test/experimental/query-tests/security/CWE-016/application.properties b/java/ql/test/experimental/query-tests/security/CWE-016/application.properties
deleted file mode 100644
index 797906a3ca3..00000000000
--- a/java/ql/test/experimental/query-tests/security/CWE-016/application.properties
+++ /dev/null
@@ -1,14 +0,0 @@
-#management.endpoints.web.base-path=/admin
-
-# vulnerable configuration (spring boot 1.0 - 1.4): exposes actuators by default
-
-# vulnerable configuration (spring boot 1.5+): requires value false to expose sensitive actuators
-management.security.enabled=false
-
-# vulnerable configuration (spring boot 2+): exposes health and info only by default, here overridden to expose everything
-management.endpoints.web.exposure.include=*
-management.endpoints.web.exposure.exclude=beans
-
-management.endpoint.shutdown.enabled=true
-
-management.endpoint.health.show-details=when_authorized
\ No newline at end of file
diff --git a/java/ql/test/experimental/query-tests/security/CWE-016/options b/java/ql/test/experimental/query-tests/security/CWE-016/options
deleted file mode 100644
index 2ce7a4743cd..00000000000
--- a/java/ql/test/experimental/query-tests/security/CWE-016/options
+++ /dev/null
@@ -1 +0,0 @@
-//semmle-extractor-options: --javac-args -cp ${testdir}/../../../../stubs/springframework-5.8.x
diff --git a/java/ql/test/experimental/query-tests/security/CWE-016/pom.xml b/java/ql/test/experimental/query-tests/security/CWE-016/pom.xml
deleted file mode 100644
index a9d5fa920c8..00000000000
--- a/java/ql/test/experimental/query-tests/security/CWE-016/pom.xml
+++ /dev/null
@@ -1,47 +0,0 @@
-
-
- 4.0.0
-
- spring-boot-actuator-app
- spring-boot-actuator-app
- 1.0-SNAPSHOT
-
-
- UTF-8
- 1.8
- 1.8
-
-
-
- org.springframework.boot
- spring-boot-starter-parent
- 2.3.8.RELEASE
-
-
-
-
-
- org.springframework.boot
- spring-boot-starter-web
-
-
- org.springframework.boot
- spring-boot-starter-actuator
-
-
- org.springframework.boot
- spring-boot-devtools
-
-
-
- org.springframework.boot
- spring-boot-test
-
-
-
-
\ No newline at end of file
From 38260e76bfa271483123f330a644153b7ae5ef26 Mon Sep 17 00:00:00 2001
From: Jami Cogswell
Date: Thu, 10 Jul 2025 10:07:05 -0400
Subject: [PATCH 037/984] Java: remove deprecation
---
.../InsecureSpringActuatorConfig.ql | 14 ++++++--------
1 file changed, 6 insertions(+), 8 deletions(-)
diff --git a/java/ql/src/Security/CWE/CWE-200/InsecureSpringActuatorConfig/InsecureSpringActuatorConfig.ql b/java/ql/src/Security/CWE/CWE-200/InsecureSpringActuatorConfig/InsecureSpringActuatorConfig.ql
index b21aa82e8ba..800fc6db564 100644
--- a/java/ql/src/Security/CWE/CWE-200/InsecureSpringActuatorConfig/InsecureSpringActuatorConfig.ql
+++ b/java/ql/src/Security/CWE/CWE-200/InsecureSpringActuatorConfig/InsecureSpringActuatorConfig.ql
@@ -111,11 +111,9 @@ predicate hasConfidentialEndPointExposed(SpringBootPom pom, ApplicationPropertie
)
}
-deprecated query predicate problems(Dependency d, string message) {
- exists(SpringBootPom pom |
- hasConfidentialEndPointExposed(pom, _) and
- d = pom.getADependency() and
- d.getArtifact().getValue() = "spring-boot-starter-actuator"
- ) and
- message = "Insecure configuration of Spring Boot Actuator exposes sensitive endpoints."
-}
+from SpringBootPom pom, ApplicationProperties ap, Dependency d
+where
+ hasConfidentialEndPointExposed(pom, ap) and
+ d = pom.getADependency() and
+ d.getArtifact().getValue() = "spring-boot-starter-actuator"
+select d, "Insecure configuration of Spring Boot Actuator exposes sensitive endpoints."
From fc930d918463721587fdc02f1a494493e26a8487 Mon Sep 17 00:00:00 2001
From: Jami Cogswell
Date: Thu, 10 Jul 2025 10:32:02 -0400
Subject: [PATCH 038/984] Java: update tests for non-experimental directory
---
.../InsecureSpringActuatorConfig.qlref | 2 +-
.../CWE-200/semmle/tests/InsecureSpringActuatorConfig/options | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/InsecureSpringActuatorConfig.qlref b/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/InsecureSpringActuatorConfig.qlref
index ada54d34dc1..bf30c44df85 100644
--- a/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/InsecureSpringActuatorConfig.qlref
+++ b/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/InsecureSpringActuatorConfig.qlref
@@ -1 +1 @@
-experimental/Security/CWE/CWE-016/InsecureSpringActuatorConfig.ql
+Security/CWE/CWE-200/InsecureSpringActuatorConfig/InsecureSpringActuatorConfig.ql
diff --git a/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/options b/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/options
index 2ce7a4743cd..ab29fd4e46f 100644
--- a/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/options
+++ b/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/options
@@ -1 +1 @@
-//semmle-extractor-options: --javac-args -cp ${testdir}/../../../../stubs/springframework-5.8.x
+//semmle-extractor-options: --javac-args -cp ${testdir}/../../../../../../stubs/springframework-5.8.x
From ed8da5e151d29c127f0e099590af62ac6d310477 Mon Sep 17 00:00:00 2001
From: Jami Cogswell
Date: Mon, 14 Jul 2025 11:59:29 -0400
Subject: [PATCH 039/984] Java: convert tests to inline expectations
---
.../InsecureSpringActuatorConfig.qlref | 3 ++-
.../CWE-200/semmle/tests/InsecureSpringActuatorConfig/pom.xml | 2 +-
2 files changed, 3 insertions(+), 2 deletions(-)
diff --git a/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/InsecureSpringActuatorConfig.qlref b/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/InsecureSpringActuatorConfig.qlref
index bf30c44df85..b826de8eed3 100644
--- a/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/InsecureSpringActuatorConfig.qlref
+++ b/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/InsecureSpringActuatorConfig.qlref
@@ -1 +1,2 @@
-Security/CWE/CWE-200/InsecureSpringActuatorConfig/InsecureSpringActuatorConfig.ql
+query: Security/CWE/CWE-200/InsecureSpringActuatorConfig/InsecureSpringActuatorConfig.ql
+postprocess: utils/test/InlineExpectationsTestQuery.ql
diff --git a/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/pom.xml b/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/pom.xml
index a9d5fa920c8..105309271f8 100644
--- a/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/pom.xml
+++ b/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/pom.xml
@@ -29,7 +29,7 @@
org.springframework.bootspring-boot-starter-actuator
-
+
org.springframework.bootspring-boot-devtools
From b479f5c8dcbfc7e0cce817833496e076b0a9d2c3 Mon Sep 17 00:00:00 2001
From: Jami Cogswell
Date: Mon, 14 Jul 2025 10:06:24 -0400
Subject: [PATCH 040/984] Java: fix integration tests
---
.../java/query-suite/java-code-scanning.qls.expected | 1 +
.../java/query-suite/java-security-and-quality.qls.expected | 1 +
.../java/query-suite/java-security-extended.qls.expected | 1 +
.../java/query-suite/not_included_in_qls.expected | 1 -
4 files changed, 3 insertions(+), 1 deletion(-)
diff --git a/java/ql/integration-tests/java/query-suite/java-code-scanning.qls.expected b/java/ql/integration-tests/java/query-suite/java-code-scanning.qls.expected
index 3290e0d84b0..90b5b7ca491 100644
--- a/java/ql/integration-tests/java/query-suite/java-code-scanning.qls.expected
+++ b/java/ql/integration-tests/java/query-suite/java-code-scanning.qls.expected
@@ -26,6 +26,7 @@ ql/java/ql/src/Security/CWE/CWE-113/NettyResponseSplitting.ql
ql/java/ql/src/Security/CWE/CWE-113/ResponseSplitting.ql
ql/java/ql/src/Security/CWE/CWE-1204/StaticInitializationVector.ql
ql/java/ql/src/Security/CWE/CWE-134/ExternallyControlledFormatString.ql
+ql/java/ql/src/Security/CWE/CWE-200/InsecureSpringActuatorConfig/InsecureSpringActuatorConfig.ql
ql/java/ql/src/Security/CWE/CWE-200/SpringBootActuators.ql
ql/java/ql/src/Security/CWE/CWE-209/SensitiveDataExposureThroughErrorMessage.ql
ql/java/ql/src/Security/CWE/CWE-209/StackTraceExposure.ql
diff --git a/java/ql/integration-tests/java/query-suite/java-security-and-quality.qls.expected b/java/ql/integration-tests/java/query-suite/java-security-and-quality.qls.expected
index f4317f8e2a5..b203ea23a62 100644
--- a/java/ql/integration-tests/java/query-suite/java-security-and-quality.qls.expected
+++ b/java/ql/integration-tests/java/query-suite/java-security-and-quality.qls.expected
@@ -142,6 +142,7 @@ ql/java/ql/src/Security/CWE/CWE-200/AndroidSensitiveNotifications.ql
ql/java/ql/src/Security/CWE/CWE-200/AndroidSensitiveTextField.ql
ql/java/ql/src/Security/CWE/CWE-200/AndroidWebViewSettingsAllowsContentAccess.ql
ql/java/ql/src/Security/CWE/CWE-200/AndroidWebViewSettingsFileAccess.ql
+ql/java/ql/src/Security/CWE/CWE-200/InsecureSpringActuatorConfig/InsecureSpringActuatorConfig.ql
ql/java/ql/src/Security/CWE/CWE-200/SpringBootActuators.ql
ql/java/ql/src/Security/CWE/CWE-200/TempDirLocalInformationDisclosure.ql
ql/java/ql/src/Security/CWE/CWE-209/SensitiveDataExposureThroughErrorMessage.ql
diff --git a/java/ql/integration-tests/java/query-suite/java-security-extended.qls.expected b/java/ql/integration-tests/java/query-suite/java-security-extended.qls.expected
index 209777cf4d9..c7dac907a96 100644
--- a/java/ql/integration-tests/java/query-suite/java-security-extended.qls.expected
+++ b/java/ql/integration-tests/java/query-suite/java-security-extended.qls.expected
@@ -45,6 +45,7 @@ ql/java/ql/src/Security/CWE/CWE-200/AndroidSensitiveNotifications.ql
ql/java/ql/src/Security/CWE/CWE-200/AndroidSensitiveTextField.ql
ql/java/ql/src/Security/CWE/CWE-200/AndroidWebViewSettingsAllowsContentAccess.ql
ql/java/ql/src/Security/CWE/CWE-200/AndroidWebViewSettingsFileAccess.ql
+ql/java/ql/src/Security/CWE/CWE-200/InsecureSpringActuatorConfig/InsecureSpringActuatorConfig.ql
ql/java/ql/src/Security/CWE/CWE-200/SpringBootActuators.ql
ql/java/ql/src/Security/CWE/CWE-200/TempDirLocalInformationDisclosure.ql
ql/java/ql/src/Security/CWE/CWE-209/SensitiveDataExposureThroughErrorMessage.ql
diff --git a/java/ql/integration-tests/java/query-suite/not_included_in_qls.expected b/java/ql/integration-tests/java/query-suite/not_included_in_qls.expected
index 1f58e51ad80..304c0387323 100644
--- a/java/ql/integration-tests/java/query-suite/not_included_in_qls.expected
+++ b/java/ql/integration-tests/java/query-suite/not_included_in_qls.expected
@@ -196,7 +196,6 @@ ql/java/ql/src/Violations of Best Practice/legacy/ParameterAssignment.ql
ql/java/ql/src/Violations of Best Practice/legacy/UnnecessaryCast.ql
ql/java/ql/src/Violations of Best Practice/legacy/UnnecessaryImport.ql
ql/java/ql/src/definitions.ql
-ql/java/ql/src/experimental/Security/CWE/CWE-016/InsecureSpringActuatorConfig.ql
ql/java/ql/src/experimental/Security/CWE/CWE-020/Log4jJndiInjection.ql
ql/java/ql/src/experimental/Security/CWE/CWE-036/OpenStream.ql
ql/java/ql/src/experimental/Security/CWE/CWE-073/FilePathInjection.ql
From 1b90a30d458aec0aee191ae3a6acbccb0a6b0eab Mon Sep 17 00:00:00 2001
From: Jami Cogswell
Date: Tue, 15 Jul 2025 11:13:02 -0400
Subject: [PATCH 041/984] Java: move code to .qll file
---
.../SpringBootActuatorsConfigQuery.qll | 93 ++++++++++++++++++
.../InsecureSpringActuatorConfig.ql | 98 +------------------
2 files changed, 94 insertions(+), 97 deletions(-)
create mode 100644 java/ql/lib/semmle/code/java/security/SpringBootActuatorsConfigQuery.qll
diff --git a/java/ql/lib/semmle/code/java/security/SpringBootActuatorsConfigQuery.qll b/java/ql/lib/semmle/code/java/security/SpringBootActuatorsConfigQuery.qll
new file mode 100644
index 00000000000..5cf54f3436c
--- /dev/null
+++ b/java/ql/lib/semmle/code/java/security/SpringBootActuatorsConfigQuery.qll
@@ -0,0 +1,93 @@
+/** Provides classes and predicates to reason about Spring Boot actuators exposed in configuration files. */
+
+import java
+private import semmle.code.configfiles.ConfigFiles
+private import semmle.code.xml.MavenPom
+
+/** The parent node of the `org.springframework.boot` group. */
+class SpringBootParent extends Parent {
+ SpringBootParent() { this.getGroup().getValue() = "org.springframework.boot" }
+}
+
+/** Class of Spring Boot dependencies. */
+class SpringBootPom extends Pom {
+ SpringBootPom() { this.getParentElement() instanceof SpringBootParent }
+
+ /** Holds if the Spring Boot Actuator module `spring-boot-starter-actuator` is used in the project. */
+ predicate isSpringBootActuatorUsed() {
+ this.getADependency().getArtifact().getValue() = "spring-boot-starter-actuator"
+ }
+
+ /**
+ * Holds if the Spring Boot Security module is used in the project, which brings in other security
+ * related libraries.
+ */
+ predicate isSpringBootSecurityUsed() {
+ this.getADependency().getArtifact().getValue() = "spring-boot-starter-security"
+ }
+}
+
+/** The properties file `application.properties`. */
+class ApplicationProperties extends ConfigPair {
+ ApplicationProperties() { this.getFile().getBaseName() = "application.properties" }
+}
+
+/** The configuration property `management.security.enabled`. */
+class ManagementSecurityConfig extends ApplicationProperties {
+ ManagementSecurityConfig() { this.getNameElement().getName() = "management.security.enabled" }
+
+ /** Gets the whitespace-trimmed value of this property. */
+ string getValue() { result = this.getValueElement().getValue().trim() }
+
+ /** Holds if `management.security.enabled` is set to `false`. */
+ predicate hasSecurityDisabled() { this.getValue() = "false" }
+
+ /** Holds if `management.security.enabled` is set to `true`. */
+ predicate hasSecurityEnabled() { this.getValue() = "true" }
+}
+
+/** The configuration property `management.endpoints.web.exposure.include`. */
+class ManagementEndPointInclude extends ApplicationProperties {
+ ManagementEndPointInclude() {
+ this.getNameElement().getName() = "management.endpoints.web.exposure.include"
+ }
+
+ /** Gets the whitespace-trimmed value of this property. */
+ string getValue() { result = this.getValueElement().getValue().trim() }
+}
+
+/**
+ * Holds if `ApplicationProperties` ap of a repository managed by `SpringBootPom` pom
+ * has a vulnerable configuration of Spring Boot Actuator management endpoints.
+ */
+predicate hasConfidentialEndPointExposed(SpringBootPom pom, ApplicationProperties ap) {
+ pom.isSpringBootActuatorUsed() and
+ not pom.isSpringBootSecurityUsed() and
+ ap.getFile()
+ .getParentContainer()
+ .getAbsolutePath()
+ .matches(pom.getFile().getParentContainer().getAbsolutePath() + "%") and // in the same sub-directory
+ exists(string springBootVersion | springBootVersion = pom.getParentElement().getVersionString() |
+ springBootVersion.regexpMatch("1\\.[0-4].*") and // version 1.0, 1.1, ..., 1.4
+ not exists(ManagementSecurityConfig me |
+ me.hasSecurityEnabled() and me.getFile() = ap.getFile()
+ )
+ or
+ springBootVersion.matches("1.5%") and // version 1.5
+ exists(ManagementSecurityConfig me | me.hasSecurityDisabled() and me.getFile() = ap.getFile())
+ or
+ springBootVersion.matches("2.%") and //version 2.x
+ exists(ManagementEndPointInclude mi |
+ mi.getFile() = ap.getFile() and
+ (
+ mi.getValue() = "*" // all endpoints are enabled
+ or
+ mi.getValue()
+ .matches([
+ "%dump%", "%trace%", "%logfile%", "%shutdown%", "%startup%", "%mappings%", "%env%",
+ "%beans%", "%sessions%"
+ ]) // confidential endpoints to check although all endpoints apart from '/health' and '/info' are considered sensitive by Spring
+ )
+ )
+ )
+}
diff --git a/java/ql/src/Security/CWE/CWE-200/InsecureSpringActuatorConfig/InsecureSpringActuatorConfig.ql b/java/ql/src/Security/CWE/CWE-200/InsecureSpringActuatorConfig/InsecureSpringActuatorConfig.ql
index 800fc6db564..66d9a52c2cf 100644
--- a/java/ql/src/Security/CWE/CWE-200/InsecureSpringActuatorConfig/InsecureSpringActuatorConfig.ql
+++ b/java/ql/src/Security/CWE/CWE-200/InsecureSpringActuatorConfig/InsecureSpringActuatorConfig.ql
@@ -11,105 +11,9 @@
* external/cwe/cwe-016
*/
-/*
- * Note this query requires properties files to be indexed before it can produce results.
- * If creating your own database with the CodeQL CLI, you should run
- * `codeql database index-files --language=properties ...`
- * If using lgtm.com, you should add `properties_files: true` to the index block of your
- * lgtm.yml file (see https://lgtm.com/help/lgtm/java-extraction)
- */
-
import java
-import semmle.code.configfiles.ConfigFiles
import semmle.code.xml.MavenPom
-
-/** The parent node of the `org.springframework.boot` group. */
-class SpringBootParent extends Parent {
- SpringBootParent() { this.getGroup().getValue() = "org.springframework.boot" }
-}
-
-/** Class of Spring Boot dependencies. */
-class SpringBootPom extends Pom {
- SpringBootPom() { this.getParentElement() instanceof SpringBootParent }
-
- /** Holds if the Spring Boot Actuator module `spring-boot-starter-actuator` is used in the project. */
- predicate isSpringBootActuatorUsed() {
- this.getADependency().getArtifact().getValue() = "spring-boot-starter-actuator"
- }
-
- /**
- * Holds if the Spring Boot Security module is used in the project, which brings in other security
- * related libraries.
- */
- predicate isSpringBootSecurityUsed() {
- this.getADependency().getArtifact().getValue() = "spring-boot-starter-security"
- }
-}
-
-/** The properties file `application.properties`. */
-class ApplicationProperties extends ConfigPair {
- ApplicationProperties() { this.getFile().getBaseName() = "application.properties" }
-}
-
-/** The configuration property `management.security.enabled`. */
-class ManagementSecurityConfig extends ApplicationProperties {
- ManagementSecurityConfig() { this.getNameElement().getName() = "management.security.enabled" }
-
- /** Gets the whitespace-trimmed value of this property. */
- string getValue() { result = this.getValueElement().getValue().trim() }
-
- /** Holds if `management.security.enabled` is set to `false`. */
- predicate hasSecurityDisabled() { this.getValue() = "false" }
-
- /** Holds if `management.security.enabled` is set to `true`. */
- predicate hasSecurityEnabled() { this.getValue() = "true" }
-}
-
-/** The configuration property `management.endpoints.web.exposure.include`. */
-class ManagementEndPointInclude extends ApplicationProperties {
- ManagementEndPointInclude() {
- this.getNameElement().getName() = "management.endpoints.web.exposure.include"
- }
-
- /** Gets the whitespace-trimmed value of this property. */
- string getValue() { result = this.getValueElement().getValue().trim() }
-}
-
-/**
- * Holds if `ApplicationProperties` ap of a repository managed by `SpringBootPom` pom
- * has a vulnerable configuration of Spring Boot Actuator management endpoints.
- */
-predicate hasConfidentialEndPointExposed(SpringBootPom pom, ApplicationProperties ap) {
- pom.isSpringBootActuatorUsed() and
- not pom.isSpringBootSecurityUsed() and
- ap.getFile()
- .getParentContainer()
- .getAbsolutePath()
- .matches(pom.getFile().getParentContainer().getAbsolutePath() + "%") and // in the same sub-directory
- exists(string springBootVersion | springBootVersion = pom.getParentElement().getVersionString() |
- springBootVersion.regexpMatch("1\\.[0-4].*") and // version 1.0, 1.1, ..., 1.4
- not exists(ManagementSecurityConfig me |
- me.hasSecurityEnabled() and me.getFile() = ap.getFile()
- )
- or
- springBootVersion.matches("1.5%") and // version 1.5
- exists(ManagementSecurityConfig me | me.hasSecurityDisabled() and me.getFile() = ap.getFile())
- or
- springBootVersion.matches("2.%") and //version 2.x
- exists(ManagementEndPointInclude mi |
- mi.getFile() = ap.getFile() and
- (
- mi.getValue() = "*" // all endpoints are enabled
- or
- mi.getValue()
- .matches([
- "%dump%", "%trace%", "%logfile%", "%shutdown%", "%startup%", "%mappings%", "%env%",
- "%beans%", "%sessions%"
- ]) // confidential endpoints to check although all endpoints apart from '/health' and '/info' are considered sensitive by Spring
- )
- )
- )
-}
+import semmle.code.java.security.SpringBootActuatorsConfigQuery
from SpringBootPom pom, ApplicationProperties ap, Dependency d
where
From 3823186dc6dc53c87fdd143fbf6d7d95dbbe4e8e Mon Sep 17 00:00:00 2001
From: Jami Cogswell
Date: Tue, 15 Jul 2025 19:21:21 -0400
Subject: [PATCH 042/984] Java: split tests by versions
splitting is required to properly test each scenario
---
.../InsecureSpringActuatorConfig.expected | 7 ++-
.../bad/default/application.properties | 1 +
.../{ => Version1.4-/bad/default}/pom.xml | 2 +-
.../bad/false/application.properties | 2 +
.../Version1.4-/bad/false/pom.xml | 47 +++++++++++++++++++
.../Version1.4-/good/application.properties | 2 +
.../Version1.4-/good/pom.xml | 47 +++++++++++++++++++
.../Version1.5/bad/application.properties | 2 +
.../Version1.5/bad/pom.xml | 47 +++++++++++++++++++
.../Version1.5/good/application.properties | 2 +
.../Version1.5/good/pom.xml | 47 +++++++++++++++++++
.../{ => Version2+}/application.properties | 0
.../Version2+/bad/application.properties | 7 +++
.../Version2+/bad/pom.xml | 47 +++++++++++++++++++
.../Version2+/good/application.properties | 2 +
.../Version2+/good/pom.xml | 47 +++++++++++++++++++
16 files changed, 307 insertions(+), 2 deletions(-)
create mode 100644 java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version1.4-/bad/default/application.properties
rename java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/{ => Version1.4-/bad/default}/pom.xml (97%)
create mode 100644 java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version1.4-/bad/false/application.properties
create mode 100644 java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version1.4-/bad/false/pom.xml
create mode 100644 java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version1.4-/good/application.properties
create mode 100644 java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version1.4-/good/pom.xml
create mode 100644 java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version1.5/bad/application.properties
create mode 100644 java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version1.5/bad/pom.xml
create mode 100644 java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version1.5/good/application.properties
create mode 100644 java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version1.5/good/pom.xml
rename java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/{ => Version2+}/application.properties (100%)
create mode 100644 java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version2+/bad/application.properties
create mode 100644 java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version2+/bad/pom.xml
create mode 100644 java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version2+/good/application.properties
create mode 100644 java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version2+/good/pom.xml
diff --git a/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/InsecureSpringActuatorConfig.expected b/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/InsecureSpringActuatorConfig.expected
index 48630293985..da7a570f982 100644
--- a/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/InsecureSpringActuatorConfig.expected
+++ b/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/InsecureSpringActuatorConfig.expected
@@ -1 +1,6 @@
-| pom.xml:29:9:32:22 | dependency | Insecure configuration of Spring Boot Actuator exposes sensitive endpoints. |
+#select
+| Version1.4-/bad/false/pom.xml:29:9:32:22 | dependency | Insecure configuration of Spring Boot Actuator exposes sensitive endpoints. |
+| Version1.5/bad/pom.xml:29:9:32:22 | dependency | Insecure configuration of Spring Boot Actuator exposes sensitive endpoints. |
+| Version2+/bad/pom.xml:29:9:32:22 | dependency | Insecure configuration of Spring Boot Actuator exposes sensitive endpoints. |
+testFailures
+| Version1.4-/bad/default/pom.xml:32:23:32:39 | $ Alert | Missing result: Alert |
diff --git a/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version1.4-/bad/default/application.properties b/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version1.4-/bad/default/application.properties
new file mode 100644
index 00000000000..a41bbc9fdca
--- /dev/null
+++ b/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version1.4-/bad/default/application.properties
@@ -0,0 +1 @@
+# vulnerable configuration (spring boot 1.0 - 1.4): exposes actuators by default
\ No newline at end of file
diff --git a/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/pom.xml b/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version1.4-/bad/default/pom.xml
similarity index 97%
rename from java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/pom.xml
rename to java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version1.4-/bad/default/pom.xml
index 105309271f8..83c7d2685f3 100644
--- a/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/pom.xml
+++ b/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version1.4-/bad/default/pom.xml
@@ -17,7 +17,7 @@
org.springframework.bootspring-boot-starter-parent
- 2.3.8.RELEASE
+ 1.2.6.RELEASE
diff --git a/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version1.4-/bad/false/application.properties b/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version1.4-/bad/false/application.properties
new file mode 100644
index 00000000000..621b859214c
--- /dev/null
+++ b/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version1.4-/bad/false/application.properties
@@ -0,0 +1,2 @@
+# vulnerable configuration (spring boot 1.0 - 1.4): exposes actuators by default
+management.security.enabled=false
\ No newline at end of file
diff --git a/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version1.4-/bad/false/pom.xml b/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version1.4-/bad/false/pom.xml
new file mode 100644
index 00000000000..83c7d2685f3
--- /dev/null
+++ b/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version1.4-/bad/false/pom.xml
@@ -0,0 +1,47 @@
+
+
+ 4.0.0
+
+ spring-boot-actuator-app
+ spring-boot-actuator-app
+ 1.0-SNAPSHOT
+
+
+ UTF-8
+ 1.8
+ 1.8
+
+
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 1.2.6.RELEASE
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+ org.springframework.boot
+ spring-boot-starter-actuator
+
+
+ org.springframework.boot
+ spring-boot-devtools
+
+
+
+ org.springframework.boot
+ spring-boot-test
+
+
+
+
\ No newline at end of file
diff --git a/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version1.4-/good/application.properties b/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version1.4-/good/application.properties
new file mode 100644
index 00000000000..6cadc4c756d
--- /dev/null
+++ b/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version1.4-/good/application.properties
@@ -0,0 +1,2 @@
+# safe configuration (spring boot 1.0 - 1.4): exposes actuators by default
+management.security.enabled=true
\ No newline at end of file
diff --git a/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version1.4-/good/pom.xml b/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version1.4-/good/pom.xml
new file mode 100644
index 00000000000..452d4b69c35
--- /dev/null
+++ b/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version1.4-/good/pom.xml
@@ -0,0 +1,47 @@
+
+
+ 4.0.0
+
+ spring-boot-actuator-app
+ spring-boot-actuator-app
+ 1.0-SNAPSHOT
+
+
+ UTF-8
+ 1.8
+ 1.8
+
+
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 1.2.6.RELEASE
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+ org.springframework.boot
+ spring-boot-starter-actuator
+
+
+ org.springframework.boot
+ spring-boot-devtools
+
+
+ org.springframework.boot
+ spring-boot-starter-security
+
+
+ org.springframework.boot
+ spring-boot-test
+
+
+
+
\ No newline at end of file
diff --git a/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version1.5/bad/application.properties b/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version1.5/bad/application.properties
new file mode 100644
index 00000000000..f1e8f6587d0
--- /dev/null
+++ b/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version1.5/bad/application.properties
@@ -0,0 +1,2 @@
+# safe configuration (spring boot 1.5+): requires value false to expose sensitive actuators
+management.security.enabled=false
\ No newline at end of file
diff --git a/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version1.5/bad/pom.xml b/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version1.5/bad/pom.xml
new file mode 100644
index 00000000000..aa1a4bcaf05
--- /dev/null
+++ b/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version1.5/bad/pom.xml
@@ -0,0 +1,47 @@
+
+
+ 4.0.0
+
+ spring-boot-actuator-app
+ spring-boot-actuator-app
+ 1.0-SNAPSHOT
+
+
+ UTF-8
+ 1.8
+ 1.8
+
+
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 1.5.6.RELEASE
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+ org.springframework.boot
+ spring-boot-starter-actuator
+
+
+ org.springframework.boot
+ spring-boot-devtools
+
+
+
+ org.springframework.boot
+ spring-boot-test
+
+
+
+
\ No newline at end of file
diff --git a/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version1.5/good/application.properties b/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version1.5/good/application.properties
new file mode 100644
index 00000000000..bec45a22b82
--- /dev/null
+++ b/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version1.5/good/application.properties
@@ -0,0 +1,2 @@
+# vulnerable configuration (spring boot 1.5+): requires value false to expose sensitive actuators
+management.security.enabled=true
\ No newline at end of file
diff --git a/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version1.5/good/pom.xml b/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version1.5/good/pom.xml
new file mode 100644
index 00000000000..39b46bef7e4
--- /dev/null
+++ b/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version1.5/good/pom.xml
@@ -0,0 +1,47 @@
+
+
+ 4.0.0
+
+ spring-boot-actuator-app
+ spring-boot-actuator-app
+ 1.0-SNAPSHOT
+
+
+ UTF-8
+ 1.8
+ 1.8
+
+
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 1.5.6.RELEASE
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+ org.springframework.boot
+ spring-boot-starter-actuator
+
+
+ org.springframework.boot
+ spring-boot-devtools
+
+
+ org.springframework.boot
+ spring-boot-starter-security
+
+
+ org.springframework.boot
+ spring-boot-test
+
+
+
+
\ No newline at end of file
diff --git a/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/application.properties b/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version2+/application.properties
similarity index 100%
rename from java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/application.properties
rename to java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version2+/application.properties
diff --git a/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version2+/bad/application.properties b/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version2+/bad/application.properties
new file mode 100644
index 00000000000..a2e73d7022c
--- /dev/null
+++ b/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version2+/bad/application.properties
@@ -0,0 +1,7 @@
+# vulnerable configuration (spring boot 2+): exposes health and info only by default, here overridden to expose everything
+management.endpoints.web.exposure.include=*
+management.endpoints.web.exposure.exclude=beans
+
+management.endpoint.shutdown.enabled=true
+
+management.endpoint.health.show-details=when_authorized
\ No newline at end of file
diff --git a/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version2+/bad/pom.xml b/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version2+/bad/pom.xml
new file mode 100644
index 00000000000..c22f08d7e7e
--- /dev/null
+++ b/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version2+/bad/pom.xml
@@ -0,0 +1,47 @@
+
+
+ 4.0.0
+
+ spring-boot-actuator-app
+ spring-boot-actuator-app
+ 1.0-SNAPSHOT
+
+
+ UTF-8
+ 1.8
+ 1.8
+
+
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 2.2.6.RELEASE
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+ org.springframework.boot
+ spring-boot-starter-actuator
+
+
+ org.springframework.boot
+ spring-boot-devtools
+
+
+
+ org.springframework.boot
+ spring-boot-test
+
+
+
+
\ No newline at end of file
diff --git a/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version2+/good/application.properties b/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version2+/good/application.properties
new file mode 100644
index 00000000000..c14bf64b13b
--- /dev/null
+++ b/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version2+/good/application.properties
@@ -0,0 +1,2 @@
+# safe configuration (spring boot 2+): exposes health and info only by default, here overridden to expose one additional endpoint which we assume is intentional and safe.
+management.endpoints.web.exposure.include=beans,info,health
\ No newline at end of file
diff --git a/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version2+/good/pom.xml b/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version2+/good/pom.xml
new file mode 100644
index 00000000000..e65ebf04701
--- /dev/null
+++ b/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version2+/good/pom.xml
@@ -0,0 +1,47 @@
+
+
+ 4.0.0
+
+ spring-boot-actuator-app
+ spring-boot-actuator-app
+ 1.0-SNAPSHOT
+
+
+ UTF-8
+ 1.8
+ 1.8
+
+
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 2.2.6.RELEASE
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+ org.springframework.boot
+ spring-boot-starter-actuator
+
+
+ org.springframework.boot
+ spring-boot-devtools
+
+
+ org.springframework.boot
+ spring-boot-starter-security
+
+
+ org.springframework.boot
+ spring-boot-test
+
+
+
+
\ No newline at end of file
From 2bfc4b4ee207a23905eb9ce64bc84b735d83a77f Mon Sep 17 00:00:00 2001
From: Jami Cogswell
Date: Tue, 15 Jul 2025 19:50:04 -0400
Subject: [PATCH 043/984] Java: fix test case for version 1.4
Need the existence of an ApplicationProperties File, not an ApplicationProperties ConfigPair
---
.../SpringBootActuatorsConfigQuery.qll | 65 ++++++++++---------
.../InsecureSpringActuatorConfig.ql | 4 +-
.../InsecureSpringActuatorConfig.expected | 4 +-
3 files changed, 39 insertions(+), 34 deletions(-)
diff --git a/java/ql/lib/semmle/code/java/security/SpringBootActuatorsConfigQuery.qll b/java/ql/lib/semmle/code/java/security/SpringBootActuatorsConfigQuery.qll
index 5cf54f3436c..241b64821e8 100644
--- a/java/ql/lib/semmle/code/java/security/SpringBootActuatorsConfigQuery.qll
+++ b/java/ql/lib/semmle/code/java/security/SpringBootActuatorsConfigQuery.qll
@@ -28,12 +28,17 @@ class SpringBootPom extends Pom {
}
/** The properties file `application.properties`. */
-class ApplicationProperties extends ConfigPair {
- ApplicationProperties() { this.getFile().getBaseName() = "application.properties" }
+class ApplicationPropertiesFile extends File {
+ ApplicationPropertiesFile() { this.getBaseName() = "application.properties" }
+}
+
+/** A name-value pair stored in an `application.properties` file. */
+class ApplicationPropertiesConfigPair extends ConfigPair {
+ ApplicationPropertiesConfigPair() { this.getFile() instanceof ApplicationPropertiesFile }
}
/** The configuration property `management.security.enabled`. */
-class ManagementSecurityConfig extends ApplicationProperties {
+class ManagementSecurityConfig extends ApplicationPropertiesConfigPair {
ManagementSecurityConfig() { this.getNameElement().getName() = "management.security.enabled" }
/** Gets the whitespace-trimmed value of this property. */
@@ -47,7 +52,7 @@ class ManagementSecurityConfig extends ApplicationProperties {
}
/** The configuration property `management.endpoints.web.exposure.include`. */
-class ManagementEndPointInclude extends ApplicationProperties {
+class ManagementEndPointInclude extends ApplicationPropertiesConfigPair {
ManagementEndPointInclude() {
this.getNameElement().getName() = "management.endpoints.web.exposure.include"
}
@@ -60,33 +65,35 @@ class ManagementEndPointInclude extends ApplicationProperties {
* Holds if `ApplicationProperties` ap of a repository managed by `SpringBootPom` pom
* has a vulnerable configuration of Spring Boot Actuator management endpoints.
*/
-predicate hasConfidentialEndPointExposed(SpringBootPom pom, ApplicationProperties ap) {
+predicate hasConfidentialEndPointExposed(SpringBootPom pom) {
pom.isSpringBootActuatorUsed() and
not pom.isSpringBootSecurityUsed() and
- ap.getFile()
- .getParentContainer()
- .getAbsolutePath()
- .matches(pom.getFile().getParentContainer().getAbsolutePath() + "%") and // in the same sub-directory
- exists(string springBootVersion | springBootVersion = pom.getParentElement().getVersionString() |
- springBootVersion.regexpMatch("1\\.[0-4].*") and // version 1.0, 1.1, ..., 1.4
- not exists(ManagementSecurityConfig me |
- me.hasSecurityEnabled() and me.getFile() = ap.getFile()
- )
- or
- springBootVersion.matches("1.5%") and // version 1.5
- exists(ManagementSecurityConfig me | me.hasSecurityDisabled() and me.getFile() = ap.getFile())
- or
- springBootVersion.matches("2.%") and //version 2.x
- exists(ManagementEndPointInclude mi |
- mi.getFile() = ap.getFile() and
- (
- mi.getValue() = "*" // all endpoints are enabled
- or
- mi.getValue()
- .matches([
- "%dump%", "%trace%", "%logfile%", "%shutdown%", "%startup%", "%mappings%", "%env%",
- "%beans%", "%sessions%"
- ]) // confidential endpoints to check although all endpoints apart from '/health' and '/info' are considered sensitive by Spring
+ exists(ApplicationPropertiesFile apFile |
+ apFile
+ .getParentContainer()
+ .getAbsolutePath()
+ .matches(pom.getFile().getParentContainer().getAbsolutePath() + "%") and // in the same sub-directory
+ exists(string springBootVersion |
+ springBootVersion = pom.getParentElement().getVersionString()
+ |
+ springBootVersion.regexpMatch("1\\.[0-4].*") and // version 1.0, 1.1, ..., 1.4
+ not exists(ManagementSecurityConfig me | me.hasSecurityEnabled() and me.getFile() = apFile)
+ or
+ springBootVersion.matches("1.5%") and // version 1.5
+ exists(ManagementSecurityConfig me | me.hasSecurityDisabled() and me.getFile() = apFile)
+ or
+ springBootVersion.matches("2.%") and //version 2.x
+ exists(ManagementEndPointInclude mi |
+ mi.getFile() = apFile and
+ (
+ mi.getValue() = "*" // all endpoints are enabled
+ or
+ mi.getValue()
+ .matches([
+ "%dump%", "%trace%", "%logfile%", "%shutdown%", "%startup%", "%mappings%",
+ "%env%", "%beans%", "%sessions%"
+ ]) // confidential endpoints to check although all endpoints apart from '/health' and '/info' are considered sensitive by Spring
+ )
)
)
)
diff --git a/java/ql/src/Security/CWE/CWE-200/InsecureSpringActuatorConfig/InsecureSpringActuatorConfig.ql b/java/ql/src/Security/CWE/CWE-200/InsecureSpringActuatorConfig/InsecureSpringActuatorConfig.ql
index 66d9a52c2cf..89f3777f0c2 100644
--- a/java/ql/src/Security/CWE/CWE-200/InsecureSpringActuatorConfig/InsecureSpringActuatorConfig.ql
+++ b/java/ql/src/Security/CWE/CWE-200/InsecureSpringActuatorConfig/InsecureSpringActuatorConfig.ql
@@ -15,9 +15,9 @@ import java
import semmle.code.xml.MavenPom
import semmle.code.java.security.SpringBootActuatorsConfigQuery
-from SpringBootPom pom, ApplicationProperties ap, Dependency d
+from SpringBootPom pom, Dependency d
where
- hasConfidentialEndPointExposed(pom, ap) and
+ hasConfidentialEndPointExposed(pom) and
d = pom.getADependency() and
d.getArtifact().getValue() = "spring-boot-starter-actuator"
select d, "Insecure configuration of Spring Boot Actuator exposes sensitive endpoints."
diff --git a/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/InsecureSpringActuatorConfig.expected b/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/InsecureSpringActuatorConfig.expected
index da7a570f982..d7043f403fb 100644
--- a/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/InsecureSpringActuatorConfig.expected
+++ b/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/InsecureSpringActuatorConfig.expected
@@ -1,6 +1,4 @@
-#select
+| Version1.4-/bad/default/pom.xml:29:9:32:22 | dependency | Insecure configuration of Spring Boot Actuator exposes sensitive endpoints. |
| Version1.4-/bad/false/pom.xml:29:9:32:22 | dependency | Insecure configuration of Spring Boot Actuator exposes sensitive endpoints. |
| Version1.5/bad/pom.xml:29:9:32:22 | dependency | Insecure configuration of Spring Boot Actuator exposes sensitive endpoints. |
| Version2+/bad/pom.xml:29:9:32:22 | dependency | Insecure configuration of Spring Boot Actuator exposes sensitive endpoints. |
-testFailures
-| Version1.4-/bad/default/pom.xml:32:23:32:39 | $ Alert | Missing result: Alert |
From ae163a9f36c0a3d08f6c78404a438bfc7101cf96 Mon Sep 17 00:00:00 2001
From: Jami Cogswell
Date: Tue, 15 Jul 2025 20:02:30 -0400
Subject: [PATCH 044/984] Java: add overlay annotations
---
.../code/java/security/SpringBootActuatorsConfigQuery.qll | 2 ++
1 file changed, 2 insertions(+)
diff --git a/java/ql/lib/semmle/code/java/security/SpringBootActuatorsConfigQuery.qll b/java/ql/lib/semmle/code/java/security/SpringBootActuatorsConfigQuery.qll
index 241b64821e8..ccae3a4f929 100644
--- a/java/ql/lib/semmle/code/java/security/SpringBootActuatorsConfigQuery.qll
+++ b/java/ql/lib/semmle/code/java/security/SpringBootActuatorsConfigQuery.qll
@@ -1,4 +1,6 @@
/** Provides classes and predicates to reason about Spring Boot actuators exposed in configuration files. */
+overlay[local?]
+module;
import java
private import semmle.code.configfiles.ConfigFiles
From 0d2a4222fd14fd2290b462d990efa10026d7efb7 Mon Sep 17 00:00:00 2001
From: Jami Cogswell
Date: Tue, 15 Jul 2025 21:45:50 -0400
Subject: [PATCH 045/984] Java: add related location to alert message
---
.../SpringBootActuatorsConfigQuery.qll | 41 +++++++++++++++----
.../InsecureSpringActuatorConfig.ql | 8 ++--
.../InsecureSpringActuatorConfig.expected | 8 ++--
3 files changed, 43 insertions(+), 14 deletions(-)
diff --git a/java/ql/lib/semmle/code/java/security/SpringBootActuatorsConfigQuery.qll b/java/ql/lib/semmle/code/java/security/SpringBootActuatorsConfigQuery.qll
index ccae3a4f929..f8ff20f9978 100644
--- a/java/ql/lib/semmle/code/java/security/SpringBootActuatorsConfigQuery.qll
+++ b/java/ql/lib/semmle/code/java/security/SpringBootActuatorsConfigQuery.qll
@@ -48,9 +48,6 @@ class ManagementSecurityConfig extends ApplicationPropertiesConfigPair {
/** Holds if `management.security.enabled` is set to `false`. */
predicate hasSecurityDisabled() { this.getValue() = "false" }
-
- /** Holds if `management.security.enabled` is set to `true`. */
- predicate hasSecurityEnabled() { this.getValue() = "true" }
}
/** The configuration property `management.endpoints.web.exposure.include`. */
@@ -63,11 +60,37 @@ class ManagementEndPointInclude extends ApplicationPropertiesConfigPair {
string getValue() { result = this.getValueElement().getValue().trim() }
}
+private newtype TOption =
+ TNone() or
+ TSome(ApplicationPropertiesConfigPair ap)
+
+/**
+ * An option type that is either a singleton `None` or a `Some` wrapping
+ * the `ApplicationPropertiesConfigPair` type.
+ */
+class ApplicationPropertiesOption extends TOption {
+ /** Gets a textual representation of this element. */
+ string toString() {
+ this = TNone() and result = "(none)"
+ or
+ result = this.asSome().toString()
+ }
+
+ /** Gets the location of this element. */
+ Location getLocation() { result = this.asSome().getLocation() }
+
+ /** Gets the wrapped element, if any. */
+ ApplicationPropertiesConfigPair asSome() { this = TSome(result) }
+
+ /** Holds if this option is the singleton `None`. */
+ predicate isNone() { this = TNone() }
+}
+
/**
* Holds if `ApplicationProperties` ap of a repository managed by `SpringBootPom` pom
* has a vulnerable configuration of Spring Boot Actuator management endpoints.
*/
-predicate hasConfidentialEndPointExposed(SpringBootPom pom) {
+predicate hasConfidentialEndPointExposed(SpringBootPom pom, ApplicationPropertiesOption apOption) {
pom.isSpringBootActuatorUsed() and
not pom.isSpringBootSecurityUsed() and
exists(ApplicationPropertiesFile apFile |
@@ -79,14 +102,18 @@ predicate hasConfidentialEndPointExposed(SpringBootPom pom) {
springBootVersion = pom.getParentElement().getVersionString()
|
springBootVersion.regexpMatch("1\\.[0-4].*") and // version 1.0, 1.1, ..., 1.4
- not exists(ManagementSecurityConfig me | me.hasSecurityEnabled() and me.getFile() = apFile)
+ not exists(ManagementSecurityConfig me | me.getFile() = apFile) and
+ apOption.isNone()
or
- springBootVersion.matches("1.5%") and // version 1.5
- exists(ManagementSecurityConfig me | me.hasSecurityDisabled() and me.getFile() = apFile)
+ springBootVersion.regexpMatch("1\\.[0-5].*") and // version 1.0, 1.1, ..., 1.5
+ exists(ManagementSecurityConfig me |
+ me.hasSecurityDisabled() and me.getFile() = apFile and me = apOption.asSome()
+ )
or
springBootVersion.matches("2.%") and //version 2.x
exists(ManagementEndPointInclude mi |
mi.getFile() = apFile and
+ mi = apOption.asSome() and
(
mi.getValue() = "*" // all endpoints are enabled
or
diff --git a/java/ql/src/Security/CWE/CWE-200/InsecureSpringActuatorConfig/InsecureSpringActuatorConfig.ql b/java/ql/src/Security/CWE/CWE-200/InsecureSpringActuatorConfig/InsecureSpringActuatorConfig.ql
index 89f3777f0c2..2437a77953d 100644
--- a/java/ql/src/Security/CWE/CWE-200/InsecureSpringActuatorConfig/InsecureSpringActuatorConfig.ql
+++ b/java/ql/src/Security/CWE/CWE-200/InsecureSpringActuatorConfig/InsecureSpringActuatorConfig.ql
@@ -15,9 +15,11 @@ import java
import semmle.code.xml.MavenPom
import semmle.code.java.security.SpringBootActuatorsConfigQuery
-from SpringBootPom pom, Dependency d
+from SpringBootPom pom, Dependency d, ApplicationPropertiesOption apOption
where
- hasConfidentialEndPointExposed(pom) and
+ hasConfidentialEndPointExposed(pom, apOption) and
d = pom.getADependency() and
d.getArtifact().getValue() = "spring-boot-starter-actuator"
-select d, "Insecure configuration of Spring Boot Actuator exposes sensitive endpoints."
+select d,
+ "Insecure $@ of Spring Boot Actuator exposes sensitive endpoints (" +
+ pom.getParentElement().getVersionString() + ").", apOption, "configuration"
diff --git a/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/InsecureSpringActuatorConfig.expected b/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/InsecureSpringActuatorConfig.expected
index d7043f403fb..70a6068ab3f 100644
--- a/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/InsecureSpringActuatorConfig.expected
+++ b/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/InsecureSpringActuatorConfig.expected
@@ -1,4 +1,4 @@
-| Version1.4-/bad/default/pom.xml:29:9:32:22 | dependency | Insecure configuration of Spring Boot Actuator exposes sensitive endpoints. |
-| Version1.4-/bad/false/pom.xml:29:9:32:22 | dependency | Insecure configuration of Spring Boot Actuator exposes sensitive endpoints. |
-| Version1.5/bad/pom.xml:29:9:32:22 | dependency | Insecure configuration of Spring Boot Actuator exposes sensitive endpoints. |
-| Version2+/bad/pom.xml:29:9:32:22 | dependency | Insecure configuration of Spring Boot Actuator exposes sensitive endpoints. |
+| Version1.4-/bad/default/pom.xml:29:9:32:22 | dependency | Insecure $@ of Spring Boot Actuator exposes sensitive endpoints (1.2.6.RELEASE). | file://:0:0:0:0 | (none) | configuration |
+| Version1.4-/bad/false/pom.xml:29:9:32:22 | dependency | Insecure $@ of Spring Boot Actuator exposes sensitive endpoints (1.2.6.RELEASE). | Version1.4-/bad/false/application.properties:2:1:2:33 | management.security.enabled=false | configuration |
+| Version1.5/bad/pom.xml:29:9:32:22 | dependency | Insecure $@ of Spring Boot Actuator exposes sensitive endpoints (1.5.6.RELEASE). | Version1.5/bad/application.properties:2:1:2:33 | management.security.enabled=false | configuration |
+| Version2+/bad/pom.xml:29:9:32:22 | dependency | Insecure $@ of Spring Boot Actuator exposes sensitive endpoints (2.2.6.RELEASE). | Version2+/bad/application.properties:2:1:2:43 | management.endpoints.web.exposure.include=* | configuration |
From afa6610cb9978b6a283e5c8dc9700781bf062d6f Mon Sep 17 00:00:00 2001
From: Jami Cogswell
Date: Thu, 17 Jul 2025 11:00:49 -0400
Subject: [PATCH 046/984] Java: update qhelp
---
.../InsecureSpringActuatorConfig.qhelp | 44 +++++++---------
.../application.properties | 22 --------
.../application_bad.properties | 10 ++++
.../application_good.properties | 11 ++++
.../InsecureSpringActuatorConfig/pom_bad.xml | 50 -------------------
.../InsecureSpringActuatorConfig/pom_good.xml | 42 +---------------
6 files changed, 41 insertions(+), 138 deletions(-)
delete mode 100644 java/ql/src/Security/CWE/CWE-200/InsecureSpringActuatorConfig/application.properties
create mode 100644 java/ql/src/Security/CWE/CWE-200/InsecureSpringActuatorConfig/application_bad.properties
create mode 100644 java/ql/src/Security/CWE/CWE-200/InsecureSpringActuatorConfig/application_good.properties
delete mode 100644 java/ql/src/Security/CWE/CWE-200/InsecureSpringActuatorConfig/pom_bad.xml
diff --git a/java/ql/src/Security/CWE/CWE-200/InsecureSpringActuatorConfig/InsecureSpringActuatorConfig.qhelp b/java/ql/src/Security/CWE/CWE-200/InsecureSpringActuatorConfig/InsecureSpringActuatorConfig.qhelp
index 7e31b43ba7a..d3e79e88ed7 100644
--- a/java/ql/src/Security/CWE/CWE-200/InsecureSpringActuatorConfig/InsecureSpringActuatorConfig.qhelp
+++ b/java/ql/src/Security/CWE/CWE-200/InsecureSpringActuatorConfig/InsecureSpringActuatorConfig.qhelp
@@ -1,43 +1,35 @@
-
Spring Boot is a popular framework that facilitates the development of stand-alone applications
-and micro services. Spring Boot Actuator helps to expose production-ready support features against
-Spring Boot applications.
-
-
Endpoints of Spring Boot Actuator allow to monitor and interact with a Spring Boot application.
-Exposing unprotected actuator endpoints through configuration files can lead to information disclosure
-or even remote code execution vulnerability.
-
-
Rather than programmatically permitting endpoint requests or enforcing access control, frequently
-developers simply leave management endpoints publicly accessible in the application configuration file
-application.properties without enforcing access control through Spring Security.
+
Spring Boot includes features called actuators that let you monitor and interact with your web
+ application. Exposing unprotected actuator endpoints through configuration files can lead to
+ information disclosure or even to remote code execution.
-
Declare the Spring Boot Starter Security module in XML configuration or programmatically enforce
-security checks on management endpoints using Spring Security. Otherwise accessing management endpoints
-on a different HTTP port other than the port that the web application is listening on also helps to
-improve the security.
+
Since actuator endpoints may contain sensitive information, carefully consider when to expose them,
+ and secure them as you would any sensitive URL. If you need to expose actuator endpoints, use Spring
+ Security, which secures actuators by default, or define a custom security configuration.
+
-
The following examples show both 'BAD' and 'GOOD' configurations. In the 'BAD' configuration,
-no security module is declared and sensitive management endpoints are exposed. In the 'GOOD' configuration,
-security is enforced and only endpoints requiring exposure are exposed.
+
The following examples show application.properties configurations that expose sensitive
+ actuator endpoints.
+
+
+
The below configurations ensure that sensitive actuator endpoints are not exposed.
+
+
+
To use Spring Security, which secures actuators by default, add the spring-boot-starter-security
+ dependency in your Maven pom.xml file.
HackerOne Report:
diff --git a/java/ql/src/Security/CWE/CWE-200/InsecureSpringActuatorConfig/application.properties b/java/ql/src/Security/CWE/CWE-200/InsecureSpringActuatorConfig/application.properties
deleted file mode 100644
index 441d752508c..00000000000
--- a/java/ql/src/Security/CWE/CWE-200/InsecureSpringActuatorConfig/application.properties
+++ /dev/null
@@ -1,22 +0,0 @@
-#management.endpoints.web.base-path=/admin
-
-
-#### BAD: All management endpoints are accessible ####
-# vulnerable configuration (spring boot 1.0 - 1.4): exposes actuators by default
-
-# vulnerable configuration (spring boot 1.5+): requires value false to expose sensitive actuators
-management.security.enabled=false
-
-# vulnerable configuration (spring boot 2+): exposes health and info only by default, here overridden to expose everything
-management.endpoints.web.exposure.include=*
-
-
-#### GOOD: All management endpoints have access control ####
-# safe configuration (spring boot 1.0 - 1.4): exposes actuators by default
-management.security.enabled=true
-
-# safe configuration (spring boot 1.5+): requires value false to expose sensitive actuators
-management.security.enabled=true
-
-# safe configuration (spring boot 2+): exposes health and info only by default, here overridden to expose one additional endpoint which we assume is intentional and safe.
-management.endpoints.web.exposure.include=beans,info,health
diff --git a/java/ql/src/Security/CWE/CWE-200/InsecureSpringActuatorConfig/application_bad.properties b/java/ql/src/Security/CWE/CWE-200/InsecureSpringActuatorConfig/application_bad.properties
new file mode 100644
index 00000000000..ccf1cb67881
--- /dev/null
+++ b/java/ql/src/Security/CWE/CWE-200/InsecureSpringActuatorConfig/application_bad.properties
@@ -0,0 +1,10 @@
+# vulnerable configuration (Spring Boot 1.0 - 1.4): exposes endpoints by default
+
+# vulnerable configuration (Spring Boot 1.5): false value exposes endpoints
+management.security.enabled=false
+
+# vulnerable configuration (Spring Boot 2.x): exposes all endpoints
+management.endpoints.web.exposure.include=*
+
+# vulnerable configuration (Spring Boot 3.x): exposes all endpoints
+management.endpoints.web.exposure.include=*
diff --git a/java/ql/src/Security/CWE/CWE-200/InsecureSpringActuatorConfig/application_good.properties b/java/ql/src/Security/CWE/CWE-200/InsecureSpringActuatorConfig/application_good.properties
new file mode 100644
index 00000000000..1af2b7b0228
--- /dev/null
+++ b/java/ql/src/Security/CWE/CWE-200/InsecureSpringActuatorConfig/application_good.properties
@@ -0,0 +1,11 @@
+# safe configuration (Spring Boot 1.0 - 1.4)
+management.security.enabled=true
+
+# safe configuration (Spring Boot 1.5+)
+management.security.enabled=true
+
+# safe configuration (Spring Boot 2.x): exposes health and info only by default
+management.endpoints.web.exposure.include=health,info
+
+# safe configuration (Spring Boot 3.x): exposes health only by default
+management.endpoints.web.exposure.include=health
diff --git a/java/ql/src/Security/CWE/CWE-200/InsecureSpringActuatorConfig/pom_bad.xml b/java/ql/src/Security/CWE/CWE-200/InsecureSpringActuatorConfig/pom_bad.xml
deleted file mode 100644
index 6bca2829ac4..00000000000
--- a/java/ql/src/Security/CWE/CWE-200/InsecureSpringActuatorConfig/pom_bad.xml
+++ /dev/null
@@ -1,50 +0,0 @@
-
-
- 4.0.0
-
- spring-boot-actuator-app
- spring-boot-actuator-app
- 1.0-SNAPSHOT
-
-
- UTF-8
- 1.8
- 1.8
-
-
-
- org.springframework.boot
- spring-boot-starter-parent
- 2.3.8.RELEASE
-
-
-
-
-
- org.springframework.boot
- spring-boot-starter-web
-
-
- org.springframework.boot
- spring-boot-starter-actuator
-
-
- org.springframework.boot
- spring-boot-devtools
-
-
-
-
-
-
- org.springframework.boot
- spring-boot-test
-
-
-
-
diff --git a/java/ql/src/Security/CWE/CWE-200/InsecureSpringActuatorConfig/pom_good.xml b/java/ql/src/Security/CWE/CWE-200/InsecureSpringActuatorConfig/pom_good.xml
index 03bc257f5bd..32fad44591e 100644
--- a/java/ql/src/Security/CWE/CWE-200/InsecureSpringActuatorConfig/pom_good.xml
+++ b/java/ql/src/Security/CWE/CWE-200/InsecureSpringActuatorConfig/pom_good.xml
@@ -1,50 +1,12 @@
-
-
- 4.0.0
-
- spring-boot-actuator-app
- spring-boot-actuator-app
- 1.0-SNAPSHOT
-
-
- UTF-8
- 1.8
- 1.8
-
-
-
- org.springframework.boot
- spring-boot-starter-parent
- 2.3.8.RELEASE
-
-
-
-
-
- org.springframework.boot
- spring-boot-starter-web
-
+...
org.springframework.bootspring-boot-starter-actuator
-
- org.springframework.boot
- spring-boot-devtools
- org.springframework.bootspring-boot-starter-security
-
-
- org.springframework.boot
- spring-boot-test
-
-
-
-
+...
From ea35fbbe3b0183ca22e94f5a7b4c0d96513c9cd4 Mon Sep 17 00:00:00 2001
From: Jami Cogswell
Date: Thu, 17 Jul 2025 11:21:17 -0400
Subject: [PATCH 047/984] Java: support version 3.x
---
.../SpringBootActuatorsConfigQuery.qll | 4 +-
.../InsecureSpringActuatorConfig.expected | 9 ++--
.../bad/default/application.properties | 0
.../bad/default/pom.xml | 0
.../bad/false/application.properties | 0
.../bad/false/pom.xml | 0
.../good/application.properties | 0
.../good/pom.xml | 0
.../bad/application.properties | 0
.../{Version1.5 => Version1.5.x}/bad/pom.xml | 0
.../good/application.properties | 0
.../{Version1.5 => Version1.5.x}/good/pom.xml | 0
.../Version2+/application.properties | 14 ------
.../Version2+/bad/application.properties | 7 ---
.../Version2+/good/application.properties | 2 -
.../Version2.x/bad/application.properties | 2 +
.../{Version2+ => Version2.x}/bad/pom.xml | 0
.../Version2.x/good/application.properties | 2 +
.../{Version2+ => Version2.x}/good/pom.xml | 0
.../Version3.x/bad/application.properties | 2 +
.../Version3.x/bad/pom.xml | 47 +++++++++++++++++++
.../Version3.x/good/application.properties | 2 +
.../Version3.x/good/pom.xml | 47 +++++++++++++++++++
23 files changed, 109 insertions(+), 29 deletions(-)
rename java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/{Version1.4- => Version1.0.x-1.4.x}/bad/default/application.properties (100%)
rename java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/{Version1.4- => Version1.0.x-1.4.x}/bad/default/pom.xml (100%)
rename java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/{Version1.4- => Version1.0.x-1.4.x}/bad/false/application.properties (100%)
rename java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/{Version1.4- => Version1.0.x-1.4.x}/bad/false/pom.xml (100%)
rename java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/{Version1.4- => Version1.0.x-1.4.x}/good/application.properties (100%)
rename java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/{Version1.4- => Version1.0.x-1.4.x}/good/pom.xml (100%)
rename java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/{Version1.5 => Version1.5.x}/bad/application.properties (100%)
rename java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/{Version1.5 => Version1.5.x}/bad/pom.xml (100%)
rename java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/{Version1.5 => Version1.5.x}/good/application.properties (100%)
rename java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/{Version1.5 => Version1.5.x}/good/pom.xml (100%)
delete mode 100644 java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version2+/application.properties
delete mode 100644 java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version2+/bad/application.properties
delete mode 100644 java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version2+/good/application.properties
create mode 100644 java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version2.x/bad/application.properties
rename java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/{Version2+ => Version2.x}/bad/pom.xml (100%)
create mode 100644 java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version2.x/good/application.properties
rename java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/{Version2+ => Version2.x}/good/pom.xml (100%)
create mode 100644 java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version3.x/bad/application.properties
create mode 100644 java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version3.x/bad/pom.xml
create mode 100644 java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version3.x/good/application.properties
create mode 100644 java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version3.x/good/pom.xml
diff --git a/java/ql/lib/semmle/code/java/security/SpringBootActuatorsConfigQuery.qll b/java/ql/lib/semmle/code/java/security/SpringBootActuatorsConfigQuery.qll
index f8ff20f9978..be78380ad3c 100644
--- a/java/ql/lib/semmle/code/java/security/SpringBootActuatorsConfigQuery.qll
+++ b/java/ql/lib/semmle/code/java/security/SpringBootActuatorsConfigQuery.qll
@@ -110,7 +110,7 @@ predicate hasConfidentialEndPointExposed(SpringBootPom pom, ApplicationPropertie
me.hasSecurityDisabled() and me.getFile() = apFile and me = apOption.asSome()
)
or
- springBootVersion.matches("2.%") and //version 2.x
+ springBootVersion.matches(["2.%", "3.%"]) and //version 2.x and 3.x
exists(ManagementEndPointInclude mi |
mi.getFile() = apFile and
mi = apOption.asSome() and
@@ -121,7 +121,7 @@ predicate hasConfidentialEndPointExposed(SpringBootPom pom, ApplicationPropertie
.matches([
"%dump%", "%trace%", "%logfile%", "%shutdown%", "%startup%", "%mappings%",
"%env%", "%beans%", "%sessions%"
- ]) // confidential endpoints to check although all endpoints apart from '/health' and '/info' are considered sensitive by Spring
+ ]) // confidential endpoints to check although all endpoints apart from '/health' are considered sensitive by Spring
)
)
)
diff --git a/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/InsecureSpringActuatorConfig.expected b/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/InsecureSpringActuatorConfig.expected
index 70a6068ab3f..5b29b16b1be 100644
--- a/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/InsecureSpringActuatorConfig.expected
+++ b/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/InsecureSpringActuatorConfig.expected
@@ -1,4 +1,5 @@
-| Version1.4-/bad/default/pom.xml:29:9:32:22 | dependency | Insecure $@ of Spring Boot Actuator exposes sensitive endpoints (1.2.6.RELEASE). | file://:0:0:0:0 | (none) | configuration |
-| Version1.4-/bad/false/pom.xml:29:9:32:22 | dependency | Insecure $@ of Spring Boot Actuator exposes sensitive endpoints (1.2.6.RELEASE). | Version1.4-/bad/false/application.properties:2:1:2:33 | management.security.enabled=false | configuration |
-| Version1.5/bad/pom.xml:29:9:32:22 | dependency | Insecure $@ of Spring Boot Actuator exposes sensitive endpoints (1.5.6.RELEASE). | Version1.5/bad/application.properties:2:1:2:33 | management.security.enabled=false | configuration |
-| Version2+/bad/pom.xml:29:9:32:22 | dependency | Insecure $@ of Spring Boot Actuator exposes sensitive endpoints (2.2.6.RELEASE). | Version2+/bad/application.properties:2:1:2:43 | management.endpoints.web.exposure.include=* | configuration |
+| Version1.0.x-1.4.x/bad/default/pom.xml:29:9:32:22 | dependency | Insecure Spring Boot actuator $@ exposes sensitive endpoints (1.2.6.RELEASE). | file://:0:0:0:0 | (none) | configuration |
+| Version1.0.x-1.4.x/bad/false/pom.xml:29:9:32:22 | dependency | Insecure Spring Boot actuator $@ exposes sensitive endpoints (1.2.6.RELEASE). | Version1.0.x-1.4.x/bad/false/application.properties:2:1:2:33 | management.security.enabled=false | configuration |
+| Version1.5.x/bad/pom.xml:29:9:32:22 | dependency | Insecure Spring Boot actuator $@ exposes sensitive endpoints (1.5.6.RELEASE). | Version1.5.x/bad/application.properties:2:1:2:33 | management.security.enabled=false | configuration |
+| Version2.x/bad/pom.xml:29:9:32:22 | dependency | Insecure Spring Boot actuator $@ exposes sensitive endpoints (2.2.6.RELEASE). | Version2.x/bad/application.properties:2:1:2:43 | management.endpoints.web.exposure.include=* | configuration |
+| Version3.x/bad/pom.xml:29:9:32:22 | dependency | Insecure Spring Boot actuator $@ exposes sensitive endpoints (3.3.5). | Version3.x/bad/application.properties:2:1:2:43 | management.endpoints.web.exposure.include=* | configuration |
diff --git a/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version1.4-/bad/default/application.properties b/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version1.0.x-1.4.x/bad/default/application.properties
similarity index 100%
rename from java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version1.4-/bad/default/application.properties
rename to java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version1.0.x-1.4.x/bad/default/application.properties
diff --git a/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version1.4-/bad/default/pom.xml b/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version1.0.x-1.4.x/bad/default/pom.xml
similarity index 100%
rename from java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version1.4-/bad/default/pom.xml
rename to java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version1.0.x-1.4.x/bad/default/pom.xml
diff --git a/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version1.4-/bad/false/application.properties b/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version1.0.x-1.4.x/bad/false/application.properties
similarity index 100%
rename from java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version1.4-/bad/false/application.properties
rename to java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version1.0.x-1.4.x/bad/false/application.properties
diff --git a/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version1.4-/bad/false/pom.xml b/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version1.0.x-1.4.x/bad/false/pom.xml
similarity index 100%
rename from java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version1.4-/bad/false/pom.xml
rename to java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version1.0.x-1.4.x/bad/false/pom.xml
diff --git a/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version1.4-/good/application.properties b/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version1.0.x-1.4.x/good/application.properties
similarity index 100%
rename from java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version1.4-/good/application.properties
rename to java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version1.0.x-1.4.x/good/application.properties
diff --git a/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version1.4-/good/pom.xml b/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version1.0.x-1.4.x/good/pom.xml
similarity index 100%
rename from java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version1.4-/good/pom.xml
rename to java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version1.0.x-1.4.x/good/pom.xml
diff --git a/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version1.5/bad/application.properties b/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version1.5.x/bad/application.properties
similarity index 100%
rename from java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version1.5/bad/application.properties
rename to java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version1.5.x/bad/application.properties
diff --git a/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version1.5/bad/pom.xml b/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version1.5.x/bad/pom.xml
similarity index 100%
rename from java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version1.5/bad/pom.xml
rename to java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version1.5.x/bad/pom.xml
diff --git a/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version1.5/good/application.properties b/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version1.5.x/good/application.properties
similarity index 100%
rename from java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version1.5/good/application.properties
rename to java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version1.5.x/good/application.properties
diff --git a/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version1.5/good/pom.xml b/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version1.5.x/good/pom.xml
similarity index 100%
rename from java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version1.5/good/pom.xml
rename to java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version1.5.x/good/pom.xml
diff --git a/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version2+/application.properties b/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version2+/application.properties
deleted file mode 100644
index 797906a3ca3..00000000000
--- a/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version2+/application.properties
+++ /dev/null
@@ -1,14 +0,0 @@
-#management.endpoints.web.base-path=/admin
-
-# vulnerable configuration (spring boot 1.0 - 1.4): exposes actuators by default
-
-# vulnerable configuration (spring boot 1.5+): requires value false to expose sensitive actuators
-management.security.enabled=false
-
-# vulnerable configuration (spring boot 2+): exposes health and info only by default, here overridden to expose everything
-management.endpoints.web.exposure.include=*
-management.endpoints.web.exposure.exclude=beans
-
-management.endpoint.shutdown.enabled=true
-
-management.endpoint.health.show-details=when_authorized
\ No newline at end of file
diff --git a/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version2+/bad/application.properties b/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version2+/bad/application.properties
deleted file mode 100644
index a2e73d7022c..00000000000
--- a/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version2+/bad/application.properties
+++ /dev/null
@@ -1,7 +0,0 @@
-# vulnerable configuration (spring boot 2+): exposes health and info only by default, here overridden to expose everything
-management.endpoints.web.exposure.include=*
-management.endpoints.web.exposure.exclude=beans
-
-management.endpoint.shutdown.enabled=true
-
-management.endpoint.health.show-details=when_authorized
\ No newline at end of file
diff --git a/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version2+/good/application.properties b/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version2+/good/application.properties
deleted file mode 100644
index c14bf64b13b..00000000000
--- a/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version2+/good/application.properties
+++ /dev/null
@@ -1,2 +0,0 @@
-# safe configuration (spring boot 2+): exposes health and info only by default, here overridden to expose one additional endpoint which we assume is intentional and safe.
-management.endpoints.web.exposure.include=beans,info,health
\ No newline at end of file
diff --git a/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version2.x/bad/application.properties b/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version2.x/bad/application.properties
new file mode 100644
index 00000000000..bbc1915b05e
--- /dev/null
+++ b/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version2.x/bad/application.properties
@@ -0,0 +1,2 @@
+# vulnerable configuration (spring boot 2+): exposes health and info only by default, here overridden to expose everything
+management.endpoints.web.exposure.include=*
\ No newline at end of file
diff --git a/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version2+/bad/pom.xml b/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version2.x/bad/pom.xml
similarity index 100%
rename from java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version2+/bad/pom.xml
rename to java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version2.x/bad/pom.xml
diff --git a/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version2.x/good/application.properties b/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version2.x/good/application.properties
new file mode 100644
index 00000000000..f7e0c1b43ac
--- /dev/null
+++ b/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version2.x/good/application.properties
@@ -0,0 +1,2 @@
+# safe configuration (spring boot 2+): exposes health and info only by default
+management.endpoints.web.exposure.include=info,health
\ No newline at end of file
diff --git a/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version2+/good/pom.xml b/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version2.x/good/pom.xml
similarity index 100%
rename from java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version2+/good/pom.xml
rename to java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version2.x/good/pom.xml
diff --git a/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version3.x/bad/application.properties b/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version3.x/bad/application.properties
new file mode 100644
index 00000000000..c5570065bae
--- /dev/null
+++ b/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version3.x/bad/application.properties
@@ -0,0 +1,2 @@
+# vulnerable configuration (spring boot 3+): exposes health only by default, here overridden to expose everything
+management.endpoints.web.exposure.include=*
\ No newline at end of file
diff --git a/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version3.x/bad/pom.xml b/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version3.x/bad/pom.xml
new file mode 100644
index 00000000000..12dab1d9421
--- /dev/null
+++ b/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version3.x/bad/pom.xml
@@ -0,0 +1,47 @@
+
+
+ 4.0.0
+
+ spring-boot-actuator-app
+ spring-boot-actuator-app
+ 1.0-SNAPSHOT
+
+
+ UTF-8
+ 1.8
+ 1.8
+
+
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 3.3.5
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+ org.springframework.boot
+ spring-boot-starter-actuator
+
+
+ org.springframework.boot
+ spring-boot-devtools
+
+
+
+ org.springframework.boot
+ spring-boot-test
+
+
+
+
\ No newline at end of file
diff --git a/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version3.x/good/application.properties b/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version3.x/good/application.properties
new file mode 100644
index 00000000000..8ba56eadc35
--- /dev/null
+++ b/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version3.x/good/application.properties
@@ -0,0 +1,2 @@
+# safe configuration (spring boot 3+): exposes health only by default.
+management.endpoints.web.exposure.include=health
\ No newline at end of file
diff --git a/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version3.x/good/pom.xml b/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version3.x/good/pom.xml
new file mode 100644
index 00000000000..a8103e681e4
--- /dev/null
+++ b/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version3.x/good/pom.xml
@@ -0,0 +1,47 @@
+
+
+ 4.0.0
+
+ spring-boot-actuator-app
+ spring-boot-actuator-app
+ 1.0-SNAPSHOT
+
+
+ UTF-8
+ 1.8
+ 1.8
+
+
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 3.3.5
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+ org.springframework.boot
+ spring-boot-starter-actuator
+
+
+ org.springframework.boot
+ spring-boot-devtools
+
+
+ org.springframework.boot
+ spring-boot-starter-security
+
+
+ org.springframework.boot
+ spring-boot-test
+
+
+
+
\ No newline at end of file
From 7d5e939a8604db18981a694d5a27369807474adc Mon Sep 17 00:00:00 2001
From: Jami Cogswell
Date: Thu, 17 Jul 2025 16:57:53 -0400
Subject: [PATCH 048/984] Java: minor refactoring
---
.../semmle/code/configfiles/ConfigFiles.qll | 7 +-
.../SpringBootActuatorsConfigQuery.qll | 86 +++++++++----------
.../InsecureSpringActuatorConfig.ql | 10 +--
3 files changed, 51 insertions(+), 52 deletions(-)
diff --git a/java/ql/lib/semmle/code/configfiles/ConfigFiles.qll b/java/ql/lib/semmle/code/configfiles/ConfigFiles.qll
index 0c69f45c56f..1655ed2d648 100644
--- a/java/ql/lib/semmle/code/configfiles/ConfigFiles.qll
+++ b/java/ql/lib/semmle/code/configfiles/ConfigFiles.qll
@@ -70,7 +70,12 @@ class ConfigValue extends @configValue, ConfigLocatable {
override string toString() { result = this.getValue() }
}
+/** A `.properties` file. */
+class PropertiesFile extends File {
+ PropertiesFile() { this.getExtension() = "properties" }
+}
+
/** A Java property is a name-value pair in a `.properties` file. */
class JavaProperty extends ConfigPair {
- JavaProperty() { this.getFile().getExtension() = "properties" }
+ JavaProperty() { this.getFile() instanceof PropertiesFile }
}
diff --git a/java/ql/lib/semmle/code/java/security/SpringBootActuatorsConfigQuery.qll b/java/ql/lib/semmle/code/java/security/SpringBootActuatorsConfigQuery.qll
index be78380ad3c..d6c889166c1 100644
--- a/java/ql/lib/semmle/code/java/security/SpringBootActuatorsConfigQuery.qll
+++ b/java/ql/lib/semmle/code/java/security/SpringBootActuatorsConfigQuery.qll
@@ -7,41 +7,33 @@ private import semmle.code.configfiles.ConfigFiles
private import semmle.code.xml.MavenPom
/** The parent node of the `org.springframework.boot` group. */
-class SpringBootParent extends Parent {
+private class SpringBootParent extends Parent {
SpringBootParent() { this.getGroup().getValue() = "org.springframework.boot" }
}
-/** Class of Spring Boot dependencies. */
+// TODO: private once done with version string debugging in alert msg.
+/** A `Pom` with a Spring Boot parent node. */
class SpringBootPom extends Pom {
SpringBootPom() { this.getParentElement() instanceof SpringBootParent }
- /** Holds if the Spring Boot Actuator module `spring-boot-starter-actuator` is used in the project. */
- predicate isSpringBootActuatorUsed() {
- this.getADependency().getArtifact().getValue() = "spring-boot-starter-actuator"
- }
-
- /**
- * Holds if the Spring Boot Security module is used in the project, which brings in other security
- * related libraries.
- */
+ /** Holds if the Spring Boot Security module is used in the project. */
predicate isSpringBootSecurityUsed() {
this.getADependency().getArtifact().getValue() = "spring-boot-starter-security"
}
}
-/** The properties file `application.properties`. */
-class ApplicationPropertiesFile extends File {
- ApplicationPropertiesFile() { this.getBaseName() = "application.properties" }
+/** A dependency with artifactId `spring-boot-starter-actuator`. */
+class SpringBootStarterActuatorDependency extends Dependency {
+ SpringBootStarterActuatorDependency() {
+ this.getArtifact().getValue() = "spring-boot-starter-actuator"
+ }
}
-/** A name-value pair stored in an `application.properties` file. */
-class ApplicationPropertiesConfigPair extends ConfigPair {
- ApplicationPropertiesConfigPair() { this.getFile() instanceof ApplicationPropertiesFile }
-}
-
-/** The configuration property `management.security.enabled`. */
-class ManagementSecurityConfig extends ApplicationPropertiesConfigPair {
- ManagementSecurityConfig() { this.getNameElement().getName() = "management.security.enabled" }
+/** The Spring Boot configuration property `management.security.enabled`. */
+private class ManagementSecurityEnabledProperty extends JavaProperty {
+ ManagementSecurityEnabledProperty() {
+ this.getNameElement().getName() = "management.security.enabled"
+ }
/** Gets the whitespace-trimmed value of this property. */
string getValue() { result = this.getValueElement().getValue().trim() }
@@ -50,9 +42,9 @@ class ManagementSecurityConfig extends ApplicationPropertiesConfigPair {
predicate hasSecurityDisabled() { this.getValue() = "false" }
}
-/** The configuration property `management.endpoints.web.exposure.include`. */
-class ManagementEndPointInclude extends ApplicationPropertiesConfigPair {
- ManagementEndPointInclude() {
+/** The Spring Boot configuration property `management.endpoints.web.exposure.include`. */
+private class ManagementEndpointsIncludeProperty extends JavaProperty {
+ ManagementEndpointsIncludeProperty() {
this.getNameElement().getName() = "management.endpoints.web.exposure.include"
}
@@ -62,13 +54,13 @@ class ManagementEndPointInclude extends ApplicationPropertiesConfigPair {
private newtype TOption =
TNone() or
- TSome(ApplicationPropertiesConfigPair ap)
+ TSome(JavaProperty jp)
/**
* An option type that is either a singleton `None` or a `Some` wrapping
- * the `ApplicationPropertiesConfigPair` type.
+ * the `JavaProperty` type.
*/
-class ApplicationPropertiesOption extends TOption {
+class JavaPropertyOption extends TOption {
/** Gets a textual representation of this element. */
string toString() {
this = TNone() and result = "(none)"
@@ -80,21 +72,23 @@ class ApplicationPropertiesOption extends TOption {
Location getLocation() { result = this.asSome().getLocation() }
/** Gets the wrapped element, if any. */
- ApplicationPropertiesConfigPair asSome() { this = TSome(result) }
+ JavaProperty asSome() { this = TSome(result) }
/** Holds if this option is the singleton `None`. */
predicate isNone() { this = TNone() }
}
/**
- * Holds if `ApplicationProperties` ap of a repository managed by `SpringBootPom` pom
- * has a vulnerable configuration of Spring Boot Actuator management endpoints.
+ * Holds if `JavaPropertyOption` jpOption of a repository using `SpringBootStarterActuatorDependency`
+ * d exposes sensitive Spring Boot Actuator endpoints.
*/
-predicate hasConfidentialEndPointExposed(SpringBootPom pom, ApplicationPropertiesOption apOption) {
- pom.isSpringBootActuatorUsed() and
- not pom.isSpringBootSecurityUsed() and
- exists(ApplicationPropertiesFile apFile |
- apFile
+predicate exposesSensitiveEndpoint(
+ SpringBootStarterActuatorDependency d, JavaPropertyOption jpOption
+) {
+ exists(PropertiesFile propFile, SpringBootPom pom |
+ d = pom.getADependency() and
+ not pom.isSpringBootSecurityUsed() and
+ propFile
.getParentContainer()
.getAbsolutePath()
.matches(pom.getFile().getParentContainer().getAbsolutePath() + "%") and // in the same sub-directory
@@ -102,26 +96,26 @@ predicate hasConfidentialEndPointExposed(SpringBootPom pom, ApplicationPropertie
springBootVersion = pom.getParentElement().getVersionString()
|
springBootVersion.regexpMatch("1\\.[0-4].*") and // version 1.0, 1.1, ..., 1.4
- not exists(ManagementSecurityConfig me | me.getFile() = apFile) and
- apOption.isNone()
+ not exists(ManagementSecurityEnabledProperty ep | ep.getFile() = propFile) and
+ jpOption.isNone()
or
springBootVersion.regexpMatch("1\\.[0-5].*") and // version 1.0, 1.1, ..., 1.5
- exists(ManagementSecurityConfig me |
- me.hasSecurityDisabled() and me.getFile() = apFile and me = apOption.asSome()
+ exists(ManagementSecurityEnabledProperty ep |
+ ep.hasSecurityDisabled() and ep.getFile() = propFile and ep = jpOption.asSome()
)
or
springBootVersion.matches(["2.%", "3.%"]) and //version 2.x and 3.x
- exists(ManagementEndPointInclude mi |
- mi.getFile() = apFile and
- mi = apOption.asSome() and
+ exists(ManagementEndpointsIncludeProperty ip |
+ ip.getFile() = propFile and
+ ip = jpOption.asSome() and
(
- mi.getValue() = "*" // all endpoints are enabled
+ ip.getValue() = "*" // all endpoints are exposed
or
- mi.getValue()
+ ip.getValue()
.matches([
"%dump%", "%trace%", "%logfile%", "%shutdown%", "%startup%", "%mappings%",
"%env%", "%beans%", "%sessions%"
- ]) // confidential endpoints to check although all endpoints apart from '/health' are considered sensitive by Spring
+ ]) // sensitive endpoints to check although all endpoints apart from '/health' are considered sensitive by Spring
)
)
)
diff --git a/java/ql/src/Security/CWE/CWE-200/InsecureSpringActuatorConfig/InsecureSpringActuatorConfig.ql b/java/ql/src/Security/CWE/CWE-200/InsecureSpringActuatorConfig/InsecureSpringActuatorConfig.ql
index 2437a77953d..989646c10af 100644
--- a/java/ql/src/Security/CWE/CWE-200/InsecureSpringActuatorConfig/InsecureSpringActuatorConfig.ql
+++ b/java/ql/src/Security/CWE/CWE-200/InsecureSpringActuatorConfig/InsecureSpringActuatorConfig.ql
@@ -15,11 +15,11 @@ import java
import semmle.code.xml.MavenPom
import semmle.code.java.security.SpringBootActuatorsConfigQuery
-from SpringBootPom pom, Dependency d, ApplicationPropertiesOption apOption
+from SpringBootStarterActuatorDependency d, JavaPropertyOption jpOption, SpringBootPom pom
where
- hasConfidentialEndPointExposed(pom, apOption) and
- d = pom.getADependency() and
- d.getArtifact().getValue() = "spring-boot-starter-actuator"
+ exposesSensitiveEndpoint(d, jpOption) and
+ // TODO: remove pom; for debugging versions
+ d = pom.getADependency()
select d,
"Insecure $@ of Spring Boot Actuator exposes sensitive endpoints (" +
- pom.getParentElement().getVersionString() + ").", apOption, "configuration"
+ pom.getParentElement().getVersionString() + ").", jpOption, "configuration"
From ea529b047b0223d025b0009fb95c944196a71da8 Mon Sep 17 00:00:00 2001
From: Jami Cogswell
Date: Thu, 17 Jul 2025 18:12:45 -0400
Subject: [PATCH 049/984] Java: adjust metadata and alert msg
---
.../InsecureSpringActuatorConfig.ql | 14 +++++++-------
1 file changed, 7 insertions(+), 7 deletions(-)
diff --git a/java/ql/src/Security/CWE/CWE-200/InsecureSpringActuatorConfig/InsecureSpringActuatorConfig.ql b/java/ql/src/Security/CWE/CWE-200/InsecureSpringActuatorConfig/InsecureSpringActuatorConfig.ql
index 989646c10af..5fb86c42b80 100644
--- a/java/ql/src/Security/CWE/CWE-200/InsecureSpringActuatorConfig/InsecureSpringActuatorConfig.ql
+++ b/java/ql/src/Security/CWE/CWE-200/InsecureSpringActuatorConfig/InsecureSpringActuatorConfig.ql
@@ -1,14 +1,14 @@
/**
- * @name Insecure Spring Boot Actuator Configuration
- * @description Exposed Spring Boot Actuator through configuration files without declarative or procedural
- * security enforcement leads to information leak or even remote code execution.
+ * @name Exposed Spring Boot actuators in configuration file
+ * @description Exposing Spring Boot actuators through configuration files may lead to information leak from
+ * the internal application, or even to remote code execution.
* @kind problem
* @problem.severity error
+ * @security-severity 6.5
* @precision high
- * @id java/insecure-spring-actuator-config
+ * @id java/spring-boot-exposed-actuators-config
* @tags security
- * experimental
- * external/cwe/cwe-016
+ * external/cwe/cwe-200
*/
import java
@@ -21,5 +21,5 @@ where
// TODO: remove pom; for debugging versions
d = pom.getADependency()
select d,
- "Insecure $@ of Spring Boot Actuator exposes sensitive endpoints (" +
+ "Insecure Spring Boot actuator $@ exposes sensitive endpoints (" +
pom.getParentElement().getVersionString() + ").", jpOption, "configuration"
From 70d51504a7372e265c0a4b500e4030590d27a8f3 Mon Sep 17 00:00:00 2001
From: Jami Cogswell
Date: Thu, 17 Jul 2025 18:20:14 -0400
Subject: [PATCH 050/984] Java: rename to align with
'java/spring-boot-exposed-actuators' query
---
.../query-suite/java-code-scanning.qls.expected | 2 +-
.../java-security-and-quality.qls.expected | 2 +-
.../query-suite/java-security-extended.qls.expected | 2 +-
.../SpringBootActuatorsConfig.qhelp} | 0
.../SpringBootActuatorsConfig.ql} | 0
.../application_bad.properties | 0
.../application_good.properties | 0
.../pom_good.xml | 0
.../InsecureSpringActuatorConfig.qlref | 2 --
.../InsecureSpringActuatorConfig/SensitiveInfo.java | 13 -------------
.../SpringBootActuatorsConfig.expected} | 0
.../SpringBootActuatorsConfig.qlref | 2 ++
.../bad/default/application.properties | 0
.../Version1.0.x-1.4.x/bad/default/pom.xml | 0
.../bad/false/application.properties | 0
.../Version1.0.x-1.4.x/bad/false/pom.xml | 0
.../Version1.0.x-1.4.x/good/application.properties | 0
.../Version1.0.x-1.4.x/good/pom.xml | 0
.../Version1.5.x/bad/application.properties | 0
.../Version1.5.x/bad/pom.xml | 0
.../Version1.5.x/good/application.properties | 0
.../Version1.5.x/good/pom.xml | 0
.../Version2.x/bad/application.properties | 0
.../Version2.x/bad/pom.xml | 0
.../Version2.x/good/application.properties | 0
.../Version2.x/good/pom.xml | 0
.../Version3.x/bad/application.properties | 0
.../Version3.x/bad/pom.xml | 0
.../Version3.x/good/application.properties | 0
.../Version3.x/good/pom.xml | 0
.../options | 0
31 files changed, 5 insertions(+), 18 deletions(-)
rename java/ql/src/Security/CWE/CWE-200/{InsecureSpringActuatorConfig/InsecureSpringActuatorConfig.qhelp => SpringBootActuatorsConfig/SpringBootActuatorsConfig.qhelp} (100%)
rename java/ql/src/Security/CWE/CWE-200/{InsecureSpringActuatorConfig/InsecureSpringActuatorConfig.ql => SpringBootActuatorsConfig/SpringBootActuatorsConfig.ql} (100%)
rename java/ql/src/Security/CWE/CWE-200/{InsecureSpringActuatorConfig => SpringBootActuatorsConfig}/application_bad.properties (100%)
rename java/ql/src/Security/CWE/CWE-200/{InsecureSpringActuatorConfig => SpringBootActuatorsConfig}/application_good.properties (100%)
rename java/ql/src/Security/CWE/CWE-200/{InsecureSpringActuatorConfig => SpringBootActuatorsConfig}/pom_good.xml (100%)
delete mode 100644 java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/InsecureSpringActuatorConfig.qlref
delete mode 100644 java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/SensitiveInfo.java
rename java/ql/test/query-tests/security/CWE-200/semmle/tests/{InsecureSpringActuatorConfig/InsecureSpringActuatorConfig.expected => SpringBootActuatorsConfig/SpringBootActuatorsConfig.expected} (100%)
create mode 100644 java/ql/test/query-tests/security/CWE-200/semmle/tests/SpringBootActuatorsConfig/SpringBootActuatorsConfig.qlref
rename java/ql/test/query-tests/security/CWE-200/semmle/tests/{InsecureSpringActuatorConfig => SpringBootActuatorsConfig}/Version1.0.x-1.4.x/bad/default/application.properties (100%)
rename java/ql/test/query-tests/security/CWE-200/semmle/tests/{InsecureSpringActuatorConfig => SpringBootActuatorsConfig}/Version1.0.x-1.4.x/bad/default/pom.xml (100%)
rename java/ql/test/query-tests/security/CWE-200/semmle/tests/{InsecureSpringActuatorConfig => SpringBootActuatorsConfig}/Version1.0.x-1.4.x/bad/false/application.properties (100%)
rename java/ql/test/query-tests/security/CWE-200/semmle/tests/{InsecureSpringActuatorConfig => SpringBootActuatorsConfig}/Version1.0.x-1.4.x/bad/false/pom.xml (100%)
rename java/ql/test/query-tests/security/CWE-200/semmle/tests/{InsecureSpringActuatorConfig => SpringBootActuatorsConfig}/Version1.0.x-1.4.x/good/application.properties (100%)
rename java/ql/test/query-tests/security/CWE-200/semmle/tests/{InsecureSpringActuatorConfig => SpringBootActuatorsConfig}/Version1.0.x-1.4.x/good/pom.xml (100%)
rename java/ql/test/query-tests/security/CWE-200/semmle/tests/{InsecureSpringActuatorConfig => SpringBootActuatorsConfig}/Version1.5.x/bad/application.properties (100%)
rename java/ql/test/query-tests/security/CWE-200/semmle/tests/{InsecureSpringActuatorConfig => SpringBootActuatorsConfig}/Version1.5.x/bad/pom.xml (100%)
rename java/ql/test/query-tests/security/CWE-200/semmle/tests/{InsecureSpringActuatorConfig => SpringBootActuatorsConfig}/Version1.5.x/good/application.properties (100%)
rename java/ql/test/query-tests/security/CWE-200/semmle/tests/{InsecureSpringActuatorConfig => SpringBootActuatorsConfig}/Version1.5.x/good/pom.xml (100%)
rename java/ql/test/query-tests/security/CWE-200/semmle/tests/{InsecureSpringActuatorConfig => SpringBootActuatorsConfig}/Version2.x/bad/application.properties (100%)
rename java/ql/test/query-tests/security/CWE-200/semmle/tests/{InsecureSpringActuatorConfig => SpringBootActuatorsConfig}/Version2.x/bad/pom.xml (100%)
rename java/ql/test/query-tests/security/CWE-200/semmle/tests/{InsecureSpringActuatorConfig => SpringBootActuatorsConfig}/Version2.x/good/application.properties (100%)
rename java/ql/test/query-tests/security/CWE-200/semmle/tests/{InsecureSpringActuatorConfig => SpringBootActuatorsConfig}/Version2.x/good/pom.xml (100%)
rename java/ql/test/query-tests/security/CWE-200/semmle/tests/{InsecureSpringActuatorConfig => SpringBootActuatorsConfig}/Version3.x/bad/application.properties (100%)
rename java/ql/test/query-tests/security/CWE-200/semmle/tests/{InsecureSpringActuatorConfig => SpringBootActuatorsConfig}/Version3.x/bad/pom.xml (100%)
rename java/ql/test/query-tests/security/CWE-200/semmle/tests/{InsecureSpringActuatorConfig => SpringBootActuatorsConfig}/Version3.x/good/application.properties (100%)
rename java/ql/test/query-tests/security/CWE-200/semmle/tests/{InsecureSpringActuatorConfig => SpringBootActuatorsConfig}/Version3.x/good/pom.xml (100%)
rename java/ql/test/query-tests/security/CWE-200/semmle/tests/{InsecureSpringActuatorConfig => SpringBootActuatorsConfig}/options (100%)
diff --git a/java/ql/integration-tests/java/query-suite/java-code-scanning.qls.expected b/java/ql/integration-tests/java/query-suite/java-code-scanning.qls.expected
index 90b5b7ca491..afa6cebba31 100644
--- a/java/ql/integration-tests/java/query-suite/java-code-scanning.qls.expected
+++ b/java/ql/integration-tests/java/query-suite/java-code-scanning.qls.expected
@@ -26,8 +26,8 @@ ql/java/ql/src/Security/CWE/CWE-113/NettyResponseSplitting.ql
ql/java/ql/src/Security/CWE/CWE-113/ResponseSplitting.ql
ql/java/ql/src/Security/CWE/CWE-1204/StaticInitializationVector.ql
ql/java/ql/src/Security/CWE/CWE-134/ExternallyControlledFormatString.ql
-ql/java/ql/src/Security/CWE/CWE-200/InsecureSpringActuatorConfig/InsecureSpringActuatorConfig.ql
ql/java/ql/src/Security/CWE/CWE-200/SpringBootActuators.ql
+ql/java/ql/src/Security/CWE/CWE-200/SpringBootActuatorsConfig/SpringBootActuatorsConfig.ql
ql/java/ql/src/Security/CWE/CWE-209/SensitiveDataExposureThroughErrorMessage.ql
ql/java/ql/src/Security/CWE/CWE-209/StackTraceExposure.ql
ql/java/ql/src/Security/CWE/CWE-266/IntentUriPermissionManipulation.ql
diff --git a/java/ql/integration-tests/java/query-suite/java-security-and-quality.qls.expected b/java/ql/integration-tests/java/query-suite/java-security-and-quality.qls.expected
index b203ea23a62..f5470c463c3 100644
--- a/java/ql/integration-tests/java/query-suite/java-security-and-quality.qls.expected
+++ b/java/ql/integration-tests/java/query-suite/java-security-and-quality.qls.expected
@@ -142,8 +142,8 @@ ql/java/ql/src/Security/CWE/CWE-200/AndroidSensitiveNotifications.ql
ql/java/ql/src/Security/CWE/CWE-200/AndroidSensitiveTextField.ql
ql/java/ql/src/Security/CWE/CWE-200/AndroidWebViewSettingsAllowsContentAccess.ql
ql/java/ql/src/Security/CWE/CWE-200/AndroidWebViewSettingsFileAccess.ql
-ql/java/ql/src/Security/CWE/CWE-200/InsecureSpringActuatorConfig/InsecureSpringActuatorConfig.ql
ql/java/ql/src/Security/CWE/CWE-200/SpringBootActuators.ql
+ql/java/ql/src/Security/CWE/CWE-200/SpringBootActuatorsConfig/SpringBootActuatorsConfig.ql
ql/java/ql/src/Security/CWE/CWE-200/TempDirLocalInformationDisclosure.ql
ql/java/ql/src/Security/CWE/CWE-209/SensitiveDataExposureThroughErrorMessage.ql
ql/java/ql/src/Security/CWE/CWE-209/StackTraceExposure.ql
diff --git a/java/ql/integration-tests/java/query-suite/java-security-extended.qls.expected b/java/ql/integration-tests/java/query-suite/java-security-extended.qls.expected
index c7dac907a96..a3ebc029d28 100644
--- a/java/ql/integration-tests/java/query-suite/java-security-extended.qls.expected
+++ b/java/ql/integration-tests/java/query-suite/java-security-extended.qls.expected
@@ -45,8 +45,8 @@ ql/java/ql/src/Security/CWE/CWE-200/AndroidSensitiveNotifications.ql
ql/java/ql/src/Security/CWE/CWE-200/AndroidSensitiveTextField.ql
ql/java/ql/src/Security/CWE/CWE-200/AndroidWebViewSettingsAllowsContentAccess.ql
ql/java/ql/src/Security/CWE/CWE-200/AndroidWebViewSettingsFileAccess.ql
-ql/java/ql/src/Security/CWE/CWE-200/InsecureSpringActuatorConfig/InsecureSpringActuatorConfig.ql
ql/java/ql/src/Security/CWE/CWE-200/SpringBootActuators.ql
+ql/java/ql/src/Security/CWE/CWE-200/SpringBootActuatorsConfig/SpringBootActuatorsConfig.ql
ql/java/ql/src/Security/CWE/CWE-200/TempDirLocalInformationDisclosure.ql
ql/java/ql/src/Security/CWE/CWE-209/SensitiveDataExposureThroughErrorMessage.ql
ql/java/ql/src/Security/CWE/CWE-209/StackTraceExposure.ql
diff --git a/java/ql/src/Security/CWE/CWE-200/InsecureSpringActuatorConfig/InsecureSpringActuatorConfig.qhelp b/java/ql/src/Security/CWE/CWE-200/SpringBootActuatorsConfig/SpringBootActuatorsConfig.qhelp
similarity index 100%
rename from java/ql/src/Security/CWE/CWE-200/InsecureSpringActuatorConfig/InsecureSpringActuatorConfig.qhelp
rename to java/ql/src/Security/CWE/CWE-200/SpringBootActuatorsConfig/SpringBootActuatorsConfig.qhelp
diff --git a/java/ql/src/Security/CWE/CWE-200/InsecureSpringActuatorConfig/InsecureSpringActuatorConfig.ql b/java/ql/src/Security/CWE/CWE-200/SpringBootActuatorsConfig/SpringBootActuatorsConfig.ql
similarity index 100%
rename from java/ql/src/Security/CWE/CWE-200/InsecureSpringActuatorConfig/InsecureSpringActuatorConfig.ql
rename to java/ql/src/Security/CWE/CWE-200/SpringBootActuatorsConfig/SpringBootActuatorsConfig.ql
diff --git a/java/ql/src/Security/CWE/CWE-200/InsecureSpringActuatorConfig/application_bad.properties b/java/ql/src/Security/CWE/CWE-200/SpringBootActuatorsConfig/application_bad.properties
similarity index 100%
rename from java/ql/src/Security/CWE/CWE-200/InsecureSpringActuatorConfig/application_bad.properties
rename to java/ql/src/Security/CWE/CWE-200/SpringBootActuatorsConfig/application_bad.properties
diff --git a/java/ql/src/Security/CWE/CWE-200/InsecureSpringActuatorConfig/application_good.properties b/java/ql/src/Security/CWE/CWE-200/SpringBootActuatorsConfig/application_good.properties
similarity index 100%
rename from java/ql/src/Security/CWE/CWE-200/InsecureSpringActuatorConfig/application_good.properties
rename to java/ql/src/Security/CWE/CWE-200/SpringBootActuatorsConfig/application_good.properties
diff --git a/java/ql/src/Security/CWE/CWE-200/InsecureSpringActuatorConfig/pom_good.xml b/java/ql/src/Security/CWE/CWE-200/SpringBootActuatorsConfig/pom_good.xml
similarity index 100%
rename from java/ql/src/Security/CWE/CWE-200/InsecureSpringActuatorConfig/pom_good.xml
rename to java/ql/src/Security/CWE/CWE-200/SpringBootActuatorsConfig/pom_good.xml
diff --git a/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/InsecureSpringActuatorConfig.qlref b/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/InsecureSpringActuatorConfig.qlref
deleted file mode 100644
index b826de8eed3..00000000000
--- a/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/InsecureSpringActuatorConfig.qlref
+++ /dev/null
@@ -1,2 +0,0 @@
-query: Security/CWE/CWE-200/InsecureSpringActuatorConfig/InsecureSpringActuatorConfig.ql
-postprocess: utils/test/InlineExpectationsTestQuery.ql
diff --git a/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/SensitiveInfo.java b/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/SensitiveInfo.java
deleted file mode 100644
index a3ff69c1b81..00000000000
--- a/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/SensitiveInfo.java
+++ /dev/null
@@ -1,13 +0,0 @@
-import org.springframework.stereotype.Controller;
-import org.springframework.web.bind.annotation.RequestParam;
-import org.springframework.web.bind.annotation.RequestMapping;
-
-@Controller
-public class SensitiveInfo {
- @RequestMapping
- public void handleLogin(@RequestParam String username, @RequestParam String password) throws Exception {
- if (!username.equals("") && password.equals("")) {
- //Blank processing
- }
- }
-}
\ No newline at end of file
diff --git a/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/InsecureSpringActuatorConfig.expected b/java/ql/test/query-tests/security/CWE-200/semmle/tests/SpringBootActuatorsConfig/SpringBootActuatorsConfig.expected
similarity index 100%
rename from java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/InsecureSpringActuatorConfig.expected
rename to java/ql/test/query-tests/security/CWE-200/semmle/tests/SpringBootActuatorsConfig/SpringBootActuatorsConfig.expected
diff --git a/java/ql/test/query-tests/security/CWE-200/semmle/tests/SpringBootActuatorsConfig/SpringBootActuatorsConfig.qlref b/java/ql/test/query-tests/security/CWE-200/semmle/tests/SpringBootActuatorsConfig/SpringBootActuatorsConfig.qlref
new file mode 100644
index 00000000000..eec8ba18ae1
--- /dev/null
+++ b/java/ql/test/query-tests/security/CWE-200/semmle/tests/SpringBootActuatorsConfig/SpringBootActuatorsConfig.qlref
@@ -0,0 +1,2 @@
+query: Security/CWE/CWE-200/SpringBootActuatorsConfig/SpringBootActuatorsConfig.ql
+postprocess: utils/test/InlineExpectationsTestQuery.ql
diff --git a/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version1.0.x-1.4.x/bad/default/application.properties b/java/ql/test/query-tests/security/CWE-200/semmle/tests/SpringBootActuatorsConfig/Version1.0.x-1.4.x/bad/default/application.properties
similarity index 100%
rename from java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version1.0.x-1.4.x/bad/default/application.properties
rename to java/ql/test/query-tests/security/CWE-200/semmle/tests/SpringBootActuatorsConfig/Version1.0.x-1.4.x/bad/default/application.properties
diff --git a/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version1.0.x-1.4.x/bad/default/pom.xml b/java/ql/test/query-tests/security/CWE-200/semmle/tests/SpringBootActuatorsConfig/Version1.0.x-1.4.x/bad/default/pom.xml
similarity index 100%
rename from java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version1.0.x-1.4.x/bad/default/pom.xml
rename to java/ql/test/query-tests/security/CWE-200/semmle/tests/SpringBootActuatorsConfig/Version1.0.x-1.4.x/bad/default/pom.xml
diff --git a/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version1.0.x-1.4.x/bad/false/application.properties b/java/ql/test/query-tests/security/CWE-200/semmle/tests/SpringBootActuatorsConfig/Version1.0.x-1.4.x/bad/false/application.properties
similarity index 100%
rename from java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version1.0.x-1.4.x/bad/false/application.properties
rename to java/ql/test/query-tests/security/CWE-200/semmle/tests/SpringBootActuatorsConfig/Version1.0.x-1.4.x/bad/false/application.properties
diff --git a/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version1.0.x-1.4.x/bad/false/pom.xml b/java/ql/test/query-tests/security/CWE-200/semmle/tests/SpringBootActuatorsConfig/Version1.0.x-1.4.x/bad/false/pom.xml
similarity index 100%
rename from java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version1.0.x-1.4.x/bad/false/pom.xml
rename to java/ql/test/query-tests/security/CWE-200/semmle/tests/SpringBootActuatorsConfig/Version1.0.x-1.4.x/bad/false/pom.xml
diff --git a/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version1.0.x-1.4.x/good/application.properties b/java/ql/test/query-tests/security/CWE-200/semmle/tests/SpringBootActuatorsConfig/Version1.0.x-1.4.x/good/application.properties
similarity index 100%
rename from java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version1.0.x-1.4.x/good/application.properties
rename to java/ql/test/query-tests/security/CWE-200/semmle/tests/SpringBootActuatorsConfig/Version1.0.x-1.4.x/good/application.properties
diff --git a/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version1.0.x-1.4.x/good/pom.xml b/java/ql/test/query-tests/security/CWE-200/semmle/tests/SpringBootActuatorsConfig/Version1.0.x-1.4.x/good/pom.xml
similarity index 100%
rename from java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version1.0.x-1.4.x/good/pom.xml
rename to java/ql/test/query-tests/security/CWE-200/semmle/tests/SpringBootActuatorsConfig/Version1.0.x-1.4.x/good/pom.xml
diff --git a/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version1.5.x/bad/application.properties b/java/ql/test/query-tests/security/CWE-200/semmle/tests/SpringBootActuatorsConfig/Version1.5.x/bad/application.properties
similarity index 100%
rename from java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version1.5.x/bad/application.properties
rename to java/ql/test/query-tests/security/CWE-200/semmle/tests/SpringBootActuatorsConfig/Version1.5.x/bad/application.properties
diff --git a/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version1.5.x/bad/pom.xml b/java/ql/test/query-tests/security/CWE-200/semmle/tests/SpringBootActuatorsConfig/Version1.5.x/bad/pom.xml
similarity index 100%
rename from java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version1.5.x/bad/pom.xml
rename to java/ql/test/query-tests/security/CWE-200/semmle/tests/SpringBootActuatorsConfig/Version1.5.x/bad/pom.xml
diff --git a/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version1.5.x/good/application.properties b/java/ql/test/query-tests/security/CWE-200/semmle/tests/SpringBootActuatorsConfig/Version1.5.x/good/application.properties
similarity index 100%
rename from java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version1.5.x/good/application.properties
rename to java/ql/test/query-tests/security/CWE-200/semmle/tests/SpringBootActuatorsConfig/Version1.5.x/good/application.properties
diff --git a/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version1.5.x/good/pom.xml b/java/ql/test/query-tests/security/CWE-200/semmle/tests/SpringBootActuatorsConfig/Version1.5.x/good/pom.xml
similarity index 100%
rename from java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version1.5.x/good/pom.xml
rename to java/ql/test/query-tests/security/CWE-200/semmle/tests/SpringBootActuatorsConfig/Version1.5.x/good/pom.xml
diff --git a/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version2.x/bad/application.properties b/java/ql/test/query-tests/security/CWE-200/semmle/tests/SpringBootActuatorsConfig/Version2.x/bad/application.properties
similarity index 100%
rename from java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version2.x/bad/application.properties
rename to java/ql/test/query-tests/security/CWE-200/semmle/tests/SpringBootActuatorsConfig/Version2.x/bad/application.properties
diff --git a/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version2.x/bad/pom.xml b/java/ql/test/query-tests/security/CWE-200/semmle/tests/SpringBootActuatorsConfig/Version2.x/bad/pom.xml
similarity index 100%
rename from java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version2.x/bad/pom.xml
rename to java/ql/test/query-tests/security/CWE-200/semmle/tests/SpringBootActuatorsConfig/Version2.x/bad/pom.xml
diff --git a/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version2.x/good/application.properties b/java/ql/test/query-tests/security/CWE-200/semmle/tests/SpringBootActuatorsConfig/Version2.x/good/application.properties
similarity index 100%
rename from java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version2.x/good/application.properties
rename to java/ql/test/query-tests/security/CWE-200/semmle/tests/SpringBootActuatorsConfig/Version2.x/good/application.properties
diff --git a/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version2.x/good/pom.xml b/java/ql/test/query-tests/security/CWE-200/semmle/tests/SpringBootActuatorsConfig/Version2.x/good/pom.xml
similarity index 100%
rename from java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version2.x/good/pom.xml
rename to java/ql/test/query-tests/security/CWE-200/semmle/tests/SpringBootActuatorsConfig/Version2.x/good/pom.xml
diff --git a/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version3.x/bad/application.properties b/java/ql/test/query-tests/security/CWE-200/semmle/tests/SpringBootActuatorsConfig/Version3.x/bad/application.properties
similarity index 100%
rename from java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version3.x/bad/application.properties
rename to java/ql/test/query-tests/security/CWE-200/semmle/tests/SpringBootActuatorsConfig/Version3.x/bad/application.properties
diff --git a/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version3.x/bad/pom.xml b/java/ql/test/query-tests/security/CWE-200/semmle/tests/SpringBootActuatorsConfig/Version3.x/bad/pom.xml
similarity index 100%
rename from java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version3.x/bad/pom.xml
rename to java/ql/test/query-tests/security/CWE-200/semmle/tests/SpringBootActuatorsConfig/Version3.x/bad/pom.xml
diff --git a/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version3.x/good/application.properties b/java/ql/test/query-tests/security/CWE-200/semmle/tests/SpringBootActuatorsConfig/Version3.x/good/application.properties
similarity index 100%
rename from java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version3.x/good/application.properties
rename to java/ql/test/query-tests/security/CWE-200/semmle/tests/SpringBootActuatorsConfig/Version3.x/good/application.properties
diff --git a/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version3.x/good/pom.xml b/java/ql/test/query-tests/security/CWE-200/semmle/tests/SpringBootActuatorsConfig/Version3.x/good/pom.xml
similarity index 100%
rename from java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/Version3.x/good/pom.xml
rename to java/ql/test/query-tests/security/CWE-200/semmle/tests/SpringBootActuatorsConfig/Version3.x/good/pom.xml
diff --git a/java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/options b/java/ql/test/query-tests/security/CWE-200/semmle/tests/SpringBootActuatorsConfig/options
similarity index 100%
rename from java/ql/test/query-tests/security/CWE-200/semmle/tests/InsecureSpringActuatorConfig/options
rename to java/ql/test/query-tests/security/CWE-200/semmle/tests/SpringBootActuatorsConfig/options
From 8decc136c41155adfb10c266335e02a159777f99 Mon Sep 17 00:00:00 2001
From: Jami Cogswell
Date: Thu, 17 Jul 2025 18:37:53 -0400
Subject: [PATCH 051/984] Java: add change note
---
.../change-notes/2025-07-17-spring-actuators-config-promo.md | 4 ++++
1 file changed, 4 insertions(+)
create mode 100644 java/ql/src/change-notes/2025-07-17-spring-actuators-config-promo.md
diff --git a/java/ql/src/change-notes/2025-07-17-spring-actuators-config-promo.md b/java/ql/src/change-notes/2025-07-17-spring-actuators-config-promo.md
new file mode 100644
index 00000000000..ec53c015fff
--- /dev/null
+++ b/java/ql/src/change-notes/2025-07-17-spring-actuators-config-promo.md
@@ -0,0 +1,4 @@
+---
+category: newQuery
+---
+* The query `java/insecure-spring-actuator-config` has been promoted from experimental to the main query pack as `java/spring-boot-exposed-actuators-config`. Its results will now appear by default. This query was originally submitted as an experimental query [by @luchua-bc](https://github.com/github/codeql/pull/5384).
From 685f68d9d39f3942864eacd1daef6cd742e1eba8 Mon Sep 17 00:00:00 2001
From: Jami Cogswell
Date: Fri, 18 Jul 2025 09:50:49 -0400
Subject: [PATCH 052/984] Java: support 'management.endpoints.web.expose'
property
---
.../SpringBootActuatorsConfigQuery.qll | 21 +++++----
.../bad/expose/application.properties | 2 +
.../Version2.x/bad/{ => expose}/pom.xml | 0
.../application.properties | 0
.../Version2.x/bad/exposure-include/pom.xml | 47 +++++++++++++++++++
5 files changed, 61 insertions(+), 9 deletions(-)
create mode 100644 java/ql/test/query-tests/security/CWE-200/semmle/tests/SpringBootActuatorsConfig/Version2.x/bad/expose/application.properties
rename java/ql/test/query-tests/security/CWE-200/semmle/tests/SpringBootActuatorsConfig/Version2.x/bad/{ => expose}/pom.xml (100%)
rename java/ql/test/query-tests/security/CWE-200/semmle/tests/SpringBootActuatorsConfig/Version2.x/bad/{ => exposure-include}/application.properties (100%)
create mode 100644 java/ql/test/query-tests/security/CWE-200/semmle/tests/SpringBootActuatorsConfig/Version2.x/bad/exposure-include/pom.xml
diff --git a/java/ql/lib/semmle/code/java/security/SpringBootActuatorsConfigQuery.qll b/java/ql/lib/semmle/code/java/security/SpringBootActuatorsConfigQuery.qll
index d6c889166c1..5f4ee632775 100644
--- a/java/ql/lib/semmle/code/java/security/SpringBootActuatorsConfigQuery.qll
+++ b/java/ql/lib/semmle/code/java/security/SpringBootActuatorsConfigQuery.qll
@@ -42,10 +42,13 @@ private class ManagementSecurityEnabledProperty extends JavaProperty {
predicate hasSecurityDisabled() { this.getValue() = "false" }
}
-/** The Spring Boot configuration property `management.endpoints.web.exposure.include`. */
-private class ManagementEndpointsIncludeProperty extends JavaProperty {
- ManagementEndpointsIncludeProperty() {
- this.getNameElement().getName() = "management.endpoints.web.exposure.include"
+/**
+ * The Spring Boot configuration property `management.endpoints.web.exposure.include`
+ * or `management.endpoints.web.expose`.
+ */
+private class ManagementEndpointsExposeProperty extends JavaProperty {
+ ManagementEndpointsExposeProperty() {
+ this.getNameElement().getName() = "management.endpoints.web." + ["exposure.include", "expose"]
}
/** Gets the whitespace-trimmed value of this property. */
@@ -105,13 +108,13 @@ predicate exposesSensitiveEndpoint(
)
or
springBootVersion.matches(["2.%", "3.%"]) and //version 2.x and 3.x
- exists(ManagementEndpointsIncludeProperty ip |
- ip.getFile() = propFile and
- ip = jpOption.asSome() and
+ exists(ManagementEndpointsExposeProperty ep |
+ ep.getFile() = propFile and
+ ep = jpOption.asSome() and
(
- ip.getValue() = "*" // all endpoints are exposed
+ ep.getValue() = "*" // all endpoints are exposed
or
- ip.getValue()
+ ep.getValue()
.matches([
"%dump%", "%trace%", "%logfile%", "%shutdown%", "%startup%", "%mappings%",
"%env%", "%beans%", "%sessions%"
diff --git a/java/ql/test/query-tests/security/CWE-200/semmle/tests/SpringBootActuatorsConfig/Version2.x/bad/expose/application.properties b/java/ql/test/query-tests/security/CWE-200/semmle/tests/SpringBootActuatorsConfig/Version2.x/bad/expose/application.properties
new file mode 100644
index 00000000000..338b1fb3a9c
--- /dev/null
+++ b/java/ql/test/query-tests/security/CWE-200/semmle/tests/SpringBootActuatorsConfig/Version2.x/bad/expose/application.properties
@@ -0,0 +1,2 @@
+# vulnerable configuration (spring boot 2.0.0.RC1): exposes health and info only by default, here overridden to expose everything
+management.endpoints.web.expose=*
\ No newline at end of file
diff --git a/java/ql/test/query-tests/security/CWE-200/semmle/tests/SpringBootActuatorsConfig/Version2.x/bad/pom.xml b/java/ql/test/query-tests/security/CWE-200/semmle/tests/SpringBootActuatorsConfig/Version2.x/bad/expose/pom.xml
similarity index 100%
rename from java/ql/test/query-tests/security/CWE-200/semmle/tests/SpringBootActuatorsConfig/Version2.x/bad/pom.xml
rename to java/ql/test/query-tests/security/CWE-200/semmle/tests/SpringBootActuatorsConfig/Version2.x/bad/expose/pom.xml
diff --git a/java/ql/test/query-tests/security/CWE-200/semmle/tests/SpringBootActuatorsConfig/Version2.x/bad/application.properties b/java/ql/test/query-tests/security/CWE-200/semmle/tests/SpringBootActuatorsConfig/Version2.x/bad/exposure-include/application.properties
similarity index 100%
rename from java/ql/test/query-tests/security/CWE-200/semmle/tests/SpringBootActuatorsConfig/Version2.x/bad/application.properties
rename to java/ql/test/query-tests/security/CWE-200/semmle/tests/SpringBootActuatorsConfig/Version2.x/bad/exposure-include/application.properties
diff --git a/java/ql/test/query-tests/security/CWE-200/semmle/tests/SpringBootActuatorsConfig/Version2.x/bad/exposure-include/pom.xml b/java/ql/test/query-tests/security/CWE-200/semmle/tests/SpringBootActuatorsConfig/Version2.x/bad/exposure-include/pom.xml
new file mode 100644
index 00000000000..c22f08d7e7e
--- /dev/null
+++ b/java/ql/test/query-tests/security/CWE-200/semmle/tests/SpringBootActuatorsConfig/Version2.x/bad/exposure-include/pom.xml
@@ -0,0 +1,47 @@
+
+
+ 4.0.0
+
+ spring-boot-actuator-app
+ spring-boot-actuator-app
+ 1.0-SNAPSHOT
+
+
+ UTF-8
+ 1.8
+ 1.8
+
+
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 2.2.6.RELEASE
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+ org.springframework.boot
+ spring-boot-starter-actuator
+
+
+ org.springframework.boot
+ spring-boot-devtools
+
+
+
+ org.springframework.boot
+ spring-boot-test
+
+
+
+
\ No newline at end of file
From 7250265c1f109ae9a80e695dc316b8ac3f39285f Mon Sep 17 00:00:00 2001
From: Jami Cogswell
Date: Fri, 18 Jul 2025 17:32:35 -0400
Subject: [PATCH 053/984] Java: consider all endpoints except for health and
info as sensitive to align with Spring docs
---
.../SpringBootActuatorsConfigQuery.qll | 15 +++---
.../SpringBootActuatorsConfig.expected | 7 ++-
.../{ => all-exposed}/application.properties | 0
.../{ => all-exposed}/pom.xml | 0
.../some-exposed/application.properties | 2 +
.../bad/exposure-include/some-exposed/pom.xml | 47 +++++++++++++++++++
.../{ => all-exposed}/application.properties | 0
.../Version3.x/bad/{ => all-exposed}/pom.xml | 0
.../bad/some-exposed/application.properties | 2 +
.../Version3.x/bad/some-exposed/pom.xml | 47 +++++++++++++++++++
10 files changed, 112 insertions(+), 8 deletions(-)
rename java/ql/test/query-tests/security/CWE-200/semmle/tests/SpringBootActuatorsConfig/Version2.x/bad/exposure-include/{ => all-exposed}/application.properties (100%)
rename java/ql/test/query-tests/security/CWE-200/semmle/tests/SpringBootActuatorsConfig/Version2.x/bad/exposure-include/{ => all-exposed}/pom.xml (100%)
create mode 100644 java/ql/test/query-tests/security/CWE-200/semmle/tests/SpringBootActuatorsConfig/Version2.x/bad/exposure-include/some-exposed/application.properties
create mode 100644 java/ql/test/query-tests/security/CWE-200/semmle/tests/SpringBootActuatorsConfig/Version2.x/bad/exposure-include/some-exposed/pom.xml
rename java/ql/test/query-tests/security/CWE-200/semmle/tests/SpringBootActuatorsConfig/Version3.x/bad/{ => all-exposed}/application.properties (100%)
rename java/ql/test/query-tests/security/CWE-200/semmle/tests/SpringBootActuatorsConfig/Version3.x/bad/{ => all-exposed}/pom.xml (100%)
create mode 100644 java/ql/test/query-tests/security/CWE-200/semmle/tests/SpringBootActuatorsConfig/Version3.x/bad/some-exposed/application.properties
create mode 100644 java/ql/test/query-tests/security/CWE-200/semmle/tests/SpringBootActuatorsConfig/Version3.x/bad/some-exposed/pom.xml
diff --git a/java/ql/lib/semmle/code/java/security/SpringBootActuatorsConfigQuery.qll b/java/ql/lib/semmle/code/java/security/SpringBootActuatorsConfigQuery.qll
index 5f4ee632775..19cb9c30ca9 100644
--- a/java/ql/lib/semmle/code/java/security/SpringBootActuatorsConfigQuery.qll
+++ b/java/ql/lib/semmle/code/java/security/SpringBootActuatorsConfigQuery.qll
@@ -112,13 +112,16 @@ predicate exposesSensitiveEndpoint(
ep.getFile() = propFile and
ep = jpOption.asSome() and
(
- ep.getValue() = "*" // all endpoints are exposed
+ // all endpoints are exposed
+ ep.getValue() = "*"
or
- ep.getValue()
- .matches([
- "%dump%", "%trace%", "%logfile%", "%shutdown%", "%startup%", "%mappings%",
- "%env%", "%beans%", "%sessions%"
- ]) // sensitive endpoints to check although all endpoints apart from '/health' are considered sensitive by Spring
+ // version 2.x: exposes health and info only by default
+ springBootVersion.matches("2.%") and
+ not ep.getValue() = ["health", "info"]
+ or
+ // version 3.x: exposes health only by default
+ springBootVersion.matches("3.%") and
+ not ep.getValue() = "health"
)
)
)
diff --git a/java/ql/test/query-tests/security/CWE-200/semmle/tests/SpringBootActuatorsConfig/SpringBootActuatorsConfig.expected b/java/ql/test/query-tests/security/CWE-200/semmle/tests/SpringBootActuatorsConfig/SpringBootActuatorsConfig.expected
index 5b29b16b1be..345d001a1f5 100644
--- a/java/ql/test/query-tests/security/CWE-200/semmle/tests/SpringBootActuatorsConfig/SpringBootActuatorsConfig.expected
+++ b/java/ql/test/query-tests/security/CWE-200/semmle/tests/SpringBootActuatorsConfig/SpringBootActuatorsConfig.expected
@@ -1,5 +1,8 @@
| Version1.0.x-1.4.x/bad/default/pom.xml:29:9:32:22 | dependency | Insecure Spring Boot actuator $@ exposes sensitive endpoints (1.2.6.RELEASE). | file://:0:0:0:0 | (none) | configuration |
| Version1.0.x-1.4.x/bad/false/pom.xml:29:9:32:22 | dependency | Insecure Spring Boot actuator $@ exposes sensitive endpoints (1.2.6.RELEASE). | Version1.0.x-1.4.x/bad/false/application.properties:2:1:2:33 | management.security.enabled=false | configuration |
| Version1.5.x/bad/pom.xml:29:9:32:22 | dependency | Insecure Spring Boot actuator $@ exposes sensitive endpoints (1.5.6.RELEASE). | Version1.5.x/bad/application.properties:2:1:2:33 | management.security.enabled=false | configuration |
-| Version2.x/bad/pom.xml:29:9:32:22 | dependency | Insecure Spring Boot actuator $@ exposes sensitive endpoints (2.2.6.RELEASE). | Version2.x/bad/application.properties:2:1:2:43 | management.endpoints.web.exposure.include=* | configuration |
-| Version3.x/bad/pom.xml:29:9:32:22 | dependency | Insecure Spring Boot actuator $@ exposes sensitive endpoints (3.3.5). | Version3.x/bad/application.properties:2:1:2:43 | management.endpoints.web.exposure.include=* | configuration |
+| Version2.x/bad/expose/pom.xml:29:9:32:22 | dependency | Insecure Spring Boot actuator $@ exposes sensitive endpoints (2.2.6.RELEASE). | Version2.x/bad/expose/application.properties:2:1:2:33 | management.endpoints.web.expose=* | configuration |
+| Version2.x/bad/exposure-include/all-exposed/pom.xml:29:9:32:22 | dependency | Insecure Spring Boot actuator $@ exposes sensitive endpoints (2.2.6.RELEASE). | Version2.x/bad/exposure-include/all-exposed/application.properties:2:1:2:43 | management.endpoints.web.exposure.include=* | configuration |
+| Version2.x/bad/exposure-include/some-exposed/pom.xml:29:9:32:22 | dependency | Insecure Spring Boot actuator $@ exposes sensitive endpoints (2.2.6.RELEASE). | Version2.x/bad/exposure-include/some-exposed/application.properties:2:1:2:59 | management.endpoints.web.exposure.include=health,info,beans | configuration |
+| Version3.x/bad/all-exposed/pom.xml:29:9:32:22 | dependency | Insecure Spring Boot actuator $@ exposes sensitive endpoints (3.3.5). | Version3.x/bad/all-exposed/application.properties:2:1:2:43 | management.endpoints.web.exposure.include=* | configuration |
+| Version3.x/bad/some-exposed/pom.xml:29:9:32:22 | dependency | Insecure Spring Boot actuator $@ exposes sensitive endpoints (3.3.5). | Version3.x/bad/some-exposed/application.properties:2:1:2:59 | management.endpoints.web.exposure.include=health,info,beans | configuration |
diff --git a/java/ql/test/query-tests/security/CWE-200/semmle/tests/SpringBootActuatorsConfig/Version2.x/bad/exposure-include/application.properties b/java/ql/test/query-tests/security/CWE-200/semmle/tests/SpringBootActuatorsConfig/Version2.x/bad/exposure-include/all-exposed/application.properties
similarity index 100%
rename from java/ql/test/query-tests/security/CWE-200/semmle/tests/SpringBootActuatorsConfig/Version2.x/bad/exposure-include/application.properties
rename to java/ql/test/query-tests/security/CWE-200/semmle/tests/SpringBootActuatorsConfig/Version2.x/bad/exposure-include/all-exposed/application.properties
diff --git a/java/ql/test/query-tests/security/CWE-200/semmle/tests/SpringBootActuatorsConfig/Version2.x/bad/exposure-include/pom.xml b/java/ql/test/query-tests/security/CWE-200/semmle/tests/SpringBootActuatorsConfig/Version2.x/bad/exposure-include/all-exposed/pom.xml
similarity index 100%
rename from java/ql/test/query-tests/security/CWE-200/semmle/tests/SpringBootActuatorsConfig/Version2.x/bad/exposure-include/pom.xml
rename to java/ql/test/query-tests/security/CWE-200/semmle/tests/SpringBootActuatorsConfig/Version2.x/bad/exposure-include/all-exposed/pom.xml
diff --git a/java/ql/test/query-tests/security/CWE-200/semmle/tests/SpringBootActuatorsConfig/Version2.x/bad/exposure-include/some-exposed/application.properties b/java/ql/test/query-tests/security/CWE-200/semmle/tests/SpringBootActuatorsConfig/Version2.x/bad/exposure-include/some-exposed/application.properties
new file mode 100644
index 00000000000..1f29407c192
--- /dev/null
+++ b/java/ql/test/query-tests/security/CWE-200/semmle/tests/SpringBootActuatorsConfig/Version2.x/bad/exposure-include/some-exposed/application.properties
@@ -0,0 +1,2 @@
+# vulnerable configuration (spring boot 2+): exposes health and info only by default, here overridden to also expose beans
+management.endpoints.web.exposure.include=health,info,beans
\ No newline at end of file
diff --git a/java/ql/test/query-tests/security/CWE-200/semmle/tests/SpringBootActuatorsConfig/Version2.x/bad/exposure-include/some-exposed/pom.xml b/java/ql/test/query-tests/security/CWE-200/semmle/tests/SpringBootActuatorsConfig/Version2.x/bad/exposure-include/some-exposed/pom.xml
new file mode 100644
index 00000000000..c22f08d7e7e
--- /dev/null
+++ b/java/ql/test/query-tests/security/CWE-200/semmle/tests/SpringBootActuatorsConfig/Version2.x/bad/exposure-include/some-exposed/pom.xml
@@ -0,0 +1,47 @@
+
+
+ 4.0.0
+
+ spring-boot-actuator-app
+ spring-boot-actuator-app
+ 1.0-SNAPSHOT
+
+
+ UTF-8
+ 1.8
+ 1.8
+
+
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 2.2.6.RELEASE
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+ org.springframework.boot
+ spring-boot-starter-actuator
+
+
+ org.springframework.boot
+ spring-boot-devtools
+
+
+
+ org.springframework.boot
+ spring-boot-test
+
+
+
+
\ No newline at end of file
diff --git a/java/ql/test/query-tests/security/CWE-200/semmle/tests/SpringBootActuatorsConfig/Version3.x/bad/application.properties b/java/ql/test/query-tests/security/CWE-200/semmle/tests/SpringBootActuatorsConfig/Version3.x/bad/all-exposed/application.properties
similarity index 100%
rename from java/ql/test/query-tests/security/CWE-200/semmle/tests/SpringBootActuatorsConfig/Version3.x/bad/application.properties
rename to java/ql/test/query-tests/security/CWE-200/semmle/tests/SpringBootActuatorsConfig/Version3.x/bad/all-exposed/application.properties
diff --git a/java/ql/test/query-tests/security/CWE-200/semmle/tests/SpringBootActuatorsConfig/Version3.x/bad/pom.xml b/java/ql/test/query-tests/security/CWE-200/semmle/tests/SpringBootActuatorsConfig/Version3.x/bad/all-exposed/pom.xml
similarity index 100%
rename from java/ql/test/query-tests/security/CWE-200/semmle/tests/SpringBootActuatorsConfig/Version3.x/bad/pom.xml
rename to java/ql/test/query-tests/security/CWE-200/semmle/tests/SpringBootActuatorsConfig/Version3.x/bad/all-exposed/pom.xml
diff --git a/java/ql/test/query-tests/security/CWE-200/semmle/tests/SpringBootActuatorsConfig/Version3.x/bad/some-exposed/application.properties b/java/ql/test/query-tests/security/CWE-200/semmle/tests/SpringBootActuatorsConfig/Version3.x/bad/some-exposed/application.properties
new file mode 100644
index 00000000000..27d08eac74f
--- /dev/null
+++ b/java/ql/test/query-tests/security/CWE-200/semmle/tests/SpringBootActuatorsConfig/Version3.x/bad/some-exposed/application.properties
@@ -0,0 +1,2 @@
+# vulnerable configuration (spring boot 3+): exposes health only by default, here overridden to also expose info and beans
+management.endpoints.web.exposure.include=health,info,beans
\ No newline at end of file
diff --git a/java/ql/test/query-tests/security/CWE-200/semmle/tests/SpringBootActuatorsConfig/Version3.x/bad/some-exposed/pom.xml b/java/ql/test/query-tests/security/CWE-200/semmle/tests/SpringBootActuatorsConfig/Version3.x/bad/some-exposed/pom.xml
new file mode 100644
index 00000000000..12dab1d9421
--- /dev/null
+++ b/java/ql/test/query-tests/security/CWE-200/semmle/tests/SpringBootActuatorsConfig/Version3.x/bad/some-exposed/pom.xml
@@ -0,0 +1,47 @@
+
+
+ 4.0.0
+
+ spring-boot-actuator-app
+ spring-boot-actuator-app
+ 1.0-SNAPSHOT
+
+
+ UTF-8
+ 1.8
+ 1.8
+
+
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 3.3.5
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+ org.springframework.boot
+ spring-boot-starter-actuator
+
+
+ org.springframework.boot
+ spring-boot-devtools
+
+
+
+ org.springframework.boot
+ spring-boot-test
+
+
+
+
\ No newline at end of file
From 0dd33b273437cfa85904760e6f4b9366fca12a81 Mon Sep 17 00:00:00 2001
From: Jami Cogswell
Date: Sat, 19 Jul 2025 13:01:00 -0400
Subject: [PATCH 054/984] Java: remove version debugging from alert message
---
.../java/security/SpringBootActuatorsConfigQuery.qll | 3 +--
.../SpringBootActuatorsConfig.ql | 11 +++--------
2 files changed, 4 insertions(+), 10 deletions(-)
diff --git a/java/ql/lib/semmle/code/java/security/SpringBootActuatorsConfigQuery.qll b/java/ql/lib/semmle/code/java/security/SpringBootActuatorsConfigQuery.qll
index 19cb9c30ca9..163cd46d5d8 100644
--- a/java/ql/lib/semmle/code/java/security/SpringBootActuatorsConfigQuery.qll
+++ b/java/ql/lib/semmle/code/java/security/SpringBootActuatorsConfigQuery.qll
@@ -11,9 +11,8 @@ private class SpringBootParent extends Parent {
SpringBootParent() { this.getGroup().getValue() = "org.springframework.boot" }
}
-// TODO: private once done with version string debugging in alert msg.
/** A `Pom` with a Spring Boot parent node. */
-class SpringBootPom extends Pom {
+private class SpringBootPom extends Pom {
SpringBootPom() { this.getParentElement() instanceof SpringBootParent }
/** Holds if the Spring Boot Security module is used in the project. */
diff --git a/java/ql/src/Security/CWE/CWE-200/SpringBootActuatorsConfig/SpringBootActuatorsConfig.ql b/java/ql/src/Security/CWE/CWE-200/SpringBootActuatorsConfig/SpringBootActuatorsConfig.ql
index 5fb86c42b80..562298257a7 100644
--- a/java/ql/src/Security/CWE/CWE-200/SpringBootActuatorsConfig/SpringBootActuatorsConfig.ql
+++ b/java/ql/src/Security/CWE/CWE-200/SpringBootActuatorsConfig/SpringBootActuatorsConfig.ql
@@ -15,11 +15,6 @@ import java
import semmle.code.xml.MavenPom
import semmle.code.java.security.SpringBootActuatorsConfigQuery
-from SpringBootStarterActuatorDependency d, JavaPropertyOption jpOption, SpringBootPom pom
-where
- exposesSensitiveEndpoint(d, jpOption) and
- // TODO: remove pom; for debugging versions
- d = pom.getADependency()
-select d,
- "Insecure Spring Boot actuator $@ exposes sensitive endpoints (" +
- pom.getParentElement().getVersionString() + ").", jpOption, "configuration"
+from SpringBootStarterActuatorDependency d, JavaPropertyOption jpOption
+where exposesSensitiveEndpoint(d, jpOption)
+select d, "Insecure Spring Boot actuator $@ exposes sensitive endpoints.", jpOption, "configuration"
From c9692a6d105cbfc1015804f6ac891704fb1f13c4 Mon Sep 17 00:00:00 2001
From: Jami Cogswell
Date: Sat, 19 Jul 2025 13:27:09 -0400
Subject: [PATCH 055/984] Java: fix test failures cause by alert msg change
---
.../SpringBootActuatorsConfig.expected | 16 ++++++++--------
1 file changed, 8 insertions(+), 8 deletions(-)
diff --git a/java/ql/test/query-tests/security/CWE-200/semmle/tests/SpringBootActuatorsConfig/SpringBootActuatorsConfig.expected b/java/ql/test/query-tests/security/CWE-200/semmle/tests/SpringBootActuatorsConfig/SpringBootActuatorsConfig.expected
index 345d001a1f5..8845d970df2 100644
--- a/java/ql/test/query-tests/security/CWE-200/semmle/tests/SpringBootActuatorsConfig/SpringBootActuatorsConfig.expected
+++ b/java/ql/test/query-tests/security/CWE-200/semmle/tests/SpringBootActuatorsConfig/SpringBootActuatorsConfig.expected
@@ -1,8 +1,8 @@
-| Version1.0.x-1.4.x/bad/default/pom.xml:29:9:32:22 | dependency | Insecure Spring Boot actuator $@ exposes sensitive endpoints (1.2.6.RELEASE). | file://:0:0:0:0 | (none) | configuration |
-| Version1.0.x-1.4.x/bad/false/pom.xml:29:9:32:22 | dependency | Insecure Spring Boot actuator $@ exposes sensitive endpoints (1.2.6.RELEASE). | Version1.0.x-1.4.x/bad/false/application.properties:2:1:2:33 | management.security.enabled=false | configuration |
-| Version1.5.x/bad/pom.xml:29:9:32:22 | dependency | Insecure Spring Boot actuator $@ exposes sensitive endpoints (1.5.6.RELEASE). | Version1.5.x/bad/application.properties:2:1:2:33 | management.security.enabled=false | configuration |
-| Version2.x/bad/expose/pom.xml:29:9:32:22 | dependency | Insecure Spring Boot actuator $@ exposes sensitive endpoints (2.2.6.RELEASE). | Version2.x/bad/expose/application.properties:2:1:2:33 | management.endpoints.web.expose=* | configuration |
-| Version2.x/bad/exposure-include/all-exposed/pom.xml:29:9:32:22 | dependency | Insecure Spring Boot actuator $@ exposes sensitive endpoints (2.2.6.RELEASE). | Version2.x/bad/exposure-include/all-exposed/application.properties:2:1:2:43 | management.endpoints.web.exposure.include=* | configuration |
-| Version2.x/bad/exposure-include/some-exposed/pom.xml:29:9:32:22 | dependency | Insecure Spring Boot actuator $@ exposes sensitive endpoints (2.2.6.RELEASE). | Version2.x/bad/exposure-include/some-exposed/application.properties:2:1:2:59 | management.endpoints.web.exposure.include=health,info,beans | configuration |
-| Version3.x/bad/all-exposed/pom.xml:29:9:32:22 | dependency | Insecure Spring Boot actuator $@ exposes sensitive endpoints (3.3.5). | Version3.x/bad/all-exposed/application.properties:2:1:2:43 | management.endpoints.web.exposure.include=* | configuration |
-| Version3.x/bad/some-exposed/pom.xml:29:9:32:22 | dependency | Insecure Spring Boot actuator $@ exposes sensitive endpoints (3.3.5). | Version3.x/bad/some-exposed/application.properties:2:1:2:59 | management.endpoints.web.exposure.include=health,info,beans | configuration |
+| Version1.0.x-1.4.x/bad/default/pom.xml:29:9:32:22 | dependency | Insecure Spring Boot actuator $@ exposes sensitive endpoints. | file://:0:0:0:0 | (none) | configuration |
+| Version1.0.x-1.4.x/bad/false/pom.xml:29:9:32:22 | dependency | Insecure Spring Boot actuator $@ exposes sensitive endpoints. | Version1.0.x-1.4.x/bad/false/application.properties:2:1:2:33 | management.security.enabled=false | configuration |
+| Version1.5.x/bad/pom.xml:29:9:32:22 | dependency | Insecure Spring Boot actuator $@ exposes sensitive endpoints. | Version1.5.x/bad/application.properties:2:1:2:33 | management.security.enabled=false | configuration |
+| Version2.x/bad/expose/pom.xml:29:9:32:22 | dependency | Insecure Spring Boot actuator $@ exposes sensitive endpoints. | Version2.x/bad/expose/application.properties:2:1:2:33 | management.endpoints.web.expose=* | configuration |
+| Version2.x/bad/exposure-include/all-exposed/pom.xml:29:9:32:22 | dependency | Insecure Spring Boot actuator $@ exposes sensitive endpoints. | Version2.x/bad/exposure-include/all-exposed/application.properties:2:1:2:43 | management.endpoints.web.exposure.include=* | configuration |
+| Version2.x/bad/exposure-include/some-exposed/pom.xml:29:9:32:22 | dependency | Insecure Spring Boot actuator $@ exposes sensitive endpoints. | Version2.x/bad/exposure-include/some-exposed/application.properties:2:1:2:59 | management.endpoints.web.exposure.include=health,info,beans | configuration |
+| Version3.x/bad/all-exposed/pom.xml:29:9:32:22 | dependency | Insecure Spring Boot actuator $@ exposes sensitive endpoints. | Version3.x/bad/all-exposed/application.properties:2:1:2:43 | management.endpoints.web.exposure.include=* | configuration |
+| Version3.x/bad/some-exposed/pom.xml:29:9:32:22 | dependency | Insecure Spring Boot actuator $@ exposes sensitive endpoints. | Version3.x/bad/some-exposed/application.properties:2:1:2:59 | management.endpoints.web.exposure.include=health,info,beans | configuration |
From bca2c2da548d6918ce6d9fde1dac2a41a4331f87 Mon Sep 17 00:00:00 2001
From: Jami Cogswell
Date: Sat, 19 Jul 2025 19:29:00 -0400
Subject: [PATCH 056/984] Java: Add 'previous-id' and adjust tags for
'java/garbage-collection' and 'java/do-not-use-finalizers'
---
.../query-suite/java-code-quality-extended.qls.expected | 1 +
.../Undesirable Calls/CallsToRunFinalizersOnExit.ql | 7 +++++--
.../Undesirable Calls/GarbageCollection.ql | 6 ++++--
java/ql/src/change-notes/2025-07-19-adjust-tags.md | 5 +++++
4 files changed, 15 insertions(+), 4 deletions(-)
create mode 100644 java/ql/src/change-notes/2025-07-19-adjust-tags.md
diff --git a/java/ql/integration-tests/java/query-suite/java-code-quality-extended.qls.expected b/java/ql/integration-tests/java/query-suite/java-code-quality-extended.qls.expected
index 7a1a986b2aa..4a736437995 100644
--- a/java/ql/integration-tests/java/query-suite/java-code-quality-extended.qls.expected
+++ b/java/ql/integration-tests/java/query-suite/java-code-quality-extended.qls.expected
@@ -80,6 +80,7 @@ ql/java/ql/src/Violations of Best Practice/Naming Conventions/LocalShadowsFieldC
ql/java/ql/src/Violations of Best Practice/Naming Conventions/SameNameAsSuper.ql
ql/java/ql/src/Violations of Best Practice/Records/IgnoredSerializationMembersOfRecordClass.ql
ql/java/ql/src/Violations of Best Practice/SpecialCharactersInLiterals/NonExplicitControlAndWhitespaceCharsInLiterals.ql
+ql/java/ql/src/Violations of Best Practice/Undesirable Calls/CallsToRunFinalizersOnExit.ql
ql/java/ql/src/Violations of Best Practice/Undesirable Calls/CallsToStringToString.ql
ql/java/ql/src/Violations of Best Practice/Undesirable Calls/DefaultToString.ql
ql/java/ql/src/Violations of Best Practice/Undesirable Calls/DoNotCallFinalize.ql
diff --git a/java/ql/src/Violations of Best Practice/Undesirable Calls/CallsToRunFinalizersOnExit.ql b/java/ql/src/Violations of Best Practice/Undesirable Calls/CallsToRunFinalizersOnExit.ql
index c2ffe45b520..568be1805e6 100644
--- a/java/ql/src/Violations of Best Practice/Undesirable Calls/CallsToRunFinalizersOnExit.ql
+++ b/java/ql/src/Violations of Best Practice/Undesirable Calls/CallsToRunFinalizersOnExit.ql
@@ -7,8 +7,11 @@
* @problem.severity error
* @precision medium
* @id java/run-finalizers-on-exit
- * @tags reliability
- * maintainability
+ * @previous-id java/do-not-use-finalizers
+ * @tags quality
+ * reliability
+ * correctness
+ * performance
*/
import java
diff --git a/java/ql/src/Violations of Best Practice/Undesirable Calls/GarbageCollection.ql b/java/ql/src/Violations of Best Practice/Undesirable Calls/GarbageCollection.ql
index 1067bdcb6dc..620177cc58c 100644
--- a/java/ql/src/Violations of Best Practice/Undesirable Calls/GarbageCollection.ql
+++ b/java/ql/src/Violations of Best Practice/Undesirable Calls/GarbageCollection.ql
@@ -6,8 +6,10 @@
* @problem.severity recommendation
* @precision low
* @id java/garbage-collection
- * @tags reliability
- * maintainability
+ * @previous-id java/do-not-use-finalizers
+ * @tags quality
+ * reliability
+ * correctness
*/
import java
diff --git a/java/ql/src/change-notes/2025-07-19-adjust-tags.md b/java/ql/src/change-notes/2025-07-19-adjust-tags.md
new file mode 100644
index 00000000000..0067adebdaf
--- /dev/null
+++ b/java/ql/src/change-notes/2025-07-19-adjust-tags.md
@@ -0,0 +1,5 @@
+---
+category: queryMetadata
+---
+* The tag `maintainability` has been removed from `java/run-finalizers-on-exit` and the tags `quality`, `correctness`, and `performance` have been added.
+* The tag `maintainability` has been removed from `java/garbage-collection` and the tags `quality` and `correctness` have been added.
From b4b848a25c8663942af6c80ee18fda7ee2acaf09 Mon Sep 17 00:00:00 2001
From: Kevin Stubbings
Date: Mon, 21 Jul 2025 21:53:35 +0000
Subject: [PATCH 057/984] Fix tests and simplify sanitizer
---
go/ql/lib/ext/os.model.yml | 1 +
.../lib/semmle/go/security/TaintedPathCustomizations.qll | 9 +--------
.../semmle/go/frameworks/StdlibTaintFlow/Os.go | 2 +-
go/ql/test/query-tests/Security/CWE-022/TaintedPath.go | 2 +-
4 files changed, 4 insertions(+), 10 deletions(-)
diff --git a/go/ql/lib/ext/os.model.yml b/go/ql/lib/ext/os.model.yml
index 66316b4ff35..7d2070b53ba 100644
--- a/go/ql/lib/ext/os.model.yml
+++ b/go/ql/lib/ext/os.model.yml
@@ -28,6 +28,7 @@ extensions:
- ["os", "", False, "ReadDir", "", "", "Argument[0]", "path-injection", "manual"]
- ["os", "", False, "ReadFile", "", "", "Argument[0]", "path-injection", "manual"]
- ["os", "", False, "MkdirTemp", "", "", "Argument[0..1]", "path-injection", "manual"]
+ - ["os", "", False, "CreateTemp", "", "", "Argument[0]", "path-injection", "manual"]
- ["os", "", False, "WriteFile", "", "", "Argument[0]", "path-injection", "manual"]
# command-injection
- ["os", "", False, "StartProcess", "", "", "Argument[0]", "command-injection", "manual"]
diff --git a/go/ql/lib/semmle/go/security/TaintedPathCustomizations.qll b/go/ql/lib/semmle/go/security/TaintedPathCustomizations.qll
index 760de2d9c54..ac6ea8c9835 100644
--- a/go/ql/lib/semmle/go/security/TaintedPathCustomizations.qll
+++ b/go/ql/lib/semmle/go/security/TaintedPathCustomizations.qll
@@ -87,14 +87,7 @@ module TaintedPath {
exists(DataFlow::CallNode cleanCall, StringOps::Concatenation concatNode |
cleanCall = any(Function f | f.hasQualifiedName("path/filepath", "Clean")).getACall() and
concatNode = cleanCall.getArgument(0) and
- (
- concatNode.getOperand(0).asExpr().(StringLit).getValue() = "/"
- or
- exists(DeclaredConstant dc |
- dc.hasQualifiedName("os", "PathSeparator") and
- dc.getAReference() = concatNode.getOperand(0).asExpr().getAChildExpr*()
- )
- ) and
+ concatNode.getOperand(0).getStringValue().prefix(1) = ["/", "\\"] and
this = cleanCall.getResult()
)
}
diff --git a/go/ql/test/library-tests/semmle/go/frameworks/StdlibTaintFlow/Os.go b/go/ql/test/library-tests/semmle/go/frameworks/StdlibTaintFlow/Os.go
index b27c5d1f47c..859a3bbd3bd 100644
--- a/go/ql/test/library-tests/semmle/go/frameworks/StdlibTaintFlow/Os.go
+++ b/go/ql/test/library-tests/semmle/go/frameworks/StdlibTaintFlow/Os.go
@@ -178,6 +178,6 @@ func fsAccesses() {
os.ReadDir(path) // $ fsaccess=path
os.ReadFile(path) // $ fsaccess=path
os.MkdirTemp(path, part) // $ fsaccess=path fsaccess=part
- os.CreateTemp(path, part) // $ fsaccess=path fsaccess=part
+ os.CreateTemp(path, part) // $ fsaccess=path
os.WriteFile(path, []byte{}, 0600) // $ fsaccess=path
}
diff --git a/go/ql/test/query-tests/Security/CWE-022/TaintedPath.go b/go/ql/test/query-tests/Security/CWE-022/TaintedPath.go
index 3949d8408a1..a6519acea00 100644
--- a/go/ql/test/query-tests/Security/CWE-022/TaintedPath.go
+++ b/go/ql/test/query-tests/Security/CWE-022/TaintedPath.go
@@ -66,7 +66,7 @@ func handler(w http.ResponseWriter, r *http.Request) {
// GOOD: Sanitized by filepath.Clean with a prepended os.PathSeparator forcing interpretation
// as an absolute path, so that Clean will throw away any leading `..` components.
- data, _ = ioutil.ReadFile(filepath.Clean(string(os.PathSeparator) + tainted_path))
+ data, _ = ioutil.ReadFile(filepath.Clean(string(os.PathSeparator) + "hardcoded" + tainted_path))
w.Write(data)
// BAD: Sanitized by path.Clean with a prepended '/' forcing interpretation
From e2f3c9d1b6c84b48c3d3dbcef86ce9b8ec02918a Mon Sep 17 00:00:00 2001
From: Owen Mansel-Chan <62447351+owen-mc@users.noreply.github.com>
Date: Tue, 22 Jul 2025 00:09:37 +0100
Subject: [PATCH 058/984] Reword change note
---
go/ql/lib/change-notes/2025-07-15-path-injection-sanitizers.md | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/go/ql/lib/change-notes/2025-07-15-path-injection-sanitizers.md b/go/ql/lib/change-notes/2025-07-15-path-injection-sanitizers.md
index 69596cf98d9..004eb973de3 100644
--- a/go/ql/lib/change-notes/2025-07-15-path-injection-sanitizers.md
+++ b/go/ql/lib/change-notes/2025-07-15-path-injection-sanitizers.md
@@ -1,4 +1,5 @@
---
category: minorAnalysis
---
-* Remove model `CreateTemp` function, from the `os` package, as a path-injection sink due to proper sanitization by Go. Add check for `os.PathSeparator` in sanitizers for path-injection query.
\ No newline at end of file
+* The second argument of the `CreateTemp` function, from the `os` package, is no longer a path-injection sink due to proper sanitization by Go.
+* The query "Uncontrolled data used in path expression" (`go/path-injection`) now detects sanitizing a path by adding `os.PathSeparator` or `\` to the beginning.
\ No newline at end of file
From 73d257e538c5fefb3dcad71ef4f69f23c6f49262 Mon Sep 17 00:00:00 2001
From: Joe Farebrother
Date: Wed, 23 Jul 2025 14:13:58 +0100
Subject: [PATCH 059/984] Port unexpected raise away from pointsto
---
.../IncorrectRaiseInSpecialMethod.ql | 160 ++++++++++++------
1 file changed, 105 insertions(+), 55 deletions(-)
diff --git a/python/ql/src/Functions/IncorrectRaiseInSpecialMethod.ql b/python/ql/src/Functions/IncorrectRaiseInSpecialMethod.ql
index 4bf52af9061..5df5f64116e 100644
--- a/python/ql/src/Functions/IncorrectRaiseInSpecialMethod.ql
+++ b/python/ql/src/Functions/IncorrectRaiseInSpecialMethod.ql
@@ -12,16 +12,18 @@
*/
import python
+import semmle.python.ApiGraphs
+import semmle.python.dataflow.new.internal.DataFlowDispatch
-private predicate attribute_method(string name) {
+private predicate attributeMethod(string name) {
name = "__getattribute__" or name = "__getattr__" or name = "__setattr__"
}
-private predicate indexing_method(string name) {
+private predicate indexingMethod(string name) {
name = "__getitem__" or name = "__setitem__" or name = "__delitem__"
}
-private predicate arithmetic_method(string name) {
+private predicate arithmeticMethod(string name) {
name in [
"__add__", "__sub__", "__or__", "__xor__", "__rshift__", "__pow__", "__mul__", "__neg__",
"__radd__", "__rsub__", "__rdiv__", "__rfloordiv__", "__div__", "__rdiv__", "__rlshift__",
@@ -32,7 +34,7 @@ private predicate arithmetic_method(string name) {
]
}
-private predicate ordering_method(string name) {
+private predicate orderingMethod(string name) {
name = "__lt__"
or
name = "__le__"
@@ -40,13 +42,9 @@ private predicate ordering_method(string name) {
name = "__gt__"
or
name = "__ge__"
- or
- name = "__cmp__" and major_version() = 2
}
-private predicate cast_method(string name) {
- name = "__nonzero__" and major_version() = 2
- or
+private predicate castMethod(string name) {
name = "__int__"
or
name = "__float__"
@@ -58,63 +56,115 @@ private predicate cast_method(string name) {
name = "__complex__"
}
-predicate correct_raise(string name, ClassObject ex) {
- ex.getAnImproperSuperType() = theTypeErrorType() and
+predicate correctRaise(string name, Expr exec) {
+ execIsOfType(exec, "TypeError") and
(
- name = "__copy__" or
- name = "__deepcopy__" or
- name = "__call__" or
- indexing_method(name) or
- attribute_method(name)
+ indexingMethod(name) or
+ attributeMethod(name)
)
or
- preferred_raise(name, ex)
- or
- preferred_raise(name, ex.getASuperType())
+ exists(string execName |
+ preferredRaise(name, execName, _) and
+ execIsOfType(exec, execName)
+ )
}
-predicate preferred_raise(string name, ClassObject ex) {
- attribute_method(name) and ex = theAttributeErrorType()
+predicate preferredRaise(string name, string execName, string message) {
+ // TODO: execName should be an IPA type
+ attributeMethod(name) and
+ execName = "AttributeError" and
+ message = "should raise an AttributeError instead."
or
- indexing_method(name) and ex = Object::builtin("LookupError")
+ indexingMethod(name) and
+ execName = "LookupError" and
+ message = "should raise a LookupError (KeyError or IndexError) instead."
or
- ordering_method(name) and ex = theTypeErrorType()
+ orderingMethod(name) and
+ execName = "TypeError" and
+ message = "should raise a TypeError, or return NotImplemented instead."
or
- arithmetic_method(name) and ex = Object::builtin("ArithmeticError")
+ arithmeticMethod(name) and
+ execName = "ArithmeticError" and
+ message = "should raise an ArithmeticError, or return NotImplemented instead."
or
- name = "__bool__" and ex = theTypeErrorType()
+ name = "__bool__" and
+ execName = "TypeError" and
+ message = "should raise a TypeError instead."
}
-predicate no_need_to_raise(string name, string message) {
- name = "__hash__" and message = "use __hash__ = None instead"
- or
- cast_method(name) and message = "there is no need to implement the method at all."
-}
-
-predicate is_abstract(FunctionObject func) {
- func.getFunction().getADecorator().(Name).getId().matches("%abstract%")
-}
-
-predicate always_raises(FunctionObject f, ClassObject ex) {
- ex = f.getARaisedType() and
- strictcount(f.getARaisedType()) = 1 and
- not exists(f.getFunction().getANormalExit()) and
- /* raising StopIteration is equivalent to a return in a generator */
- not ex = theStopIterationType()
-}
-
-from FunctionObject f, ClassObject cls, string message
-where
- f.getFunction().isSpecialMethod() and
- not is_abstract(f) and
- always_raises(f, cls) and
- (
- no_need_to_raise(f.getName(), message) and not cls.getName() = "NotImplementedError"
+predicate execIsOfType(Expr exec, string execName) {
+ exists(string subclass |
+ execName = "TypeError" and
+ subclass = "TypeError"
or
- not correct_raise(f.getName(), cls) and
- not cls.getName() = "NotImplementedError" and
- exists(ClassObject preferred | preferred_raise(f.getName(), preferred) |
- message = "raise " + preferred.getName() + " instead"
+ execName = "LookupError" and
+ subclass = ["LookupError", "KeyError", "IndexError"]
+ or
+ execName = "ArithmeticError" and
+ subclass = ["ArithmeticError", "FloatingPointError", "OverflowError", "ZeroDivisionError"]
+ or
+ execName = "AttributeError" and
+ subclass = "AttributeError"
+ |
+ exec = API::builtin(subclass).getACall().asExpr()
+ or
+ exec = API::builtin(subclass).getASubclass().getACall().asExpr()
+ )
+}
+
+predicate noNeedToAlwaysRaise(Function meth, string message, boolean allowNotImplemented) {
+ meth.getName() = "__hash__" and
+ message = "use __hash__ = None instead." and
+ allowNotImplemented = false
+ or
+ castMethod(meth.getName()) and
+ message = "this method does not need to be implemented." and
+ allowNotImplemented = true and
+ not exists(Function overridden |
+ overridden.getName() = meth.getName() and
+ overridden.getScope() = getADirectSuperclass+(meth.getScope()) and
+ alwaysRaises(overridden, _)
+ )
+}
+
+predicate isAbstract(Function func) { func.getADecorator().(Name).getId().matches("%abstract%") }
+
+predicate alwaysRaises(Function f, Expr exec) {
+ directlyRaises(f, exec) and
+ strictcount(Expr e | directlyRaises(f, e)) = 1 and
+ not exists(f.getANormalExit())
+}
+
+predicate directlyRaises(Function f, Expr exec) {
+ exists(Raise r |
+ r.getScope() = f and
+ exec = r.getException() and
+ not exec = API::builtin("StopIteration").asSource().asExpr()
+ )
+}
+
+predicate isNotImplementedError(Expr exec) {
+ exec = API::builtin("NotImplementedError").getACall().asExpr()
+}
+
+from Function f, Expr exec, string message
+where
+ f.isSpecialMethod() and
+ not isAbstract(f) and
+ directlyRaises(f, exec) and
+ (
+ exists(boolean allowNotImplemented, string subMessage |
+ alwaysRaises(f, exec) and
+ noNeedToAlwaysRaise(f, subMessage, allowNotImplemented) and
+ (allowNotImplemented = false or not isNotImplementedError(exec)) and
+ message = "This method always raises $@ - " + subMessage
+ )
+ or
+ alwaysRaises(f, exec) and // for now consider only alwaysRaises cases as original query
+ not isNotImplementedError(exec) and
+ not correctRaise(f.getName(), exec) and
+ exists(string subMessage | preferredRaise(f.getName(), _, subMessage) |
+ message = "This method always raises $@ - " + subMessage
)
)
-select f, "Function always raises $@; " + message, cls, cls.toString()
+select f, message, exec, exec.toString() // TODO: remove tostring
From b9738066de1d9d67f73b452a16c3aea22e3a0470 Mon Sep 17 00:00:00 2001
From: Joe Farebrother
Date: Thu, 24 Jul 2025 11:18:28 +0100
Subject: [PATCH 060/984] try excluding set methods, add methods, update alert
messages
---
.../IncorrectRaiseInSpecialMethod.ql | 55 ++++++++++---------
1 file changed, 29 insertions(+), 26 deletions(-)
diff --git a/python/ql/src/Functions/IncorrectRaiseInSpecialMethod.ql b/python/ql/src/Functions/IncorrectRaiseInSpecialMethod.ql
index 5df5f64116e..0c61b0cf775 100644
--- a/python/ql/src/Functions/IncorrectRaiseInSpecialMethod.ql
+++ b/python/ql/src/Functions/IncorrectRaiseInSpecialMethod.ql
@@ -16,15 +16,16 @@ import semmle.python.ApiGraphs
import semmle.python.dataflow.new.internal.DataFlowDispatch
private predicate attributeMethod(string name) {
- name = "__getattribute__" or name = "__getattr__" or name = "__setattr__"
+ name = ["__getattribute__", "__getattr__"] // __setattr__ excluded as it makes sense to raise different kinds of errors based on the `value` parameter
}
private predicate indexingMethod(string name) {
- name = "__getitem__" or name = "__setitem__" or name = "__delitem__"
+ name = ["__getitem__", "__delitem__"] // __setitem__ excluded as it makes sense to raise different kinds of errors based on the `value` parameter
}
private predicate arithmeticMethod(string name) {
- name in [
+ name =
+ [
"__add__", "__sub__", "__or__", "__xor__", "__rshift__", "__pow__", "__mul__", "__neg__",
"__radd__", "__rsub__", "__rdiv__", "__rfloordiv__", "__div__", "__rdiv__", "__rlshift__",
"__rand__", "__ror__", "__rxor__", "__rrshift__", "__rpow__", "__rmul__", "__truediv__",
@@ -35,32 +36,32 @@ private predicate arithmeticMethod(string name) {
}
private predicate orderingMethod(string name) {
- name = "__lt__"
- or
- name = "__le__"
- or
- name = "__gt__"
- or
- name = "__ge__"
+ name =
+ [
+ "__lt__",
+ "__le__",
+ "__gt__",
+ "__ge__",
+ ]
}
private predicate castMethod(string name) {
- name = "__int__"
- or
- name = "__float__"
- or
- name = "__long__"
- or
- name = "__trunc__"
- or
- name = "__complex__"
+ name =
+ [
+ "__int__",
+ "__float__",
+ "__long__",
+ "__trunc__",
+ "__complex__"
+ ]
}
predicate correctRaise(string name, Expr exec) {
execIsOfType(exec, "TypeError") and
(
indexingMethod(name) or
- attributeMethod(name)
+ attributeMethod(name) or
+ name = ["__add__", "__iadd__", "__radd__"]
)
or
exists(string execName |
@@ -81,11 +82,11 @@ predicate preferredRaise(string name, string execName, string message) {
or
orderingMethod(name) and
execName = "TypeError" and
- message = "should raise a TypeError, or return NotImplemented instead."
+ message = "should raise a TypeError or return NotImplemented instead."
or
arithmeticMethod(name) and
execName = "ArithmeticError" and
- message = "should raise an ArithmeticError, or return NotImplemented instead."
+ message = "should raise an ArithmeticError or return NotImplemented instead."
or
name = "__bool__" and
execName = "TypeError" and
@@ -120,6 +121,7 @@ predicate noNeedToAlwaysRaise(Function meth, string message, boolean allowNotImp
castMethod(meth.getName()) and
message = "this method does not need to be implemented." and
allowNotImplemented = true and
+ // Allow an always raising cast method if it's overriding other behavior
not exists(Function overridden |
overridden.getName() = meth.getName() and
overridden.getScope() = getADirectSuperclass+(meth.getScope()) and
@@ -139,7 +141,7 @@ predicate directlyRaises(Function f, Expr exec) {
exists(Raise r |
r.getScope() = f and
exec = r.getException() and
- not exec = API::builtin("StopIteration").asSource().asExpr()
+ exec instanceof Call
)
}
@@ -156,15 +158,16 @@ where
exists(boolean allowNotImplemented, string subMessage |
alwaysRaises(f, exec) and
noNeedToAlwaysRaise(f, subMessage, allowNotImplemented) and
- (allowNotImplemented = false or not isNotImplementedError(exec)) and
+ (allowNotImplemented = true implies not isNotImplementedError(exec)) and // don't alert if it's a NotImplementedError and that's ok
message = "This method always raises $@ - " + subMessage
)
or
- alwaysRaises(f, exec) and // for now consider only alwaysRaises cases as original query
not isNotImplementedError(exec) and
not correctRaise(f.getName(), exec) and
exists(string subMessage | preferredRaise(f.getName(), _, subMessage) |
- message = "This method always raises $@ - " + subMessage
+ if alwaysRaises(f, exec)
+ then message = "This method always raises $@ - " + subMessage
+ else message = "This method raises $@ - " + subMessage
)
)
select f, message, exec, exec.toString() // TODO: remove tostring
From b9f6657adedbbc121ad63f32ee8ce5b1133a0aa1 Mon Sep 17 00:00:00 2001
From: Joe Farebrother
Date: Thu, 24 Jul 2025 13:50:27 +0100
Subject: [PATCH 061/984] Remove use of toString. This does also reduce reaults
from cases where the exception is not a simple identifier.
---
python/ql/src/Functions/IncorrectRaiseInSpecialMethod.ql | 6 ++++--
1 file changed, 4 insertions(+), 2 deletions(-)
diff --git a/python/ql/src/Functions/IncorrectRaiseInSpecialMethod.ql b/python/ql/src/Functions/IncorrectRaiseInSpecialMethod.ql
index 0c61b0cf775..ca1996a1e01 100644
--- a/python/ql/src/Functions/IncorrectRaiseInSpecialMethod.ql
+++ b/python/ql/src/Functions/IncorrectRaiseInSpecialMethod.ql
@@ -71,7 +71,6 @@ predicate correctRaise(string name, Expr exec) {
}
predicate preferredRaise(string name, string execName, string message) {
- // TODO: execName should be an IPA type
attributeMethod(name) and
execName = "AttributeError" and
message = "should raise an AttributeError instead."
@@ -94,6 +93,7 @@ predicate preferredRaise(string name, string execName, string message) {
}
predicate execIsOfType(Expr exec, string execName) {
+ // Might make sense to have execName be an IPA type here. Or part of a more general API modelling builtin/stdlib subclass relations.
exists(string subclass |
execName = "TypeError" and
subclass = "TypeError"
@@ -149,6 +149,8 @@ predicate isNotImplementedError(Expr exec) {
exec = API::builtin("NotImplementedError").getACall().asExpr()
}
+string getExecName(Expr exec) { result = exec.(Call).getFunc().(Name).getId() }
+
from Function f, Expr exec, string message
where
f.isSpecialMethod() and
@@ -170,4 +172,4 @@ where
else message = "This method raises $@ - " + subMessage
)
)
-select f, message, exec, exec.toString() // TODO: remove tostring
+select f, message, exec, getExecName(exec)
From 362bfba0496e494c85177eb8771cccbc1ac12c58 Mon Sep 17 00:00:00 2001
From: Joe Farebrother
Date: Thu, 24 Jul 2025 14:50:36 +0100
Subject: [PATCH 062/984] Update unit tests
---
.../IncorrectRaiseInSpecialMethod.ql | 9 +--
.../IncorrectRaiseInSpecialMethod.expected | 6 ++
.../IncorrectRaiseInSpecialMethod.qlref | 2 +
.../IncorrectRaiseInSpcialMethod/test.py | 66 +++++++++++++++++++
.../IncorrectRaiseInSpecialMethod.expected | 3 -
.../IncorrectRaiseInSpecialMethod.qlref | 1 -
6 files changed, 79 insertions(+), 8 deletions(-)
create mode 100644 python/ql/test/query-tests/Functions/IncorrectRaiseInSpcialMethod/IncorrectRaiseInSpecialMethod.expected
create mode 100644 python/ql/test/query-tests/Functions/IncorrectRaiseInSpcialMethod/IncorrectRaiseInSpecialMethod.qlref
create mode 100644 python/ql/test/query-tests/Functions/IncorrectRaiseInSpcialMethod/test.py
delete mode 100644 python/ql/test/query-tests/Functions/general/IncorrectRaiseInSpecialMethod.expected
delete mode 100644 python/ql/test/query-tests/Functions/general/IncorrectRaiseInSpecialMethod.qlref
diff --git a/python/ql/src/Functions/IncorrectRaiseInSpecialMethod.ql b/python/ql/src/Functions/IncorrectRaiseInSpecialMethod.ql
index ca1996a1e01..12107821aa6 100644
--- a/python/ql/src/Functions/IncorrectRaiseInSpecialMethod.ql
+++ b/python/ql/src/Functions/IncorrectRaiseInSpecialMethod.ql
@@ -7,7 +7,7 @@
* error-handling
* @problem.severity recommendation
* @sub-severity high
- * @precision very-high
+ * @precision high
* @id py/unexpected-raise-in-special-method
*/
@@ -16,7 +16,7 @@ import semmle.python.ApiGraphs
import semmle.python.dataflow.new.internal.DataFlowDispatch
private predicate attributeMethod(string name) {
- name = ["__getattribute__", "__getattr__"] // __setattr__ excluded as it makes sense to raise different kinds of errors based on the `value` parameter
+ name = ["__getattribute__", "__getattr__", "__delattr__"] // __setattr__ excluded as it makes sense to raise different kinds of errors based on the `value` parameter
}
private predicate indexingMethod(string name) {
@@ -50,7 +50,7 @@ private predicate castMethod(string name) {
[
"__int__",
"__float__",
- "__long__",
+ "__index__",
"__trunc__",
"__complex__"
]
@@ -61,6 +61,7 @@ predicate correctRaise(string name, Expr exec) {
(
indexingMethod(name) or
attributeMethod(name) or
+ // Allow add methods to raise a TypeError, as they can be used for sequence concatenation as well as arithmetic
name = ["__add__", "__iadd__", "__radd__"]
)
or
@@ -125,7 +126,7 @@ predicate noNeedToAlwaysRaise(Function meth, string message, boolean allowNotImp
not exists(Function overridden |
overridden.getName() = meth.getName() and
overridden.getScope() = getADirectSuperclass+(meth.getScope()) and
- alwaysRaises(overridden, _)
+ not alwaysRaises(overridden, _)
)
}
diff --git a/python/ql/test/query-tests/Functions/IncorrectRaiseInSpcialMethod/IncorrectRaiseInSpecialMethod.expected b/python/ql/test/query-tests/Functions/IncorrectRaiseInSpcialMethod/IncorrectRaiseInSpecialMethod.expected
new file mode 100644
index 00000000000..3907a725ee1
--- /dev/null
+++ b/python/ql/test/query-tests/Functions/IncorrectRaiseInSpcialMethod/IncorrectRaiseInSpecialMethod.expected
@@ -0,0 +1,6 @@
+| test.py:6:5:6:33 | Function __getitem__ | This method always raises $@ - should raise a LookupError (KeyError or IndexError) instead. | test.py:7:15:7:33 | ZeroDivisionError() | ZeroDivisionError |
+| test.py:9:5:9:32 | Function __getattr__ | This method always raises $@ - should raise an AttributeError instead. | test.py:10:15:10:33 | ZeroDivisionError() | ZeroDivisionError |
+| test.py:12:5:12:23 | Function __bool__ | This method always raises $@ - should raise a TypeError instead. | test.py:13:15:13:26 | ValueError() | ValueError |
+| test.py:15:5:15:22 | Function __int__ | This method always raises $@ - this method does not need to be implemented. | test.py:16:15:16:26 | ValueError() | ValueError |
+| test.py:24:5:24:23 | Function __hash__ | This method always raises $@ - use __hash__ = None instead. | test.py:25:15:25:35 | NotImplementedError() | NotImplementedError |
+| test.py:28:5:28:29 | Function __sub__ | This method raises $@ - should raise an ArithmeticError or return NotImplemented instead. | test.py:30:19:30:29 | TypeError() | TypeError |
diff --git a/python/ql/test/query-tests/Functions/IncorrectRaiseInSpcialMethod/IncorrectRaiseInSpecialMethod.qlref b/python/ql/test/query-tests/Functions/IncorrectRaiseInSpcialMethod/IncorrectRaiseInSpecialMethod.qlref
new file mode 100644
index 00000000000..a81e499ea66
--- /dev/null
+++ b/python/ql/test/query-tests/Functions/IncorrectRaiseInSpcialMethod/IncorrectRaiseInSpecialMethod.qlref
@@ -0,0 +1,2 @@
+query: Functions/IncorrectRaiseInSpecialMethod.ql
+postprocess: utils/test/InlineExpectationsTestQuery.ql
\ No newline at end of file
diff --git a/python/ql/test/query-tests/Functions/IncorrectRaiseInSpcialMethod/test.py b/python/ql/test/query-tests/Functions/IncorrectRaiseInSpcialMethod/test.py
new file mode 100644
index 00000000000..d5b1bc585f6
--- /dev/null
+++ b/python/ql/test/query-tests/Functions/IncorrectRaiseInSpcialMethod/test.py
@@ -0,0 +1,66 @@
+class A:
+
+ def __add__(self, other): # No alert - Always allow NotImplementedError
+ raise NotImplementedError()
+
+ def __getitem__(self, index): # $ Alert
+ raise ZeroDivisionError()
+
+ def __getattr__(self, name): # $ Alert
+ raise ZeroDivisionError()
+
+ def __bool__(self): # $ Alert
+ raise ValueError()
+
+ def __int__(self): # $ Alert # Cast method need not be defined to always raise
+ raise ValueError()
+
+ def __float__(self): # No alert - OK to raise conditionally
+ if self.z:
+ return 0
+ else:
+ raise ValueError()
+
+ def __hash__(self): # $ Alert # should use __hash__=None rather than stub implementation to make class unhashable
+ raise NotImplementedError()
+
+class B:
+ def __sub__(self, other): # $ Alert # should return NotImplemented instead
+ if not isinstance(other,B):
+ raise TypeError()
+ return self
+
+ def __add__(self, other): # No alert - allow add to raise a TypeError, as it is sometimes used for sequence concatenation as well as arithmetic
+ if not isinstance(other,B):
+ raise TypeError()
+ return self
+
+ def __setitem__(self, key, val): # No alert - allow setitem to raise arbitrary exceptions as they could be due to the value, rather than a LookupError relating to the key
+ if val < 0:
+ raise ValueError()
+
+ def __getitem__(self, key): # No alert - indexing method allowed to raise TypeError or subclasses of LookupError.
+ if not isinstance(key, int):
+ raise TypeError()
+ if key < 0:
+ raise KeyError()
+ return 3
+
+ def __getattribute__(self, name):
+ if name != "a":
+ raise AttributeError()
+ return 2
+
+ def __div__(self, other):
+ if other == 0:
+ raise ZeroDivisionError()
+ return self
+
+
+class D:
+ def __int__(self):
+ return 2
+
+class E(D):
+ def __int__(self): # No alert - cast method may override to raise exception
+ raise TypeError()
\ No newline at end of file
diff --git a/python/ql/test/query-tests/Functions/general/IncorrectRaiseInSpecialMethod.expected b/python/ql/test/query-tests/Functions/general/IncorrectRaiseInSpecialMethod.expected
deleted file mode 100644
index dd4429de02e..00000000000
--- a/python/ql/test/query-tests/Functions/general/IncorrectRaiseInSpecialMethod.expected
+++ /dev/null
@@ -1,3 +0,0 @@
-| protocols.py:98:5:98:33 | Function __getitem__ | Function always raises $@; raise LookupError instead | file://:Compiled Code:0:0:0:0 | builtin-class ZeroDivisionError | builtin-class ZeroDivisionError |
-| protocols.py:101:5:101:26 | Function __getattr__ | Function always raises $@; raise AttributeError instead | file://:Compiled Code:0:0:0:0 | builtin-class ZeroDivisionError | builtin-class ZeroDivisionError |
-| protocols.py:104:5:104:23 | Function __bool__ | Function always raises $@; raise TypeError instead | file://:Compiled Code:0:0:0:0 | builtin-class ZeroDivisionError | builtin-class ZeroDivisionError |
diff --git a/python/ql/test/query-tests/Functions/general/IncorrectRaiseInSpecialMethod.qlref b/python/ql/test/query-tests/Functions/general/IncorrectRaiseInSpecialMethod.qlref
deleted file mode 100644
index 07fd22a9376..00000000000
--- a/python/ql/test/query-tests/Functions/general/IncorrectRaiseInSpecialMethod.qlref
+++ /dev/null
@@ -1 +0,0 @@
-Functions/IncorrectRaiseInSpecialMethod.ql
\ No newline at end of file
From 871688f02617921452a77f50aba33fd8c5b4dbe5 Mon Sep 17 00:00:00 2001
From: Joe Farebrother
Date: Thu, 24 Jul 2025 16:01:57 +0100
Subject: [PATCH 063/984] Update docs
---
.../IncorrectRaiseInSpecialMethod.py | 16 --------
.../IncorrectRaiseInSpecialMethod.qhelp | 40 +++++++++----------
.../IncorrectRaiseInSpecialMethod2.py | 15 -------
.../IncorrectRaiseInSpecialMethod3.py | 27 -------------
.../examples/IncorrectRaiseInSpecialMethod.py | 22 ++++++++++
.../IncorrectRaiseInSpecialMethod2.py | 7 ++++
.../IncorrectRaiseInSpecialMethod3.py | 4 ++
7 files changed, 52 insertions(+), 79 deletions(-)
delete mode 100644 python/ql/src/Functions/IncorrectRaiseInSpecialMethod.py
delete mode 100644 python/ql/src/Functions/IncorrectRaiseInSpecialMethod2.py
delete mode 100644 python/ql/src/Functions/IncorrectRaiseInSpecialMethod3.py
create mode 100644 python/ql/src/Functions/examples/IncorrectRaiseInSpecialMethod.py
create mode 100644 python/ql/src/Functions/examples/IncorrectRaiseInSpecialMethod2.py
create mode 100644 python/ql/src/Functions/examples/IncorrectRaiseInSpecialMethod3.py
diff --git a/python/ql/src/Functions/IncorrectRaiseInSpecialMethod.py b/python/ql/src/Functions/IncorrectRaiseInSpecialMethod.py
deleted file mode 100644
index e76c27145db..00000000000
--- a/python/ql/src/Functions/IncorrectRaiseInSpecialMethod.py
+++ /dev/null
@@ -1,16 +0,0 @@
-#Incorrect unhashable class
-class MyMutableThing(object):
-
- def __init__(self):
- pass
-
- def __hash__(self):
- raise NotImplementedError("%r is unhashable" % self)
-
-#Make class unhashable in the standard way
-class MyCorrectMutableThing(object):
-
- def __init__(self):
- pass
-
- __hash__ = None
diff --git a/python/ql/src/Functions/IncorrectRaiseInSpecialMethod.qhelp b/python/ql/src/Functions/IncorrectRaiseInSpecialMethod.qhelp
index f4f0cd6920a..a0c3463b9d1 100644
--- a/python/ql/src/Functions/IncorrectRaiseInSpecialMethod.qhelp
+++ b/python/ql/src/Functions/IncorrectRaiseInSpecialMethod.qhelp
@@ -9,7 +9,7 @@ When the expression a + b is evaluated the Python virtual machine w
is not implemented it will call type(b).__radd__(b, a).
Since the virtual machine calls these special methods for common expressions, users of the class will expect these operations to raise standard exceptions.
-For example, users would expect that the expression a.b might raise an AttributeError
+For example, users would expect that the expression a.b may raise an AttributeError
if the object a does not have an attribute b.
If a KeyError were raised instead,
then this would be unexpected and may break code that expected an AttributeError, but not a KeyError.
@@ -20,18 +20,18 @@ Therefore, if a method is unable to perform the expected operation then its resp
-
Attribute access, a.b: Raise AttributeError
-
Arithmetic operations, a + b: Do not raise an exception, return NotImplemented instead.
-
Indexing, a[b]: Raise KeyError.
-
Hashing, hash(a): Use __hash__ = None to indicate that an object is unhashable.
-
Equality methods, a != b: Never raise an exception, always return True or False.
-
Ordering comparison methods, a < b: Raise a TypeError if the objects cannot be ordered.
Arithmetic operations, a + b (__add__): Do not raise an exception, return NotImplemented instead.
+
Indexing, a[b] (__getitem__): Raise KeyError or IndexError.
+
Hashing, hash(a) (__hash__): Should not raise an exception. Use __hash__ = None to indicate that an object is unhashable rather than raising an exception.
+
Equality methods, a == b (__eq__): Never raise an exception, always return True or False.
+
Ordering comparison methods, a < b (__lt__): Raise a TypeError if the objects cannot be ordered.
Most others: Ideally, do not implement the method at all, otherwise raise TypeError to indicate that the operation is unsupported.
-
If the method is meant to be abstract, then declare it so using the @abstractmethod decorator.
+
If the method is intended to be abstract, then declare it so using the @abstractmethod decorator.
Otherwise, either remove the method or ensure that the method raises an exception of the correct type.
@@ -39,31 +39,29 @@ Otherwise, either remove the method or ensure that the method raises an exceptio
-This example shows two unhashable classes. The first class is unhashable in a non-standard way which may cause maintenance problems.
-The second, corrected, class uses the standard idiom for unhashable classes.
+In the following example, the __add__ method of A raises a TypeError if other is of the wrong type.
+However, it should return NotImplemented instead of rising an exception, to allow other classes to support adding to A.
+This is demonstrated in the class B.
-
+
-In this example, the first class is implicitly abstract; the __add__ method is unimplemented,
-presumably with the expectation that it will be implemented by sub-classes.
-The second class makes this explicit with an @abstractmethod decoration on the unimplemented __add__ method.
+In the following example, the __getitem__ method of C raises a ValueError, rather than a KeyError or IndexError as expected.
-
+
-In this last example, the first class implements a collection backed by the file store.
-However, should an IOError be raised in the __getitem__ it will propagate to the caller.
-The second class handles any IOError by reraising a KeyError which is the standard exception for
-the __getitem__ method.
+In the following example, the class __hash__ method of D raises TypeError.
+This causes D to be incorrectly identified as hashable by isinstance(obj, collections.abc.Hashable); so the correct
+way to make a class unhashable is to set __hash__ = None.
Arithmetic operations, a + b (__add__): Do not raise an exception, return NotImplemented instead.
Indexing, a[b] (__getitem__): Raise KeyError or IndexError.
Hashing, hash(a) (__hash__): Should not raise an exception. Use __hash__ = None to indicate that an object is unhashable rather than raising an exception.
Equality methods, a == b (__eq__): Never raise an exception, always return True or False.
Ordering comparison methods, a < b (__lt__): Raise a TypeError if the objects cannot be ordered.
-
Most others: Ideally, do not implement the method at all, otherwise raise TypeError to indicate that the operation is unsupported.
+
Most others: If the operation is never supported, the method often does not need to be implemented at all; otherwise a TypeError should be raised.
-
If the method is intended to be abstract, then declare it so using the @abstractmethod decorator.
+
If the method is intended to be abstract, and thus always raise an exception, then declare it so using the @abstractmethod decorator.
Otherwise, either remove the method or ensure that the method raises an exception of the correct type.
diff --git a/python/ql/src/Functions/IncorrectRaiseInSpecialMethod.ql b/python/ql/src/Functions/IncorrectRaiseInSpecialMethod.ql
index 12107821aa6..3232ef51a2d 100644
--- a/python/ql/src/Functions/IncorrectRaiseInSpecialMethod.ql
+++ b/python/ql/src/Functions/IncorrectRaiseInSpecialMethod.ql
@@ -94,7 +94,7 @@ predicate preferredRaise(string name, string execName, string message) {
}
predicate execIsOfType(Expr exec, string execName) {
- // Might make sense to have execName be an IPA type here. Or part of a more general API modelling builtin/stdlib subclass relations.
+ // Might make sense to have execName be an IPA type here. Or part of a more general API modeling builtin/stdlib subclass relations.
exists(string subclass |
execName = "TypeError" and
subclass = "TypeError"
diff --git a/python/ql/src/change-notes/2025-07-25-unexpected-raise-special-method.md b/python/ql/src/change-notes/2025-07-25-unexpected-raise-special-method.md
new file mode 100644
index 00000000000..4b79dbc3b81
--- /dev/null
+++ b/python/ql/src/change-notes/2025-07-25-unexpected-raise-special-method.md
@@ -0,0 +1,5 @@
+---
+category: minorAnalysis
+---
+* The `py/unexpected-raise-in-special-method` query has been modernized. It produces additional results in cases where the exception is
+only raised conditionally. Its precision has been changed from `very-high` to `high`.
\ No newline at end of file
From 8bdf6801b3b92b40135dcfbab56a75c6f75b0ad8 Mon Sep 17 00:00:00 2001
From: Joe Farebrother
Date: Fri, 25 Jul 2025 10:05:09 +0100
Subject: [PATCH 065/984] Add qldoc
---
.../IncorrectRaiseInSpecialMethod.ql | 19 ++++++++++++++++++-
1 file changed, 18 insertions(+), 1 deletion(-)
diff --git a/python/ql/src/Functions/IncorrectRaiseInSpecialMethod.ql b/python/ql/src/Functions/IncorrectRaiseInSpecialMethod.ql
index 3232ef51a2d..fbb02822bf7 100644
--- a/python/ql/src/Functions/IncorrectRaiseInSpecialMethod.ql
+++ b/python/ql/src/Functions/IncorrectRaiseInSpecialMethod.ql
@@ -15,14 +15,17 @@ import python
import semmle.python.ApiGraphs
import semmle.python.dataflow.new.internal.DataFlowDispatch
+/** Holds if `name` is the name of a special method for attribute access such as `a.b`, that should raise an `AttributeError`. */
private predicate attributeMethod(string name) {
name = ["__getattribute__", "__getattr__", "__delattr__"] // __setattr__ excluded as it makes sense to raise different kinds of errors based on the `value` parameter
}
+/** Holds if `name` is the name of a special method for indexing operations such as `a[b]`, that should raise a `LookupError`. */
private predicate indexingMethod(string name) {
name = ["__getitem__", "__delitem__"] // __setitem__ excluded as it makes sense to raise different kinds of errors based on the `value` parameter
}
+/** Holds if `name` is the name of a special method for arithmetic operations. */
private predicate arithmeticMethod(string name) {
name =
[
@@ -35,6 +38,7 @@ private predicate arithmeticMethod(string name) {
]
}
+/** Holds if `name is the name of a special method for ordering operations such as `a < b`. */
private predicate orderingMethod(string name) {
name =
[
@@ -45,6 +49,7 @@ private predicate orderingMethod(string name) {
]
}
+/** Holds if `name` is the name of a special method for casting an object to a numeric type, such as `int(x)` */
private predicate castMethod(string name) {
name =
[
@@ -53,9 +58,10 @@ private predicate castMethod(string name) {
"__index__",
"__trunc__",
"__complex__"
- ]
+ ] // __bool__ excluded as it makes sense to allow it to always raise
}
+/** Holds if we allow a special method named `name` to raise `exec` as an exception. */
predicate correctRaise(string name, Expr exec) {
execIsOfType(exec, "TypeError") and
(
@@ -71,6 +77,7 @@ predicate correctRaise(string name, Expr exec) {
)
}
+/** Holds if it is preferred for `name` to raise exceptions of type `execName`. `message` is the alert message. */
predicate preferredRaise(string name, string execName, string message) {
attributeMethod(name) and
execName = "AttributeError" and
@@ -93,6 +100,7 @@ predicate preferredRaise(string name, string execName, string message) {
message = "should raise a TypeError instead."
}
+/** Holds if `exec` is an exception object of the type named `execName`. */
predicate execIsOfType(Expr exec, string execName) {
// Might make sense to have execName be an IPA type here. Or part of a more general API modeling builtin/stdlib subclass relations.
exists(string subclass |
@@ -114,6 +122,10 @@ predicate execIsOfType(Expr exec, string execName) {
)
}
+/**
+ * Holds if `meth` need not be implemented if it always raises. `message` is the alert message, and `allowNotImplemented` is true
+ * if we still allow the method to always raise `NotImplementedError`.
+ */
predicate noNeedToAlwaysRaise(Function meth, string message, boolean allowNotImplemented) {
meth.getName() = "__hash__" and
message = "use __hash__ = None instead." and
@@ -130,14 +142,17 @@ predicate noNeedToAlwaysRaise(Function meth, string message, boolean allowNotImp
)
}
+/** Holds if `func` has a decorator likely marking it as an abstract method. */
predicate isAbstract(Function func) { func.getADecorator().(Name).getId().matches("%abstract%") }
+/** Holds if `f` always raises the exception `exec`. */
predicate alwaysRaises(Function f, Expr exec) {
directlyRaises(f, exec) and
strictcount(Expr e | directlyRaises(f, e)) = 1 and
not exists(f.getANormalExit())
}
+/** Holds if `f` directly raises `expr` using a `raise` statement. */
predicate directlyRaises(Function f, Expr exec) {
exists(Raise r |
r.getScope() = f and
@@ -146,10 +161,12 @@ predicate directlyRaises(Function f, Expr exec) {
)
}
+/** Holds if `exec` is a `NotImplementedError`. */
predicate isNotImplementedError(Expr exec) {
exec = API::builtin("NotImplementedError").getACall().asExpr()
}
+/** Gets the name of the builtin exception type `exec` constructs, if it can be determined. */
string getExecName(Expr exec) { result = exec.(Call).getFunc().(Name).getId() }
from Function f, Expr exec, string message
From 9af2ab83dc66df7fd48501bf3f6ed75c2b6bba35 Mon Sep 17 00:00:00 2001
From: Joe Farebrother
Date: Fri, 25 Jul 2025 10:22:51 +0100
Subject: [PATCH 066/984] Cleanups
---
.../src/Functions/IncorrectRaiseInSpecialMethod.ql | 12 ++++++------
1 file changed, 6 insertions(+), 6 deletions(-)
diff --git a/python/ql/src/Functions/IncorrectRaiseInSpecialMethod.ql b/python/ql/src/Functions/IncorrectRaiseInSpecialMethod.ql
index fbb02822bf7..07c6fb1c5d3 100644
--- a/python/ql/src/Functions/IncorrectRaiseInSpecialMethod.ql
+++ b/python/ql/src/Functions/IncorrectRaiseInSpecialMethod.ql
@@ -30,11 +30,11 @@ private predicate arithmeticMethod(string name) {
name =
[
"__add__", "__sub__", "__or__", "__xor__", "__rshift__", "__pow__", "__mul__", "__neg__",
- "__radd__", "__rsub__", "__rdiv__", "__rfloordiv__", "__div__", "__rdiv__", "__rlshift__",
- "__rand__", "__ror__", "__rxor__", "__rrshift__", "__rpow__", "__rmul__", "__truediv__",
- "__rtruediv__", "__pos__", "__iadd__", "__isub__", "__idiv__", "__ifloordiv__", "__idiv__",
- "__ilshift__", "__iand__", "__ior__", "__ixor__", "__irshift__", "__abs__", "__ipow__",
- "__imul__", "__itruediv__", "__floordiv__", "__div__", "__divmod__", "__lshift__", "__and__"
+ "__radd__", "__rsub__", "__rdiv__", "__rfloordiv__", "__rlshift__", "__rand__", "__ror__",
+ "__rxor__", "__rrshift__", "__rpow__", "__rmul__", "__truediv__", "__rtruediv__", "__pos__",
+ "__iadd__", "__isub__", "__idiv__", "__ifloordiv__", "__idiv__", "__ilshift__", "__iand__",
+ "__ior__", "__ixor__", "__irshift__", "__abs__", "__ipow__", "__imul__", "__itruediv__",
+ "__floordiv__", "__div__", "__divmod__", "__lshift__", "__and__"
]
}
@@ -152,7 +152,7 @@ predicate alwaysRaises(Function f, Expr exec) {
not exists(f.getANormalExit())
}
-/** Holds if `f` directly raises `expr` using a `raise` statement. */
+/** Holds if `f` directly raises `exec` using a `raise` statement. */
predicate directlyRaises(Function f, Expr exec) {
exists(Raise r |
r.getScope() = f and
From d7b855c4e379fef782a45f79e07ecc3305a6cc54 Mon Sep 17 00:00:00 2001
From: Joe Farebrother
Date: Fri, 25 Jul 2025 10:24:58 +0100
Subject: [PATCH 067/984] qhelp fix
---
python/ql/src/Functions/IncorrectRaiseInSpecialMethod.qhelp | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/python/ql/src/Functions/IncorrectRaiseInSpecialMethod.qhelp b/python/ql/src/Functions/IncorrectRaiseInSpecialMethod.qhelp
index d6ce2167b8c..42d7d421b0a 100644
--- a/python/ql/src/Functions/IncorrectRaiseInSpecialMethod.qhelp
+++ b/python/ql/src/Functions/IncorrectRaiseInSpecialMethod.qhelp
@@ -49,7 +49,7 @@ In the following example, the __getitem__ method of C
-In the following example, the class __hash__ method of D raises TypeError.
+In the following example, the class __hash__ method of D raises NotImplementedError.
This causes D to be incorrectly identified as hashable by isinstance(obj, collections.abc.Hashable); so the correct
way to make a class unhashable is to set __hash__ = None.
From 958fddb638b4ae13f2682b1fe984fc5af67e3138 Mon Sep 17 00:00:00 2001
From: Joe Farebrother
Date: Fri, 25 Jul 2025 10:57:19 +0100
Subject: [PATCH 068/984] cleanup order and remove duplicates for arithmetic
methods
---
.../src/Functions/IncorrectRaiseInSpecialMethod.ql | 13 +++++++------
1 file changed, 7 insertions(+), 6 deletions(-)
diff --git a/python/ql/src/Functions/IncorrectRaiseInSpecialMethod.ql b/python/ql/src/Functions/IncorrectRaiseInSpecialMethod.ql
index 07c6fb1c5d3..3cd7e0fe987 100644
--- a/python/ql/src/Functions/IncorrectRaiseInSpecialMethod.ql
+++ b/python/ql/src/Functions/IncorrectRaiseInSpecialMethod.ql
@@ -29,12 +29,13 @@ private predicate indexingMethod(string name) {
private predicate arithmeticMethod(string name) {
name =
[
- "__add__", "__sub__", "__or__", "__xor__", "__rshift__", "__pow__", "__mul__", "__neg__",
- "__radd__", "__rsub__", "__rdiv__", "__rfloordiv__", "__rlshift__", "__rand__", "__ror__",
- "__rxor__", "__rrshift__", "__rpow__", "__rmul__", "__truediv__", "__rtruediv__", "__pos__",
- "__iadd__", "__isub__", "__idiv__", "__ifloordiv__", "__idiv__", "__ilshift__", "__iand__",
- "__ior__", "__ixor__", "__irshift__", "__abs__", "__ipow__", "__imul__", "__itruediv__",
- "__floordiv__", "__div__", "__divmod__", "__lshift__", "__and__"
+ "__add__", "__sub__", "__and__", "__or__", "__xor__", "__lshift__", "__rshift__", "__pow__",
+ "__mul__", "__div__", "__divmod__", "__truediv__", "__floordiv__", "__matmul__", "__radd__",
+ "__rsub__", "__rand__", "__ror__", "__rxor__", "__rlshift__", "__rrshift__", "__rpow__",
+ "__rmul__", "__rdiv__", "__rdivmod__", "__rtruediv__", "__rfloordiv__", "__rmatmul__",
+ "__iadd__", "__isub__", "__iand__", "__ior__", "__ixor__", "__ilshift__", "__irshift__",
+ "__ipow__", "__imul__", "__idiv__", "__idivmod__", "__itruediv__", "__ifloordiv__",
+ "__imatmul__", "__pos__", "__neg__", "__abs__", "__invert__",
]
}
From c0da9c407e12535984b98370f84a4f176bb17b34 Mon Sep 17 00:00:00 2001
From: Joe Farebrother
Date: Fri, 25 Jul 2025 13:15:46 +0100
Subject: [PATCH 069/984] Fix typo in test dir name + update examples
---
.../src/Functions/examples/IncorrectRaiseInSpecialMethod.py | 4 ++--
.../src/Functions/examples/IncorrectRaiseInSpecialMethod3.py | 2 +-
.../IncorrectRaiseInSpecialMethod.expected | 0
.../IncorrectRaiseInSpecialMethod.qlref | 0
.../test.py | 0
5 files changed, 3 insertions(+), 3 deletions(-)
rename python/ql/test/query-tests/Functions/{IncorrectRaiseInSpcialMethod => IncorrectRaiseInSpecialMethod}/IncorrectRaiseInSpecialMethod.expected (100%)
rename python/ql/test/query-tests/Functions/{IncorrectRaiseInSpcialMethod => IncorrectRaiseInSpecialMethod}/IncorrectRaiseInSpecialMethod.qlref (100%)
rename python/ql/test/query-tests/Functions/{IncorrectRaiseInSpcialMethod => IncorrectRaiseInSpecialMethod}/test.py (100%)
diff --git a/python/ql/src/Functions/examples/IncorrectRaiseInSpecialMethod.py b/python/ql/src/Functions/examples/IncorrectRaiseInSpecialMethod.py
index 77c623bef79..d565a86cab2 100644
--- a/python/ql/src/Functions/examples/IncorrectRaiseInSpecialMethod.py
+++ b/python/ql/src/Functions/examples/IncorrectRaiseInSpecialMethod.py
@@ -5,7 +5,7 @@ class A:
def __add__(self, other):
# BAD: Should return NotImplemented instead of raising
if not isinstance(other,A):
- raise TypeError(f"Cannot add A to {other.__type__}")
+ raise TypeError(f"Cannot add A to {other.__class__}")
return A(self.a + other.a)
class B:
@@ -13,7 +13,7 @@ class B:
self.a = a
def __add__(self, other):
- # GOOD: Returning NotImplemented allows for other classes to support adding do B.
+ # GOOD: Returning NotImplemented allows for the operation to fallback to other implementations to allow other classes to support adding to B.
if not isinstance(other,B):
return NotImplemented
return B(self.a + other.a)
diff --git a/python/ql/src/Functions/examples/IncorrectRaiseInSpecialMethod3.py b/python/ql/src/Functions/examples/IncorrectRaiseInSpecialMethod3.py
index 84ce9d18d27..33541adc7e6 100644
--- a/python/ql/src/Functions/examples/IncorrectRaiseInSpecialMethod3.py
+++ b/python/ql/src/Functions/examples/IncorrectRaiseInSpecialMethod3.py
@@ -1,4 +1,4 @@
class D:
def __hash__(self):
# BAD: Use `__hash__ = None` instead.
- raise NotImplementedError(f"{self.__type__} is unhashable.")
\ No newline at end of file
+ raise NotImplementedError(f"{self.__class__} is unhashable.")
\ No newline at end of file
diff --git a/python/ql/test/query-tests/Functions/IncorrectRaiseInSpcialMethod/IncorrectRaiseInSpecialMethod.expected b/python/ql/test/query-tests/Functions/IncorrectRaiseInSpecialMethod/IncorrectRaiseInSpecialMethod.expected
similarity index 100%
rename from python/ql/test/query-tests/Functions/IncorrectRaiseInSpcialMethod/IncorrectRaiseInSpecialMethod.expected
rename to python/ql/test/query-tests/Functions/IncorrectRaiseInSpecialMethod/IncorrectRaiseInSpecialMethod.expected
diff --git a/python/ql/test/query-tests/Functions/IncorrectRaiseInSpcialMethod/IncorrectRaiseInSpecialMethod.qlref b/python/ql/test/query-tests/Functions/IncorrectRaiseInSpecialMethod/IncorrectRaiseInSpecialMethod.qlref
similarity index 100%
rename from python/ql/test/query-tests/Functions/IncorrectRaiseInSpcialMethod/IncorrectRaiseInSpecialMethod.qlref
rename to python/ql/test/query-tests/Functions/IncorrectRaiseInSpecialMethod/IncorrectRaiseInSpecialMethod.qlref
diff --git a/python/ql/test/query-tests/Functions/IncorrectRaiseInSpcialMethod/test.py b/python/ql/test/query-tests/Functions/IncorrectRaiseInSpecialMethod/test.py
similarity index 100%
rename from python/ql/test/query-tests/Functions/IncorrectRaiseInSpcialMethod/test.py
rename to python/ql/test/query-tests/Functions/IncorrectRaiseInSpecialMethod/test.py
From 358617f53365db5b808bc668f0aae704e56790c2 Mon Sep 17 00:00:00 2001
From: Napalys Klicius
Date: Wed, 30 Jul 2025 09:49:04 +0000
Subject: [PATCH 070/984] Move CORS misconfiguration query from experimental to
Security
---
.../semmle/javascript/frameworks}/Apollo.qll | 0
.../semmle/javascript/frameworks}/Cors.qll | 0
.../CorsPermissiveConfigurationCustomizations.qll | 10 +++++-----
.../security}/CorsPermissiveConfigurationQuery.qll | 0
.../CWE-942/CorsPermissiveConfiguration.qhelp | 0
.../Security/CWE-942/CorsPermissiveConfiguration.ql | 11 ++++++-----
.../examples/CorsPermissiveConfigurationBad.js | 0
.../examples/CorsPermissiveConfigurationGood.js | 0
.../CWE-942/CorsPermissiveConfiguration.qlref | 1 -
.../CWE-942/CorsPermissiveConfiguration.expected | 0
.../CWE-942/CorsPermissiveConfiguration.qlref | 1 +
.../Security/CWE-942/apollo-test.js | 0
.../Security/CWE-942/express-test.js | 0
13 files changed, 12 insertions(+), 11 deletions(-)
rename javascript/ql/{src/experimental/Security/CWE-942 => lib/semmle/javascript/frameworks}/Apollo.qll (100%)
rename javascript/ql/{src/experimental/Security/CWE-942 => lib/semmle/javascript/frameworks}/Cors.qll (100%)
rename javascript/ql/{src/experimental/Security/CWE-942 => lib/semmle/javascript/security}/CorsPermissiveConfigurationCustomizations.qll (94%)
rename javascript/ql/{src/experimental/Security/CWE-942 => lib/semmle/javascript/security}/CorsPermissiveConfigurationQuery.qll (100%)
rename javascript/ql/src/{experimental => }/Security/CWE-942/CorsPermissiveConfiguration.qhelp (100%)
rename javascript/ql/src/{experimental => }/Security/CWE-942/CorsPermissiveConfiguration.ql (53%)
rename javascript/ql/src/{experimental => }/Security/CWE-942/examples/CorsPermissiveConfigurationBad.js (100%)
rename javascript/ql/src/{experimental => }/Security/CWE-942/examples/CorsPermissiveConfigurationGood.js (100%)
delete mode 100644 javascript/ql/test/experimental/Security/CWE-942/CorsPermissiveConfiguration.qlref
rename javascript/ql/test/{experimental => query-tests}/Security/CWE-942/CorsPermissiveConfiguration.expected (100%)
create mode 100644 javascript/ql/test/query-tests/Security/CWE-942/CorsPermissiveConfiguration.qlref
rename javascript/ql/test/{experimental => query-tests}/Security/CWE-942/apollo-test.js (100%)
rename javascript/ql/test/{experimental => query-tests}/Security/CWE-942/express-test.js (100%)
diff --git a/javascript/ql/src/experimental/Security/CWE-942/Apollo.qll b/javascript/ql/lib/semmle/javascript/frameworks/Apollo.qll
similarity index 100%
rename from javascript/ql/src/experimental/Security/CWE-942/Apollo.qll
rename to javascript/ql/lib/semmle/javascript/frameworks/Apollo.qll
diff --git a/javascript/ql/src/experimental/Security/CWE-942/Cors.qll b/javascript/ql/lib/semmle/javascript/frameworks/Cors.qll
similarity index 100%
rename from javascript/ql/src/experimental/Security/CWE-942/Cors.qll
rename to javascript/ql/lib/semmle/javascript/frameworks/Cors.qll
diff --git a/javascript/ql/src/experimental/Security/CWE-942/CorsPermissiveConfigurationCustomizations.qll b/javascript/ql/lib/semmle/javascript/security/CorsPermissiveConfigurationCustomizations.qll
similarity index 94%
rename from javascript/ql/src/experimental/Security/CWE-942/CorsPermissiveConfigurationCustomizations.qll
rename to javascript/ql/lib/semmle/javascript/security/CorsPermissiveConfigurationCustomizations.qll
index 8876373a3d2..b642b98b35b 100644
--- a/javascript/ql/src/experimental/Security/CWE-942/CorsPermissiveConfigurationCustomizations.qll
+++ b/javascript/ql/lib/semmle/javascript/security/CorsPermissiveConfigurationCustomizations.qll
@@ -5,8 +5,8 @@
*/
import javascript
-import Cors::Cors
-import Apollo::Apollo
+private import semmle.javascript.frameworks.Apollo
+private import semmle.javascript.frameworks.Cors
/** Module containing sources, sinks, and sanitizers for overly permissive CORS configurations. */
module CorsPermissiveConfiguration {
@@ -105,7 +105,7 @@ module CorsPermissiveConfiguration {
*/
class CorsApolloServer extends Sink, DataFlow::ValueNode {
CorsApolloServer() {
- exists(ApolloServer agql |
+ exists(Apollo::ApolloServer agql |
this =
agql.getOptionArgument(0, "cors").getALocalSource().getAPropertyWrite("origin").getRhs()
)
@@ -125,7 +125,7 @@ module CorsPermissiveConfiguration {
* An express route setup configured with the `cors` package.
*/
class CorsConfiguration extends DataFlow::MethodCallNode {
- Cors corsConfig;
+ Cors::Cors corsConfig;
CorsConfiguration() {
exists(Express::RouteSetup setup | this = setup |
@@ -136,6 +136,6 @@ module CorsPermissiveConfiguration {
}
/** Gets the expression that configures `cors` on this route setup. */
- Cors getCorsConfiguration() { result = corsConfig }
+ Cors::Cors getCorsConfiguration() { result = corsConfig }
}
}
diff --git a/javascript/ql/src/experimental/Security/CWE-942/CorsPermissiveConfigurationQuery.qll b/javascript/ql/lib/semmle/javascript/security/CorsPermissiveConfigurationQuery.qll
similarity index 100%
rename from javascript/ql/src/experimental/Security/CWE-942/CorsPermissiveConfigurationQuery.qll
rename to javascript/ql/lib/semmle/javascript/security/CorsPermissiveConfigurationQuery.qll
diff --git a/javascript/ql/src/experimental/Security/CWE-942/CorsPermissiveConfiguration.qhelp b/javascript/ql/src/Security/CWE-942/CorsPermissiveConfiguration.qhelp
similarity index 100%
rename from javascript/ql/src/experimental/Security/CWE-942/CorsPermissiveConfiguration.qhelp
rename to javascript/ql/src/Security/CWE-942/CorsPermissiveConfiguration.qhelp
diff --git a/javascript/ql/src/experimental/Security/CWE-942/CorsPermissiveConfiguration.ql b/javascript/ql/src/Security/CWE-942/CorsPermissiveConfiguration.ql
similarity index 53%
rename from javascript/ql/src/experimental/Security/CWE-942/CorsPermissiveConfiguration.ql
rename to javascript/ql/src/Security/CWE-942/CorsPermissiveConfiguration.ql
index 87db66ad98d..05084202858 100644
--- a/javascript/ql/src/experimental/Security/CWE-942/CorsPermissiveConfiguration.ql
+++ b/javascript/ql/src/Security/CWE-942/CorsPermissiveConfiguration.ql
@@ -1,5 +1,5 @@
/**
- * @name overly CORS configuration
+ * @name Permissive CORS configuration
* @description Misconfiguration of CORS HTTP headers allows CSRF attacks.
* @kind path-problem
* @problem.severity error
@@ -11,11 +11,12 @@
*/
import javascript
-import CorsPermissiveConfigurationQuery
-import CorsPermissiveConfigurationFlow::PathGraph
+import semmle.javascript.security.CorsPermissiveConfigurationQuery as CorsQuery
+import CorsQuery::CorsPermissiveConfigurationFlow::PathGraph
from
- CorsPermissiveConfigurationFlow::PathNode source, CorsPermissiveConfigurationFlow::PathNode sink
-where CorsPermissiveConfigurationFlow::flowPath(source, sink)
+ CorsQuery::CorsPermissiveConfigurationFlow::PathNode source,
+ CorsQuery::CorsPermissiveConfigurationFlow::PathNode sink
+where CorsQuery::CorsPermissiveConfigurationFlow::flowPath(source, sink)
select sink.getNode(), source, sink, "CORS Origin misconfiguration due to a $@.", source.getNode(),
"too permissive or user controlled value"
diff --git a/javascript/ql/src/experimental/Security/CWE-942/examples/CorsPermissiveConfigurationBad.js b/javascript/ql/src/Security/CWE-942/examples/CorsPermissiveConfigurationBad.js
similarity index 100%
rename from javascript/ql/src/experimental/Security/CWE-942/examples/CorsPermissiveConfigurationBad.js
rename to javascript/ql/src/Security/CWE-942/examples/CorsPermissiveConfigurationBad.js
diff --git a/javascript/ql/src/experimental/Security/CWE-942/examples/CorsPermissiveConfigurationGood.js b/javascript/ql/src/Security/CWE-942/examples/CorsPermissiveConfigurationGood.js
similarity index 100%
rename from javascript/ql/src/experimental/Security/CWE-942/examples/CorsPermissiveConfigurationGood.js
rename to javascript/ql/src/Security/CWE-942/examples/CorsPermissiveConfigurationGood.js
diff --git a/javascript/ql/test/experimental/Security/CWE-942/CorsPermissiveConfiguration.qlref b/javascript/ql/test/experimental/Security/CWE-942/CorsPermissiveConfiguration.qlref
deleted file mode 100644
index 1e6a39679c0..00000000000
--- a/javascript/ql/test/experimental/Security/CWE-942/CorsPermissiveConfiguration.qlref
+++ /dev/null
@@ -1 +0,0 @@
-./experimental/Security/CWE-942/CorsPermissiveConfiguration.ql
\ No newline at end of file
diff --git a/javascript/ql/test/experimental/Security/CWE-942/CorsPermissiveConfiguration.expected b/javascript/ql/test/query-tests/Security/CWE-942/CorsPermissiveConfiguration.expected
similarity index 100%
rename from javascript/ql/test/experimental/Security/CWE-942/CorsPermissiveConfiguration.expected
rename to javascript/ql/test/query-tests/Security/CWE-942/CorsPermissiveConfiguration.expected
diff --git a/javascript/ql/test/query-tests/Security/CWE-942/CorsPermissiveConfiguration.qlref b/javascript/ql/test/query-tests/Security/CWE-942/CorsPermissiveConfiguration.qlref
new file mode 100644
index 00000000000..4f4178905a2
--- /dev/null
+++ b/javascript/ql/test/query-tests/Security/CWE-942/CorsPermissiveConfiguration.qlref
@@ -0,0 +1 @@
+Security/CWE-942/CorsPermissiveConfiguration.ql
\ No newline at end of file
diff --git a/javascript/ql/test/experimental/Security/CWE-942/apollo-test.js b/javascript/ql/test/query-tests/Security/CWE-942/apollo-test.js
similarity index 100%
rename from javascript/ql/test/experimental/Security/CWE-942/apollo-test.js
rename to javascript/ql/test/query-tests/Security/CWE-942/apollo-test.js
diff --git a/javascript/ql/test/experimental/Security/CWE-942/express-test.js b/javascript/ql/test/query-tests/Security/CWE-942/express-test.js
similarity index 100%
rename from javascript/ql/test/experimental/Security/CWE-942/express-test.js
rename to javascript/ql/test/query-tests/Security/CWE-942/express-test.js
From 92daa7d42cd2835a00c5e17deea4cc1a44401112 Mon Sep 17 00:00:00 2001
From: Napalys Klicius
Date: Wed, 30 Jul 2025 10:27:14 +0000
Subject: [PATCH 071/984] Updated suite expectations
---
.../query-suite/javascript-code-scanning.qls.expected | 1 +
.../query-suite/javascript-security-and-quality.qls.expected | 1 +
.../query-suite/javascript-security-extended.qls.expected | 1 +
.../integration-tests/query-suite/not_included_in_qls.expected | 1 -
4 files changed, 3 insertions(+), 1 deletion(-)
diff --git a/javascript/ql/integration-tests/query-suite/javascript-code-scanning.qls.expected b/javascript/ql/integration-tests/query-suite/javascript-code-scanning.qls.expected
index 652ac0ebc1b..0c417e661c7 100644
--- a/javascript/ql/integration-tests/query-suite/javascript-code-scanning.qls.expected
+++ b/javascript/ql/integration-tests/query-suite/javascript-code-scanning.qls.expected
@@ -83,5 +83,6 @@ ql/javascript/ql/src/Security/CWE-915/PrototypePollutingFunction.ql
ql/javascript/ql/src/Security/CWE-915/PrototypePollutingMergeCall.ql
ql/javascript/ql/src/Security/CWE-916/InsufficientPasswordHash.ql
ql/javascript/ql/src/Security/CWE-918/RequestForgery.ql
+ql/javascript/ql/src/Security/CWE-942/CorsPermissiveConfiguration.ql
ql/javascript/ql/src/Summary/LinesOfCode.ql
ql/javascript/ql/src/Summary/LinesOfUserCode.ql
diff --git a/javascript/ql/integration-tests/query-suite/javascript-security-and-quality.qls.expected b/javascript/ql/integration-tests/query-suite/javascript-security-and-quality.qls.expected
index dd587768308..f87cd2bf505 100644
--- a/javascript/ql/integration-tests/query-suite/javascript-security-and-quality.qls.expected
+++ b/javascript/ql/integration-tests/query-suite/javascript-security-and-quality.qls.expected
@@ -184,6 +184,7 @@ ql/javascript/ql/src/Security/CWE-915/PrototypePollutingMergeCall.ql
ql/javascript/ql/src/Security/CWE-916/InsufficientPasswordHash.ql
ql/javascript/ql/src/Security/CWE-918/ClientSideRequestForgery.ql
ql/javascript/ql/src/Security/CWE-918/RequestForgery.ql
+ql/javascript/ql/src/Security/CWE-942/CorsPermissiveConfiguration.ql
ql/javascript/ql/src/Statements/DanglingElse.ql
ql/javascript/ql/src/Statements/IgnoreArrayResult.ql
ql/javascript/ql/src/Statements/InconsistentLoopOrientation.ql
diff --git a/javascript/ql/integration-tests/query-suite/javascript-security-extended.qls.expected b/javascript/ql/integration-tests/query-suite/javascript-security-extended.qls.expected
index 9b7cfd22ed6..ac5e0e2c498 100644
--- a/javascript/ql/integration-tests/query-suite/javascript-security-extended.qls.expected
+++ b/javascript/ql/integration-tests/query-suite/javascript-security-extended.qls.expected
@@ -99,5 +99,6 @@ ql/javascript/ql/src/Security/CWE-915/PrototypePollutingMergeCall.ql
ql/javascript/ql/src/Security/CWE-916/InsufficientPasswordHash.ql
ql/javascript/ql/src/Security/CWE-918/ClientSideRequestForgery.ql
ql/javascript/ql/src/Security/CWE-918/RequestForgery.ql
+ql/javascript/ql/src/Security/CWE-942/CorsPermissiveConfiguration.ql
ql/javascript/ql/src/Summary/LinesOfCode.ql
ql/javascript/ql/src/Summary/LinesOfUserCode.ql
diff --git a/javascript/ql/integration-tests/query-suite/not_included_in_qls.expected b/javascript/ql/integration-tests/query-suite/not_included_in_qls.expected
index 1b119f60c75..fa52a97a4e4 100644
--- a/javascript/ql/integration-tests/query-suite/not_included_in_qls.expected
+++ b/javascript/ql/integration-tests/query-suite/not_included_in_qls.expected
@@ -75,7 +75,6 @@ ql/javascript/ql/src/experimental/Security/CWE-347/decodeJwtWithoutVerificationL
ql/javascript/ql/src/experimental/Security/CWE-444/InsecureHttpParser.ql
ql/javascript/ql/src/experimental/Security/CWE-522-DecompressionBombs/DecompressionBombs.ql
ql/javascript/ql/src/experimental/Security/CWE-918/SSRF.ql
-ql/javascript/ql/src/experimental/Security/CWE-942/CorsPermissiveConfiguration.ql
ql/javascript/ql/src/experimental/StandardLibrary/MultipleArgumentsToSetConstructor.ql
ql/javascript/ql/src/experimental/heuristics/ql/src/Security/CWE-020/UntrustedDataToExternalAPI.ql
ql/javascript/ql/src/experimental/heuristics/ql/src/Security/CWE-078/CommandInjection.ql
From 95743d7109180c7076fe732c2e51ff1ee88d14ab Mon Sep 17 00:00:00 2001
From: Napalys Klicius
Date: Wed, 30 Jul 2025 10:42:55 +0000
Subject: [PATCH 072/984] Added inline test expectations for cors permissive
config
---
.../Security/CWE-942/CorsPermissiveConfiguration.qlref | 3 ++-
.../ql/test/query-tests/Security/CWE-942/apollo-test.js | 8 ++++----
.../ql/test/query-tests/Security/CWE-942/express-test.js | 6 +++---
3 files changed, 9 insertions(+), 8 deletions(-)
diff --git a/javascript/ql/test/query-tests/Security/CWE-942/CorsPermissiveConfiguration.qlref b/javascript/ql/test/query-tests/Security/CWE-942/CorsPermissiveConfiguration.qlref
index 4f4178905a2..b38b30eb842 100644
--- a/javascript/ql/test/query-tests/Security/CWE-942/CorsPermissiveConfiguration.qlref
+++ b/javascript/ql/test/query-tests/Security/CWE-942/CorsPermissiveConfiguration.qlref
@@ -1 +1,2 @@
-Security/CWE-942/CorsPermissiveConfiguration.ql
\ No newline at end of file
+query: Security/CWE-942/CorsPermissiveConfiguration.ql
+postprocess: utils/test/InlineExpectationsTestQuery.ql
\ No newline at end of file
diff --git a/javascript/ql/test/query-tests/Security/CWE-942/apollo-test.js b/javascript/ql/test/query-tests/Security/CWE-942/apollo-test.js
index f55d5dc2c3e..22019a72258 100644
--- a/javascript/ql/test/query-tests/Security/CWE-942/apollo-test.js
+++ b/javascript/ql/test/query-tests/Security/CWE-942/apollo-test.js
@@ -5,10 +5,10 @@ var https = require('https'),
var server = https.createServer(function () { });
server.on('request', function (req, res) {
- let user_origin = url.parse(req.url, true).query.origin;
+ let user_origin = url.parse(req.url, true).query.origin; // $ Source
// BAD: CORS too permissive
const server_1 = new ApolloServer({
- cors: { origin: true }
+ cors: { origin: true } // $ Alert
});
// GOOD: restrictive CORS
@@ -18,11 +18,11 @@ server.on('request', function (req, res) {
// BAD: CORS too permissive
const server_3 = new ApolloServer({
- cors: { origin: null }
+ cors: { origin: null } // $ Alert
});
// BAD: CORS is controlled by user
const server_4 = new ApolloServer({
- cors: { origin: user_origin }
+ cors: { origin: user_origin } // $ Alert
});
});
\ No newline at end of file
diff --git a/javascript/ql/test/query-tests/Security/CWE-942/express-test.js b/javascript/ql/test/query-tests/Security/CWE-942/express-test.js
index 3ad31a6a31a..9b21ed56873 100644
--- a/javascript/ql/test/query-tests/Security/CWE-942/express-test.js
+++ b/javascript/ql/test/query-tests/Security/CWE-942/express-test.js
@@ -7,7 +7,7 @@ var https = require('https'),
var server = https.createServer(function () { });
server.on('request', function (req, res) {
- let user_origin = url.parse(req.url, true).query.origin;
+ let user_origin = url.parse(req.url, true).query.origin; // $ Source
// BAD: CORS too permissive, default value is *
var app1 = express();
@@ -23,14 +23,14 @@ server.on('request', function (req, res) {
// BAD: CORS too permissive
var app3 = express();
var corsOption3 = {
- origin: '*'
+ origin: '*' // $ Alert
};
app3.use(cors(corsOption3));
// BAD: CORS is controlled by user
var app4 = express();
var corsOption4 = {
- origin: user_origin
+ origin: user_origin // $ Alert
};
app4.use(cors(corsOption4));
});
\ No newline at end of file
From 84ffbbec33cbff31393da847b815f2c56ae076c2 Mon Sep 17 00:00:00 2001
From: Napalys Klicius
Date: Wed, 30 Jul 2025 10:51:38 +0000
Subject: [PATCH 073/984] Added missing doc strings
---
.../security/CorsPermissiveConfigurationCustomizations.qll | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/javascript/ql/lib/semmle/javascript/security/CorsPermissiveConfigurationCustomizations.qll b/javascript/ql/lib/semmle/javascript/security/CorsPermissiveConfigurationCustomizations.qll
index b642b98b35b..4751ace2a60 100644
--- a/javascript/ql/lib/semmle/javascript/security/CorsPermissiveConfigurationCustomizations.qll
+++ b/javascript/ql/lib/semmle/javascript/security/CorsPermissiveConfigurationCustomizations.qll
@@ -26,6 +26,7 @@ module CorsPermissiveConfiguration {
this = TWildcard() and result = "wildcard"
}
+ /** DEPRECATED: Converts this flow state to a flow label. */
deprecated DataFlow::FlowLabel toFlowLabel() {
this = TTaint() and result.isTaint()
or
@@ -37,6 +38,7 @@ module CorsPermissiveConfiguration {
/** Predicates for working with flow states. */
module FlowState {
+ /** DEPRECATED: Gets a flow state from a flow label. */
deprecated FlowState fromFlowLabel(DataFlow::FlowLabel label) { result.toFlowLabel() = label }
/** A tainted value. */
@@ -81,6 +83,7 @@ module CorsPermissiveConfiguration {
TrueAndNull() { this = "TrueAndNull" }
}
+ /** DEPRECATED: Gets a flow label representing `true` and `null` values. */
deprecated TrueAndNull truenullLabel() { any() }
/** A flow label representing `*` value. */
@@ -88,6 +91,7 @@ module CorsPermissiveConfiguration {
Wildcard() { this = "Wildcard" }
}
+ /** DEPRECATED: Gets a flow label representing `*` value. */
deprecated Wildcard wildcardLabel() { any() }
/** An overly permissive value for `origin` (Apollo) */
From af94ebe1fc65229b2589b2eba93d8263340c1d47 Mon Sep 17 00:00:00 2001
From: Joe Farebrother
Date: Tue, 29 Jul 2025 14:16:16 +0100
Subject: [PATCH 074/984] Modernize attribute shadows subclass, Add cases for
properties
---
python/ql/src/Classes/SubclassShadowing.ql | 70 ++++++++++++-------
.../SubclassShadowing.qlref | 3 +-
.../subclass-shadowing/subclass_shadowing.py | 45 ++++++++----
3 files changed, 81 insertions(+), 37 deletions(-)
diff --git a/python/ql/src/Classes/SubclassShadowing.ql b/python/ql/src/Classes/SubclassShadowing.ql
index 542cf31c76a..6e915250a54 100644
--- a/python/ql/src/Classes/SubclassShadowing.ql
+++ b/python/ql/src/Classes/SubclassShadowing.ql
@@ -17,31 +17,53 @@
* defined in a super-class
*/
-/* Need to find attributes defined in superclass (only in __init__?) */
import python
+import semmle.python.ApiGraphs
+import semmle.python.dataflow.new.internal.DataFlowDispatch
-predicate shadowed_by_super_class(
- ClassObject c, ClassObject supercls, Assign assign, FunctionObject f
-) {
- c.getASuperType() = supercls and
- c.declaredAttribute(_) = f and
- exists(FunctionObject init, Attribute attr |
- supercls.declaredAttribute("__init__") = init and
- attr = assign.getATarget() and
- attr.getObject().(Name).getId() = "self" and
- attr.getName() = f.getName() and
- assign.getScope() = init.getOrigin().(FunctionExpr).getInnerScope()
- ) and
- /*
- * It's OK if the super class defines the method as well.
- * We assume that the original method must have been defined for a reason.
- */
-
- not supercls.hasAttribute(f.getName())
+predicate isSettableProperty(Function prop) {
+ isProperty(prop) and
+ exists(Function setter, DataFlow::AttrRead setterRead, FunctionExpr propExpr |
+ setterRead.asExpr() = setter.getADecorator() and
+ setterRead.getAttributeName() = "setter" and
+ propExpr.getInnerScope() = prop and
+ DataFlow::exprNode(propExpr).(DataFlow::LocalSourceNode).flowsTo(setterRead.getObject())
+ )
}
-from ClassObject c, ClassObject supercls, Assign assign, FunctionObject shadowed
-where shadowed_by_super_class(c, supercls, assign, shadowed)
-select shadowed.getOrigin(),
- "Method " + shadowed.getName() + " is shadowed by an $@ in super class '" + supercls.getName() +
- "'.", assign, "attribute"
+predicate isProperty(Function prop) {
+ prop.getADecorator() = API::builtin("property").asSource().asExpr()
+}
+
+predicate shadowedBySuperclass(
+ Class cls, Class superclass, DataFlow::AttrWrite write, Function shadowed
+) {
+ getADirectSuperclass+(cls) = superclass and
+ shadowed = cls.getAMethod() and
+ exists(Function init |
+ init = superclass.getInitMethod() and
+ DataFlow::parameterNode(init.getArg(0)).(DataFlow::LocalSourceNode).flowsTo(write.getObject()) and
+ write.getAttributeName() = shadowed.getName()
+ ) and
+ // Allow cases in which the super class defines the method as well.
+ // We assume that the original method must have been defined for a reason.
+ not exists(Function superShadowed |
+ superShadowed = superclass.getAMethod() and
+ superShadowed.getName() = shadowed.getName()
+ ) and
+ // Allow properties if they have setters, as the write in the superclass will call the setter.
+ not isSettableProperty(shadowed)
+}
+
+from Class cls, Class superclass, DataFlow::AttrWrite write, Function shadowed, string extra
+where
+ shadowedBySuperclass(cls, superclass, write, shadowed) and
+ (
+ if isProperty(shadowed)
+ then
+ not isSettableProperty(shadowed) and
+ extra = " (read-only property may cause an error if written to.)"
+ else extra = ""
+ )
+select shadowed, "This method is shadowed by $@ in superclass $@." + extra, write,
+ "attribute " + write.getAttributeName(), superclass, superclass.getName()
diff --git a/python/ql/test/query-tests/Classes/subclass-shadowing/SubclassShadowing.qlref b/python/ql/test/query-tests/Classes/subclass-shadowing/SubclassShadowing.qlref
index 5fed3f9f8fc..ab31ad285c5 100644
--- a/python/ql/test/query-tests/Classes/subclass-shadowing/SubclassShadowing.qlref
+++ b/python/ql/test/query-tests/Classes/subclass-shadowing/SubclassShadowing.qlref
@@ -1 +1,2 @@
-Classes/SubclassShadowing.ql
+query: Classes/SubclassShadowing.ql
+postprocess: utils/test/InlineExpectationsTestQuery.ql
\ No newline at end of file
diff --git a/python/ql/test/query-tests/Classes/subclass-shadowing/subclass_shadowing.py b/python/ql/test/query-tests/Classes/subclass-shadowing/subclass_shadowing.py
index 98e7f992e84..b9fcd975eb3 100644
--- a/python/ql/test/query-tests/Classes/subclass-shadowing/subclass_shadowing.py
+++ b/python/ql/test/query-tests/Classes/subclass-shadowing/subclass_shadowing.py
@@ -1,30 +1,51 @@
#Subclass shadowing
-class Base(object):
+# BAD: `shadow` method shadows attribute
+class Base:
def __init__(self):
self.shadow = 4
class Derived(Base):
- def shadow(self):
+ def shadow(self): # $ Alert
pass
-#OK if the super class defines the method as well.
-#Since the original method must exist for some reason.
-#See JSONEncoder.default for real example
+# OK: Allow if superclass also shadows its own method, as this is likely intended.
+# Example: stdlib JSONEncoder.default uses this pattern.
+class Base2:
-class Base2(object):
+ def __init__(self, default=None):
+ if default:
+ self.default = default
- def __init__(self, shadowy=None):
- if shadowy:
- self.shadow = shadowy
-
- def shadow(self):
+ def default(self):
pass
class Derived2(Base2):
- def shadow(self):
+ def default(self): # No alert
return 0
+
+# Properties
+
+class Base3:
+ def __init__(self):
+ self.foo = 1
+ self.bar = 2
+
+class Derived3(Base3):
+ # BAD: Write to foo in superclass init raises an error.
+ @property
+ def foo(self): # $ Alert
+ return 2
+
+ # OK: This property has a setter, so the write is OK.
+ @property
+ def bar(self): # No alert
+ return self._bar
+
+ @bar.setter
+ def bar(self, val):
+ self._bar = val
\ No newline at end of file
From 796a6060b204a6cc243618606e38bb4bd4583721 Mon Sep 17 00:00:00 2001
From: Joe Farebrother
Date: Wed, 30 Jul 2025 10:11:59 +0100
Subject: [PATCH 075/984] Exclude setters and update tests
---
python/ql/src/Classes/SubclassShadowing.ql | 28 ++++++++++---------
.../SubclassShadowing.expected | 3 +-
2 files changed, 17 insertions(+), 14 deletions(-)
diff --git a/python/ql/src/Classes/SubclassShadowing.ql b/python/ql/src/Classes/SubclassShadowing.ql
index 6e915250a54..eab8520857b 100644
--- a/python/ql/src/Classes/SubclassShadowing.ql
+++ b/python/ql/src/Classes/SubclassShadowing.ql
@@ -12,22 +12,23 @@
* @id py/attribute-shadows-method
*/
-/*
- * Determine if a class defines a method that is shadowed by an attribute
- * defined in a super-class
- */
-
import python
import semmle.python.ApiGraphs
import semmle.python.dataflow.new.internal.DataFlowDispatch
predicate isSettableProperty(Function prop) {
isProperty(prop) and
- exists(Function setter, DataFlow::AttrRead setterRead, FunctionExpr propExpr |
- setterRead.asExpr() = setter.getADecorator() and
- setterRead.getAttributeName() = "setter" and
- propExpr.getInnerScope() = prop and
- DataFlow::exprNode(propExpr).(DataFlow::LocalSourceNode).flowsTo(setterRead.getObject())
+ exists(Function setter |
+ setter.getScope() = prop.getScope() and
+ setter.getName() = prop.getName() and
+ isSetter(setter)
+ )
+}
+
+predicate isSetter(Function f) {
+ exists(DataFlow::AttrRead attr |
+ f.getADecorator() = attr.asExpr() and
+ attr.getAttributeName() = "setter"
)
}
@@ -52,7 +53,8 @@ predicate shadowedBySuperclass(
superShadowed.getName() = shadowed.getName()
) and
// Allow properties if they have setters, as the write in the superclass will call the setter.
- not isSettableProperty(shadowed)
+ not isSettableProperty(shadowed) and
+ not isSetter(shadowed)
}
from Class cls, Class superclass, DataFlow::AttrWrite write, Function shadowed, string extra
@@ -61,8 +63,8 @@ where
(
if isProperty(shadowed)
then
- not isSettableProperty(shadowed) and
- extra = " (read-only property may cause an error if written to.)"
+ // it's not a setter, so it's a read-only property
+ extra = " (read-only property may cause an error if written to in the superclass.)"
else extra = ""
)
select shadowed, "This method is shadowed by $@ in superclass $@." + extra, write,
diff --git a/python/ql/test/query-tests/Classes/subclass-shadowing/SubclassShadowing.expected b/python/ql/test/query-tests/Classes/subclass-shadowing/SubclassShadowing.expected
index caad71a9a31..3852b977a22 100644
--- a/python/ql/test/query-tests/Classes/subclass-shadowing/SubclassShadowing.expected
+++ b/python/ql/test/query-tests/Classes/subclass-shadowing/SubclassShadowing.expected
@@ -1 +1,2 @@
-| subclass_shadowing.py:10:5:10:21 | FunctionExpr | Method shadow is shadowed by an $@ in super class 'Base'. | subclass_shadowing.py:6:9:6:23 | AssignStmt | attribute |
+| subclass_shadowing.py:11:5:11:21 | Function shadow | This method is shadowed by $@ in superclass $@. | subclass_shadowing.py:7:9:7:19 | ControlFlowNode for Attribute | attribute shadow | subclass_shadowing.py:4:1:4:11 | Class Base | Base |
+| subclass_shadowing.py:41:5:41:18 | Function foo | This method is shadowed by $@ in superclass $@. (read-only property may cause an error if written to.) | subclass_shadowing.py:35:9:35:16 | ControlFlowNode for Attribute | attribute foo | subclass_shadowing.py:33:1:33:12 | Class Base3 | Base3 |
From 34317d2d4ad66e2f5cd33a10a92389116ef5a2f1 Mon Sep 17 00:00:00 2001
From: Joe Farebrother
Date: Wed, 30 Jul 2025 13:24:43 +0100
Subject: [PATCH 076/984] Update documentation
---
python/ql/src/Classes/SubclassShadowing.py | 20 ++++---------
python/ql/src/Classes/SubclassShadowing.qhelp | 30 ++++++++++++-------
2 files changed, 26 insertions(+), 24 deletions(-)
diff --git a/python/ql/src/Classes/SubclassShadowing.py b/python/ql/src/Classes/SubclassShadowing.py
index 617db3c58e0..4699b58d7e4 100644
--- a/python/ql/src/Classes/SubclassShadowing.py
+++ b/python/ql/src/Classes/SubclassShadowing.py
@@ -1,17 +1,9 @@
-class Mammal(object):
-
- def __init__(self, milk = 0):
- self.milk = milk
-
-
-class Cow(Mammal):
-
+class A:
def __init__(self):
- Mammal.__init__(self)
+ self._foo = 3
- def milk(self):
- return "Milk"
-
-#Cow().milk() will raise an error as Cow().milk is the 'milk' attribute
-#set in Mammal.__init__, not the 'milk' method defined on Cow.
+class B:
+ # BAD: _foo is shadowed by attribute A._foo
+ def _foo(self):
+ return 2
diff --git a/python/ql/src/Classes/SubclassShadowing.qhelp b/python/ql/src/Classes/SubclassShadowing.qhelp
index 90daa9a992a..c0a82012af3 100644
--- a/python/ql/src/Classes/SubclassShadowing.qhelp
+++ b/python/ql/src/Classes/SubclassShadowing.qhelp
@@ -3,25 +3,35 @@
"qhelp.dtd">
-
Subclass shadowing occurs when an instance attribute of a superclass has the
-the same name as a method of a subclass, or vice-versa.
-The semantics of Python attribute look-up mean that the instance attribute of
-the superclass hides the method in the subclass.
+
+When an object has an attribute that shares the same name a method on the object's class (or another class attribute), the instance attribute is
+prioritized during attribute lookup, shadowing the method.
+
+If a method on a subclass is shadowed by an attribute on a superclass in this way, this may lead to unexpected results or errors, as this
+shadowing behavior is nonlocal and may be unintended.
-
Rename the method in the subclass or rename the attribute in the superclass.
+
+Ensure method names on subclasses don't conflict with attribute names on superclasses, and rename one.
+If the shadowing behavior is intended, ensure this is explicit in the superclass.
+
-
The following code includes an example of subclass shadowing. When you call Cow().milk()
-an error is raised because Cow().milk is interpreted as the 'milk' attribute set in
-Mammal.__init__, not the 'milk' method defined within Cow. This can be fixed
-by changing the name of either the 'milk' attribute or the 'milk' method.
+
+In the following example, the _foo attribute of class A shadows the method _foo of class B.
+Calls to B()._foo() will result in a TypeError, as 3 will be called instead.
+
+
+
+
+
+In the following example...
+
-
From 2516f9452e8b725f923c8834bc3ad65c3bd2886f Mon Sep 17 00:00:00 2001
From: Joe Farebrother
Date: Wed, 30 Jul 2025 15:17:19 +0100
Subject: [PATCH 077/984] Move to subfolder
---
.../src/Classes/{ => SubclassShadowing}/SubclassShadowing.qhelp | 2 +-
.../ql/src/Classes/{ => SubclassShadowing}/SubclassShadowing.ql | 0
.../examples/SubclassShadowingGood.py} | 0
.../Classes/subclass-shadowing/SubclassShadowing.qlref | 2 +-
4 files changed, 2 insertions(+), 2 deletions(-)
rename python/ql/src/Classes/{ => SubclassShadowing}/SubclassShadowing.qhelp (95%)
rename python/ql/src/Classes/{ => SubclassShadowing}/SubclassShadowing.ql (100%)
rename python/ql/src/Classes/{SubclassShadowing.py => SubclassShadowing/examples/SubclassShadowingGood.py} (100%)
diff --git a/python/ql/src/Classes/SubclassShadowing.qhelp b/python/ql/src/Classes/SubclassShadowing/SubclassShadowing.qhelp
similarity index 95%
rename from python/ql/src/Classes/SubclassShadowing.qhelp
rename to python/ql/src/Classes/SubclassShadowing/SubclassShadowing.qhelp
index c0a82012af3..acbcae65318 100644
--- a/python/ql/src/Classes/SubclassShadowing.qhelp
+++ b/python/ql/src/Classes/SubclassShadowing/SubclassShadowing.qhelp
@@ -26,7 +26,7 @@ In the following example, the _foo attribute of class AB()._foo() will result in a TypeError, as 3 will be called instead.
-
+
In the following example...
diff --git a/python/ql/src/Classes/SubclassShadowing.ql b/python/ql/src/Classes/SubclassShadowing/SubclassShadowing.ql
similarity index 100%
rename from python/ql/src/Classes/SubclassShadowing.ql
rename to python/ql/src/Classes/SubclassShadowing/SubclassShadowing.ql
diff --git a/python/ql/src/Classes/SubclassShadowing.py b/python/ql/src/Classes/SubclassShadowing/examples/SubclassShadowingGood.py
similarity index 100%
rename from python/ql/src/Classes/SubclassShadowing.py
rename to python/ql/src/Classes/SubclassShadowing/examples/SubclassShadowingGood.py
diff --git a/python/ql/test/query-tests/Classes/subclass-shadowing/SubclassShadowing.qlref b/python/ql/test/query-tests/Classes/subclass-shadowing/SubclassShadowing.qlref
index ab31ad285c5..5205014a3d5 100644
--- a/python/ql/test/query-tests/Classes/subclass-shadowing/SubclassShadowing.qlref
+++ b/python/ql/test/query-tests/Classes/subclass-shadowing/SubclassShadowing.qlref
@@ -1,2 +1,2 @@
-query: Classes/SubclassShadowing.ql
+query: Classes/SubclassShadowing/SubclassShadowing.ql
postprocess: utils/test/InlineExpectationsTestQuery.ql
\ No newline at end of file
From 63577f0cca1f9346390c46a34893468172f6c4d5 Mon Sep 17 00:00:00 2001
From: Joe Farebrother
Date: Wed, 30 Jul 2025 15:52:26 +0100
Subject: [PATCH 078/984] Add extra example
---
.../SubclassShadowing/SubclassShadowing.qhelp | 6 ++++--
.../examples/SubclassShadowingBad.py | 9 +++++++++
.../examples/SubclassShadowingGood.py | 20 ++++++++++++-------
3 files changed, 26 insertions(+), 9 deletions(-)
create mode 100644 python/ql/src/Classes/SubclassShadowing/examples/SubclassShadowingBad.py
diff --git a/python/ql/src/Classes/SubclassShadowing/SubclassShadowing.qhelp b/python/ql/src/Classes/SubclassShadowing/SubclassShadowing.qhelp
index acbcae65318..5345d2c9178 100644
--- a/python/ql/src/Classes/SubclassShadowing/SubclassShadowing.qhelp
+++ b/python/ql/src/Classes/SubclassShadowing/SubclassShadowing.qhelp
@@ -26,12 +26,14 @@ In the following example, the _foo attribute of class AB()._foo() will result in a TypeError, as 3 will be called instead.
-
+
-In the following example...
+In the following example, the behavior of the default attribute being shadowed to allow for customization during initialization is
+intended in within the superclass A. Overriding default in the subclass B is then OK.
+
diff --git a/python/ql/src/Classes/SubclassShadowing/examples/SubclassShadowingBad.py b/python/ql/src/Classes/SubclassShadowing/examples/SubclassShadowingBad.py
new file mode 100644
index 00000000000..4699b58d7e4
--- /dev/null
+++ b/python/ql/src/Classes/SubclassShadowing/examples/SubclassShadowingBad.py
@@ -0,0 +1,9 @@
+class A:
+ def __init__(self):
+ self._foo = 3
+
+class B:
+ # BAD: _foo is shadowed by attribute A._foo
+ def _foo(self):
+ return 2
+
diff --git a/python/ql/src/Classes/SubclassShadowing/examples/SubclassShadowingGood.py b/python/ql/src/Classes/SubclassShadowing/examples/SubclassShadowingGood.py
index 4699b58d7e4..8fca041176c 100644
--- a/python/ql/src/Classes/SubclassShadowing/examples/SubclassShadowingGood.py
+++ b/python/ql/src/Classes/SubclassShadowing/examples/SubclassShadowingGood.py
@@ -1,9 +1,15 @@
class A:
- def __init__(self):
- self._foo = 3
-
-class B:
- # BAD: _foo is shadowed by attribute A._foo
- def _foo(self):
- return 2
+ def __init__(self, default_func=None):
+ if default_func is not None:
+ self.default = default_func
+ # GOOD: The shadowing behavior is explicitly intended in the superclass.
+ def default(self):
+ return []
+
+class B(A):
+
+ # Subclasses may override the method `default`, which will still be shadowed by the attribute `default` if it is set.
+ # As this is part of the expected behavior of the superclass, this is fine.
+ def default(self):
+ return {}
\ No newline at end of file
From 1efc09bbba4c408652544404054a5de0b4cdaff1 Mon Sep 17 00:00:00 2001
From: Joe Farebrother
Date: Wed, 30 Jul 2025 15:54:39 +0100
Subject: [PATCH 079/984] Update integration tests
---
.../query-suite/python-code-quality-extended.qls.expected | 2 +-
.../query-suite/python-code-quality.qls.expected | 2 +-
.../query-suite/python-security-and-quality.qls.expected | 2 +-
3 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/python/ql/integration-tests/query-suite/python-code-quality-extended.qls.expected b/python/ql/integration-tests/query-suite/python-code-quality-extended.qls.expected
index 960972c508c..bb44ee105b5 100644
--- a/python/ql/integration-tests/query-suite/python-code-quality-extended.qls.expected
+++ b/python/ql/integration-tests/query-suite/python-code-quality-extended.qls.expected
@@ -6,7 +6,7 @@ ql/python/ql/src/Classes/InitCallsSubclass/InitCallsSubclassMethod.ql
ql/python/ql/src/Classes/MissingCallToDel.ql
ql/python/ql/src/Classes/MissingCallToInit.ql
ql/python/ql/src/Classes/MutatingDescriptor.ql
-ql/python/ql/src/Classes/SubclassShadowing.ql
+ql/python/ql/src/Classes/SubclassShadowing/SubclassShadowing.ql
ql/python/ql/src/Classes/SuperclassDelCalledMultipleTimes.ql
ql/python/ql/src/Classes/SuperclassInitCalledMultipleTimes.ql
ql/python/ql/src/Classes/WrongNameForArgumentInClassInstantiation.ql
diff --git a/python/ql/integration-tests/query-suite/python-code-quality.qls.expected b/python/ql/integration-tests/query-suite/python-code-quality.qls.expected
index 960972c508c..bb44ee105b5 100644
--- a/python/ql/integration-tests/query-suite/python-code-quality.qls.expected
+++ b/python/ql/integration-tests/query-suite/python-code-quality.qls.expected
@@ -6,7 +6,7 @@ ql/python/ql/src/Classes/InitCallsSubclass/InitCallsSubclassMethod.ql
ql/python/ql/src/Classes/MissingCallToDel.ql
ql/python/ql/src/Classes/MissingCallToInit.ql
ql/python/ql/src/Classes/MutatingDescriptor.ql
-ql/python/ql/src/Classes/SubclassShadowing.ql
+ql/python/ql/src/Classes/SubclassShadowing/SubclassShadowing.ql
ql/python/ql/src/Classes/SuperclassDelCalledMultipleTimes.ql
ql/python/ql/src/Classes/SuperclassInitCalledMultipleTimes.ql
ql/python/ql/src/Classes/WrongNameForArgumentInClassInstantiation.ql
diff --git a/python/ql/integration-tests/query-suite/python-security-and-quality.qls.expected b/python/ql/integration-tests/query-suite/python-security-and-quality.qls.expected
index 170d9f442f9..8799990b86e 100644
--- a/python/ql/integration-tests/query-suite/python-security-and-quality.qls.expected
+++ b/python/ql/integration-tests/query-suite/python-security-and-quality.qls.expected
@@ -11,7 +11,7 @@ ql/python/ql/src/Classes/MutatingDescriptor.ql
ql/python/ql/src/Classes/OverwritingAttributeInSuperClass.ql
ql/python/ql/src/Classes/PropertyInOldStyleClass.ql
ql/python/ql/src/Classes/SlotsInOldStyleClass.ql
-ql/python/ql/src/Classes/SubclassShadowing.ql
+ql/python/ql/src/Classes/SubclassShadowing/SubclassShadowing.ql
ql/python/ql/src/Classes/SuperInOldStyleClass.ql
ql/python/ql/src/Classes/SuperclassDelCalledMultipleTimes.ql
ql/python/ql/src/Classes/SuperclassInitCalledMultipleTimes.ql
From 71a6b22815ef97b581de675470a6a128fe922667 Mon Sep 17 00:00:00 2001
From: Joe Farebrother
Date: Thu, 31 Jul 2025 06:05:25 +0100
Subject: [PATCH 080/984] Update
python/ql/src/Classes/SubclassShadowing/examples/SubclassShadowingBad.py
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
---
.../Classes/SubclassShadowing/examples/SubclassShadowingBad.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/python/ql/src/Classes/SubclassShadowing/examples/SubclassShadowingBad.py b/python/ql/src/Classes/SubclassShadowing/examples/SubclassShadowingBad.py
index 4699b58d7e4..00a221760b4 100644
--- a/python/ql/src/Classes/SubclassShadowing/examples/SubclassShadowingBad.py
+++ b/python/ql/src/Classes/SubclassShadowing/examples/SubclassShadowingBad.py
@@ -2,7 +2,7 @@ class A:
def __init__(self):
self._foo = 3
-class B:
+class B(A):
# BAD: _foo is shadowed by attribute A._foo
def _foo(self):
return 2
From 79d1deb28d0927a51b9909a24c59778dcd4bc325 Mon Sep 17 00:00:00 2001
From: Joe Farebrother
Date: Thu, 31 Jul 2025 06:05:48 +0100
Subject: [PATCH 081/984] Update
python/ql/src/Classes/SubclassShadowing/SubclassShadowing.ql
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
---
python/ql/src/Classes/SubclassShadowing/SubclassShadowing.ql | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/python/ql/src/Classes/SubclassShadowing/SubclassShadowing.ql b/python/ql/src/Classes/SubclassShadowing/SubclassShadowing.ql
index eab8520857b..39a320f75ac 100644
--- a/python/ql/src/Classes/SubclassShadowing/SubclassShadowing.ql
+++ b/python/ql/src/Classes/SubclassShadowing/SubclassShadowing.ql
@@ -64,7 +64,7 @@ where
if isProperty(shadowed)
then
// it's not a setter, so it's a read-only property
- extra = " (read-only property may cause an error if written to in the superclass.)"
+ extra = " (read-only property may cause an error if written to in the superclass)"
else extra = ""
)
select shadowed, "This method is shadowed by $@ in superclass $@." + extra, write,
From fd4233e30edc5b828c53f1b4b8cfe76becf154b3 Mon Sep 17 00:00:00 2001
From: Napalys Klicius
Date: Thu, 31 Jul 2025 10:53:03 +0200
Subject: [PATCH 082/984] Moved apollo modeling to MaD
---
javascript/ql/lib/ext/apollo-server.model.yml | 12 +++++++
.../semmle/javascript/frameworks/Apollo.qll | 36 -------------------
...sPermissiveConfigurationCustomizations.qll | 4 +--
3 files changed, 14 insertions(+), 38 deletions(-)
delete mode 100644 javascript/ql/lib/semmle/javascript/frameworks/Apollo.qll
diff --git a/javascript/ql/lib/ext/apollo-server.model.yml b/javascript/ql/lib/ext/apollo-server.model.yml
index ffceb6a6d5a..5962b8ee7d0 100644
--- a/javascript/ql/lib/ext/apollo-server.model.yml
+++ b/javascript/ql/lib/ext/apollo-server.model.yml
@@ -5,6 +5,12 @@ extensions:
data:
- ["@apollo/server", "Member[ApolloServer,ApolloServerBase].Argument[0].AnyMember.AnyMember.AnyMember.Parameter[1]", "remote"]
+ - addsTo:
+ pack: codeql/javascript-all
+ extensible: sinkModel
+ data:
+ - ["@apollo/server", "Member[gql].Argument[0]", "sql-injection"]
+
- addsTo:
pack: codeql/javascript-all
extensible: typeModel
@@ -13,3 +19,9 @@ extensions:
- ["@apollo/server", "apollo-server-express", ""]
- ["@apollo/server", "apollo-server-core", ""]
- ["@apollo/server", "apollo-server", ""]
+ - ["@apollo/server", "@apollo/apollo-server-express", ""]
+ - ["@apollo/server", "apollo-server-express", ""]
+ - ["@apollo/server", "@apollo/server", ""]
+ - ["@apollo/server", "@apollo/apollo-server-core", ""]
+ - ["ApolloServer", "@apollo/server", "Member[ApolloServer]"]
+ - ["GraphQLApollo", "@apollo/server", "Member[gql]"]
diff --git a/javascript/ql/lib/semmle/javascript/frameworks/Apollo.qll b/javascript/ql/lib/semmle/javascript/frameworks/Apollo.qll
deleted file mode 100644
index 983c0a8ac89..00000000000
--- a/javascript/ql/lib/semmle/javascript/frameworks/Apollo.qll
+++ /dev/null
@@ -1,36 +0,0 @@
-/**
- * Provides classes for working with Apollo GraphQL connectors.
- */
-
-import javascript
-
-/** Provides classes modeling the apollo packages [@apollo/server](https://npmjs.com/package/@apollo/server`) */
-module Apollo {
- /** Get a reference to the `ApolloServer` class. */
- private API::Node apollo() {
- result =
- API::moduleImport([
- "@apollo/server", "@apollo/apollo-server-express", "@apollo/apollo-server-core",
- "apollo-server", "apollo-server-express"
- ]).getMember("ApolloServer")
- }
-
- /** Gets a reference to the `gql` function that parses GraphQL strings. */
- private API::Node gql() {
- result =
- API::moduleImport([
- "@apollo/server", "@apollo/apollo-server-express", "@apollo/apollo-server-core",
- "apollo-server", "apollo-server-express"
- ]).getMember("gql")
- }
-
- /** An instantiation of an `ApolloServer`. */
- class ApolloServer extends API::NewNode {
- ApolloServer() { this = apollo().getAnInstantiation() }
- }
-
- /** A string that is interpreted as a GraphQL query by a `apollo` package. */
- private class ApolloGraphQLString extends GraphQL::GraphQLString {
- ApolloGraphQLString() { this = gql().getACall().getArgument(0) }
- }
-}
diff --git a/javascript/ql/lib/semmle/javascript/security/CorsPermissiveConfigurationCustomizations.qll b/javascript/ql/lib/semmle/javascript/security/CorsPermissiveConfigurationCustomizations.qll
index 4751ace2a60..a504f66ba22 100644
--- a/javascript/ql/lib/semmle/javascript/security/CorsPermissiveConfigurationCustomizations.qll
+++ b/javascript/ql/lib/semmle/javascript/security/CorsPermissiveConfigurationCustomizations.qll
@@ -5,7 +5,6 @@
*/
import javascript
-private import semmle.javascript.frameworks.Apollo
private import semmle.javascript.frameworks.Cors
/** Module containing sources, sinks, and sanitizers for overly permissive CORS configurations. */
@@ -109,7 +108,8 @@ module CorsPermissiveConfiguration {
*/
class CorsApolloServer extends Sink, DataFlow::ValueNode {
CorsApolloServer() {
- exists(Apollo::ApolloServer agql |
+ exists(API::NewNode agql |
+ agql = ModelOutput::getATypeNode("ApolloServer").getAnInstantiation() and
this =
agql.getOptionArgument(0, "cors").getALocalSource().getAPropertyWrite("origin").getRhs()
)
From 2baca58b278827703fd803889555c71b5bd05a8e Mon Sep 17 00:00:00 2001
From: Napalys Klicius
Date: Thu, 31 Jul 2025 11:08:22 +0200
Subject: [PATCH 083/984] Removed deprecations from cors as it was moved out
experimental
---
...sPermissiveConfigurationCustomizations.qll | 33 -------------------
.../CorsPermissiveConfigurationQuery.qll | 28 ----------------
2 files changed, 61 deletions(-)
diff --git a/javascript/ql/lib/semmle/javascript/security/CorsPermissiveConfigurationCustomizations.qll b/javascript/ql/lib/semmle/javascript/security/CorsPermissiveConfigurationCustomizations.qll
index a504f66ba22..583847ab0d9 100644
--- a/javascript/ql/lib/semmle/javascript/security/CorsPermissiveConfigurationCustomizations.qll
+++ b/javascript/ql/lib/semmle/javascript/security/CorsPermissiveConfigurationCustomizations.qll
@@ -24,22 +24,10 @@ module CorsPermissiveConfiguration {
or
this = TWildcard() and result = "wildcard"
}
-
- /** DEPRECATED: Converts this flow state to a flow label. */
- deprecated DataFlow::FlowLabel toFlowLabel() {
- this = TTaint() and result.isTaint()
- or
- this = TTrueOrNull() and result instanceof TrueAndNull
- or
- this = TWildcard() and result instanceof Wildcard
- }
}
/** Predicates for working with flow states. */
module FlowState {
- /** DEPRECATED: Gets a flow state from a flow label. */
- deprecated FlowState fromFlowLabel(DataFlow::FlowLabel label) { result.toFlowLabel() = label }
-
/** A tainted value. */
FlowState taint() { result = TTaint() }
@@ -65,11 +53,6 @@ module CorsPermissiveConfiguration {
*/
abstract class Sanitizer extends DataFlow::Node { }
- /**
- * DEPRECATED: Use `ActiveThreatModelSource` from Concepts instead!
- */
- deprecated class RemoteFlowSourceAsSource = ActiveThreatModelSourceAsSource;
-
/**
* An active threat-model source, considered as a flow source.
*/
@@ -77,22 +60,6 @@ module CorsPermissiveConfiguration {
ActiveThreatModelSourceAsSource() { not this instanceof ClientSideRemoteFlowSource }
}
- /** A flow label representing `true` and `null` values. */
- abstract deprecated class TrueAndNull extends DataFlow::FlowLabel {
- TrueAndNull() { this = "TrueAndNull" }
- }
-
- /** DEPRECATED: Gets a flow label representing `true` and `null` values. */
- deprecated TrueAndNull truenullLabel() { any() }
-
- /** A flow label representing `*` value. */
- abstract deprecated class Wildcard extends DataFlow::FlowLabel {
- Wildcard() { this = "Wildcard" }
- }
-
- /** DEPRECATED: Gets a flow label representing `*` value. */
- deprecated Wildcard wildcardLabel() { any() }
-
/** An overly permissive value for `origin` (Apollo) */
class TrueNullValue extends Source {
TrueNullValue() { this.mayHaveBooleanValue(true) or this.asExpr() instanceof NullLiteral }
diff --git a/javascript/ql/lib/semmle/javascript/security/CorsPermissiveConfigurationQuery.qll b/javascript/ql/lib/semmle/javascript/security/CorsPermissiveConfigurationQuery.qll
index 3605a1adaa9..0db678e43af 100644
--- a/javascript/ql/lib/semmle/javascript/security/CorsPermissiveConfigurationQuery.qll
+++ b/javascript/ql/lib/semmle/javascript/security/CorsPermissiveConfigurationQuery.qll
@@ -39,31 +39,3 @@ module CorsPermissiveConfigurationConfig implements DataFlow::StateConfigSig {
module CorsPermissiveConfigurationFlow =
TaintTracking::GlobalWithState;
-
-/**
- * DEPRECATED. Use the `CorsPermissiveConfigurationFlow` module instead.
- */
-deprecated class Configuration extends TaintTracking::Configuration {
- Configuration() { this = "CorsPermissiveConfiguration" }
-
- override predicate isSource(DataFlow::Node source, DataFlow::FlowLabel label) {
- CorsPermissiveConfigurationConfig::isSource(source, FlowState::fromFlowLabel(label))
- }
-
- override predicate isSink(DataFlow::Node sink, DataFlow::FlowLabel label) {
- CorsPermissiveConfigurationConfig::isSink(sink, FlowState::fromFlowLabel(label))
- }
-
- override predicate isSanitizer(DataFlow::Node node) {
- super.isSanitizer(node) or
- CorsPermissiveConfigurationConfig::isBarrier(node)
- }
-}
-
-deprecated private class WildcardActivated extends DataFlow::FlowLabel, Wildcard {
- WildcardActivated() { this = this }
-}
-
-deprecated private class TrueAndNullActivated extends DataFlow::FlowLabel, TrueAndNull {
- TrueAndNullActivated() { this = this }
-}
From 791a7e242e5ca1e9ad68c21a49cd76730bdb0370 Mon Sep 17 00:00:00 2001
From: Napalys Klicius
Date: Thu, 31 Jul 2025 11:31:10 +0200
Subject: [PATCH 084/984] Updated qhelp for cors permissive configuration
---
.../CWE-942/CorsPermissiveConfiguration.qhelp | 92 ++++++++++---------
1 file changed, 47 insertions(+), 45 deletions(-)
diff --git a/javascript/ql/src/Security/CWE-942/CorsPermissiveConfiguration.qhelp b/javascript/ql/src/Security/CWE-942/CorsPermissiveConfiguration.qhelp
index fc79eee743b..04796dfbc18 100644
--- a/javascript/ql/src/Security/CWE-942/CorsPermissiveConfiguration.qhelp
+++ b/javascript/ql/src/Security/CWE-942/CorsPermissiveConfiguration.qhelp
@@ -3,69 +3,71 @@
"qhelp.dtd">
-
-
+
+
- A server can use CORS (Cross-Origin Resource Sharing) to relax the
- restrictions imposed by the SOP (Same-Origin Policy), allowing controlled, secure
- cross-origin requests when necessary.
+ A server can use CORS (Cross-Origin Resource Sharing) to relax the
+ restrictions imposed by the Same-Origin Policy, allowing controlled, secure
+ cross-origin requests when necessary.
- A server with an overly permissive CORS configuration may inadvertently
- expose sensitive data or lead to CSRF which is an attack that allows attackers to trick
- users into performing unwanted operations in websites they're authenticated to.
+
+
-
+ A server with an overly permissive CORS configuration may inadvertently
+ expose sensitive data or enable CSRF attacks, which allow attackers to trick
+ users into performing unwanted operations on websites they're authenticated to.
-
+
+
-
-
+
+
- When the origin is set to true, it signifies that the server
- is accepting requests from any origin, potentially exposing the system to
- CSRF attacks. This can be fixed using false as origin value or using a whitelist.
+ When the origin is set to true, the server
+ accepts requests from any origin, potentially exposing the system to
+ CSRF attacks. Use false as the origin value or implement a whitelist
+ of allowed origins instead.
-
-
+
+
- On the other hand, if the origin is
- set to null, it can be exploited by an attacker to deceive a user into making
- requests from a null origin form, often hosted within a sandboxed iframe.
+ When the origin is set to null, it can be
+ exploited by an attacker who can deceive a user into making
+ requests from a null origin, often hosted within a sandboxed iframe.
-
+
+
-
+ If the origin value is user-controlled, ensure that the data
+ is properly sanitized and validated against a whitelist of allowed origins.
- If the origin value is user controlled, make sure that the data
- is properly sanitized.
+
+
-
-
+
+
-
-
+ In the following example, server_1 accepts requests from any origin
+ because the value of origin is set to true.
+ server_2 uses user-controlled data for the origin without validation.
- In the example below, the server_1 accepts requests from any origin
- since the value of origin is set to true.
- And server_2's origin is user-controlled.
+
-
+
-
+
-
+ To fix these issues, server_1 uses a restrictive CORS configuration
+ that is not vulnerable to CSRF attacks. server_2 properly validates
+ user-controlled data against a whitelist before using it.
- In the example below, the server_1 CORS is restrictive so it's not
- vulnerable to CSRF attacks. And server_2's is using properly sanitized
- user-controlled data.
+
+
From 021aa13ee2d544de36bcb26f16857c9c085401d2 Mon Sep 17 00:00:00 2001
From: Napalys Klicius
Date: Thu, 31 Jul 2025 12:45:34 +0200
Subject: [PATCH 085/984] Added change note
---
.../change-notes/2025-07-31-cors-move-out-of-experimental.md | 4 ++++
1 file changed, 4 insertions(+)
create mode 100644 javascript/ql/src/change-notes/2025-07-31-cors-move-out-of-experimental.md
diff --git a/javascript/ql/src/change-notes/2025-07-31-cors-move-out-of-experimental.md b/javascript/ql/src/change-notes/2025-07-31-cors-move-out-of-experimental.md
new file mode 100644
index 00000000000..112fb0c628f
--- /dev/null
+++ b/javascript/ql/src/change-notes/2025-07-31-cors-move-out-of-experimental.md
@@ -0,0 +1,4 @@
+---
+category: minorAnalysis
+---
+* The query "CORS misconfiguration" (`js/cors-misconfiguration`) has been promoted from experimental and is now part of the default security suite.
From d8083add3e0c9b8923471a3c16d2fb105f661024 Mon Sep 17 00:00:00 2001
From: Joe Farebrother
Date: Fri, 1 Aug 2025 12:35:01 +0100
Subject: [PATCH 086/984] Doc updates
---
python/ql/src/Functions/IncorrectRaiseInSpecialMethod.qhelp | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/python/ql/src/Functions/IncorrectRaiseInSpecialMethod.qhelp b/python/ql/src/Functions/IncorrectRaiseInSpecialMethod.qhelp
index 42d7d421b0a..44e79f3afdd 100644
--- a/python/ql/src/Functions/IncorrectRaiseInSpecialMethod.qhelp
+++ b/python/ql/src/Functions/IncorrectRaiseInSpecialMethod.qhelp
@@ -5,7 +5,7 @@
User-defined classes interact with the Python virtual machine via special methods (also called "magic methods").
For example, for a class to support addition it must implement the __add__ and __radd__ special methods.
-When the expression a + b is evaluated the Python virtual machine will call type(a).__add__(a, b) and if that
+When the expression a + b is evaluated, the Python virtual machine will call type(a).__add__(a, b), and if that
is not implemented it will call type(b).__radd__(b, a).
Since the virtual machine calls these special methods for common expressions, users of the class will expect these operations to raise standard exceptions.
@@ -31,8 +31,8 @@ Therefore, if a method is unable to perform the expected operation then its resp
-
If the method is intended to be abstract, and thus always raise an exception, then declare it so using the @abstractmethod decorator.
-Otherwise, either remove the method or ensure that the method raises an exception of the correct type.
+
If the method always raises as exception, then if it is intended to be an abstract method, the @abstractmethod decorator should be used.
+Otherwise, ensure that the method raises an exception of the correct type, or remove the method if the operation dos not need to be supported.
From bc60914ed7edc9ffd1065b8c64288f175c6264ad Mon Sep 17 00:00:00 2001
From: Joe Farebrother
Date: Fri, 1 Aug 2025 12:37:51 +0100
Subject: [PATCH 087/984] Update test output
---
.../Classes/subclass-shadowing/SubclassShadowing.expected | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/python/ql/test/query-tests/Classes/subclass-shadowing/SubclassShadowing.expected b/python/ql/test/query-tests/Classes/subclass-shadowing/SubclassShadowing.expected
index 3852b977a22..5f5513ae990 100644
--- a/python/ql/test/query-tests/Classes/subclass-shadowing/SubclassShadowing.expected
+++ b/python/ql/test/query-tests/Classes/subclass-shadowing/SubclassShadowing.expected
@@ -1,2 +1,2 @@
| subclass_shadowing.py:11:5:11:21 | Function shadow | This method is shadowed by $@ in superclass $@. | subclass_shadowing.py:7:9:7:19 | ControlFlowNode for Attribute | attribute shadow | subclass_shadowing.py:4:1:4:11 | Class Base | Base |
-| subclass_shadowing.py:41:5:41:18 | Function foo | This method is shadowed by $@ in superclass $@. (read-only property may cause an error if written to.) | subclass_shadowing.py:35:9:35:16 | ControlFlowNode for Attribute | attribute foo | subclass_shadowing.py:33:1:33:12 | Class Base3 | Base3 |
+| subclass_shadowing.py:41:5:41:18 | Function foo | This method is shadowed by $@ in superclass $@. (read-only property may cause an error if written to in the superclass.) | subclass_shadowing.py:35:9:35:16 | ControlFlowNode for Attribute | attribute foo | subclass_shadowing.py:33:1:33:12 | Class Base3 | Base3 |
From d41a5e3a25f7585c74551aa7e80716e9ff845cbd Mon Sep 17 00:00:00 2001
From: Napalys Klicius
Date: Thu, 7 Aug 2025 13:25:32 +0200
Subject: [PATCH 088/984] Java: Added basic test cases for `java/jvm-exit`
---
.../CallsToSystemExit.expected | 8 ++++
.../CallsToSystemExit/CallsToSystemExit.qlref | 2 +
.../CallsToSystemExit/ExampleRuntimeExit.java | 37 +++++++++++++++++++
.../CallsToSystemExit/ExampleRuntimeHalt.java | 25 +++++++++++++
.../CallsToSystemExit/ExampleSystemExit.java | 37 +++++++++++++++++++
5 files changed, 109 insertions(+)
create mode 100644 java/ql/test/query-tests/CallsToSystemExit/CallsToSystemExit.expected
create mode 100644 java/ql/test/query-tests/CallsToSystemExit/CallsToSystemExit.qlref
create mode 100644 java/ql/test/query-tests/CallsToSystemExit/ExampleRuntimeExit.java
create mode 100644 java/ql/test/query-tests/CallsToSystemExit/ExampleRuntimeHalt.java
create mode 100644 java/ql/test/query-tests/CallsToSystemExit/ExampleSystemExit.java
diff --git a/java/ql/test/query-tests/CallsToSystemExit/CallsToSystemExit.expected b/java/ql/test/query-tests/CallsToSystemExit/CallsToSystemExit.expected
new file mode 100644
index 00000000000..fa06d66fbc1
--- /dev/null
+++ b/java/ql/test/query-tests/CallsToSystemExit/CallsToSystemExit.expected
@@ -0,0 +1,8 @@
+| ExampleRuntimeExit.java:22:17:22:44 | exit(...) | Avoid calls to Runtime.exit() as this makes code harder to reuse. |
+| ExampleRuntimeExit.java:25:17:25:44 | exit(...) | Avoid calls to Runtime.exit() as this makes code harder to reuse. |
+| ExampleRuntimeExit.java:35:9:35:43 | exit(...) | Avoid calls to Runtime.exit() as this makes code harder to reuse. |
+| ExampleRuntimeHalt.java:18:17:18:44 | halt(...) | Avoid calls to Runtime.halt() as this makes code harder to reuse. |
+| ExampleRuntimeHalt.java:21:17:21:44 | halt(...) | Avoid calls to Runtime.halt() as this makes code harder to reuse. |
+| ExampleSystemExit.java:22:17:22:30 | exit(...) | Avoid calls to System.exit() as this makes code harder to reuse. |
+| ExampleSystemExit.java:25:17:25:30 | exit(...) | Avoid calls to System.exit() as this makes code harder to reuse. |
+| ExampleSystemExit.java:35:9:35:29 | exit(...) | Avoid calls to System.exit() as this makes code harder to reuse. |
diff --git a/java/ql/test/query-tests/CallsToSystemExit/CallsToSystemExit.qlref b/java/ql/test/query-tests/CallsToSystemExit/CallsToSystemExit.qlref
new file mode 100644
index 00000000000..4561fcfcfd0
--- /dev/null
+++ b/java/ql/test/query-tests/CallsToSystemExit/CallsToSystemExit.qlref
@@ -0,0 +1,2 @@
+query: Violations of Best Practice/Undesirable Calls/CallsToSystemExit.ql
+postprocess: utils/test/InlineExpectationsTestQuery.ql
diff --git a/java/ql/test/query-tests/CallsToSystemExit/ExampleRuntimeExit.java b/java/ql/test/query-tests/CallsToSystemExit/ExampleRuntimeExit.java
new file mode 100644
index 00000000000..78603ea2bd9
--- /dev/null
+++ b/java/ql/test/query-tests/CallsToSystemExit/ExampleRuntimeExit.java
@@ -0,0 +1,37 @@
+import java.io.FileOutputStream;
+import java.io.IOException;
+
+public class ExampleRuntimeExit {
+
+ public static void main(String[] args) {
+ Action action = new Action();
+ try {
+ action.run();
+ } catch (Exception e) {
+ printUsageAndExit(e.getMessage(), 1);
+ }
+ Runtime.getRuntime().exit(0); // COMPLIANT
+ }
+
+ static class Action {
+ public void run() {
+ try {
+ FileOutputStream fos = new FileOutputStream("output.txt");
+ fos.write("Hello, World!".getBytes());
+ fos.close();
+ Runtime.getRuntime().exit(0); // $ Alert
+ } catch (IOException e) {
+ e.printStackTrace();
+ Runtime.getRuntime().exit(1); // $ Alert
+ } catch (Exception e) {
+ // re-throw the exception
+ throw e;
+ }
+ }
+ }
+
+ protected static void printUsageAndExit(final String message, final int exitCode) {
+ System.err.println("Usage: : " + message);
+ Runtime.getRuntime().exit(exitCode); // $ SPURIOUS: Alert
+ }
+}
diff --git a/java/ql/test/query-tests/CallsToSystemExit/ExampleRuntimeHalt.java b/java/ql/test/query-tests/CallsToSystemExit/ExampleRuntimeHalt.java
new file mode 100644
index 00000000000..b1d4be04f20
--- /dev/null
+++ b/java/ql/test/query-tests/CallsToSystemExit/ExampleRuntimeHalt.java
@@ -0,0 +1,25 @@
+import java.io.FileOutputStream;
+import java.io.IOException;
+
+public class ExampleRuntimeHalt {
+
+ public static void main(String[] args) {
+ Action action = new Action();
+ action.run();
+ Runtime.getRuntime().halt(0); // COMPLIANT
+ }
+
+ static class Action {
+ public void run() {
+ try {
+ FileOutputStream fos = new FileOutputStream("output.txt");
+ fos.write("Hello, World!".getBytes());
+ fos.close();
+ Runtime.getRuntime().halt(0); // $ Alert
+ } catch (IOException e) {
+ e.printStackTrace();
+ Runtime.getRuntime().halt(1); // $ Alert
+ }
+ }
+ }
+}
diff --git a/java/ql/test/query-tests/CallsToSystemExit/ExampleSystemExit.java b/java/ql/test/query-tests/CallsToSystemExit/ExampleSystemExit.java
new file mode 100644
index 00000000000..fbb383550b6
--- /dev/null
+++ b/java/ql/test/query-tests/CallsToSystemExit/ExampleSystemExit.java
@@ -0,0 +1,37 @@
+import java.io.FileOutputStream;
+import java.io.IOException;
+
+public class ExampleSystemExit {
+
+ public static void main(String[] args) {
+ Action action = new Action();
+ try {
+ action.run();
+ } catch (Exception e) {
+ printUsageAndExit(e.getMessage(), 1);
+ }
+ System.exit(0); // COMPLIANT
+ }
+
+ static class Action {
+ public void run() {
+ try {
+ FileOutputStream fos = new FileOutputStream("output.txt");
+ fos.write("Hello, World!".getBytes());
+ fos.close();
+ System.exit(0); // $ Alert
+ } catch (IOException e) {
+ e.printStackTrace();
+ System.exit(1); // $ Alert
+ } catch (Exception e) {
+ // re-throw the exception
+ throw e;
+ }
+ }
+ }
+
+ protected static void printUsageAndExit(final String message, final int exitCode) {
+ System.err.println("Usage: : " + message);
+ System.exit(exitCode); // $ SPURIOUS: Alert
+ }
+}
From 4df613ce37251ef60dc3d82760366520ecf2be8a Mon Sep 17 00:00:00 2001
From: Napalys Klicius
Date: Thu, 7 Aug 2025 15:39:15 +0200
Subject: [PATCH 089/984] Java: Improved `java/jvm-exit` query to remove FP's.
---
.../Undesirable Calls/CallsToSystemExit.ql | 78 +++++++++++++++----
.../CallsToSystemExit.expected | 14 ++--
.../CallsToSystemExit/ExampleRuntimeExit.java | 2 +-
.../CallsToSystemExit/ExampleSystemExit.java | 2 +-
4 files changed, 72 insertions(+), 24 deletions(-)
diff --git a/java/ql/src/Violations of Best Practice/Undesirable Calls/CallsToSystemExit.ql b/java/ql/src/Violations of Best Practice/Undesirable Calls/CallsToSystemExit.ql
index 93d7911694c..3760edf5663 100644
--- a/java/ql/src/Violations of Best Practice/Undesirable Calls/CallsToSystemExit.ql
+++ b/java/ql/src/Violations of Best Practice/Undesirable Calls/CallsToSystemExit.ql
@@ -13,17 +13,67 @@
import java
-from Method m, MethodCall sysexitCall, Method sysexit, Class system
-where
- sysexitCall = m.getACallSite(sysexit) and
- (sysexit.hasName("exit") or sysexit.hasName("halt")) and
- sysexit.getDeclaringType() = system and
- (
- system.hasQualifiedName("java.lang", "System") or
- system.hasQualifiedName("java.lang", "Runtime")
- ) and
- m.fromSource() and
- not m instanceof MainMethod
-select sysexitCall,
- "Avoid calls to " + sysexit.getDeclaringType().getName() + "." + sysexit.getName() +
- "() as this makes code harder to reuse."
+/**
+ * A `Method` which, when called, causes the JVM to exit or halt.
+ * Explicitly includes these methods from the java standard library:
+ * - `java.lang.System.exit`
+ * - `java.lang.Runtime.halt`
+ * - `java.lang.Runtime.exit`
+ */
+class ExitOrHaltMethod extends Method {
+ ExitOrHaltMethod() {
+ exists(Class system |
+ this.getDeclaringType() = system and
+ (
+ this.hasName("exit") and
+ (
+ system.hasQualifiedName("java.lang", "System") or
+ system.hasQualifiedName("java.lang", "Runtime")
+ )
+ or
+ this.hasName("halt") and
+ system.hasQualifiedName("java.lang", "Runtime")
+ )
+ )
+ }
+}
+
+/** A `MethodCall` to an `ExitOrHaltMethod`, which causes the JVM to exit abruptly. */
+class ExitOrHaltMethodCall extends MethodCall {
+ ExitOrHaltMethodCall() {
+ exists(ExitOrHaltMethod exitMethod | this.getMethod() = exitMethod |
+ exists(SourceMethodNotMainOrTest srcMethod | this = srcMethod.getACallSite(exitMethod))
+ )
+ }
+}
+
+/**
+ * Represents an intentional `MethodCall` to a system or runtime "exit" method, such as for
+ * functions which exist for the purpose of exiting the program. Assumes that a an exit method
+ * call within a method is intentional if the exit code is passed from a parameter of the
+ * enclosing method.
+ */
+class IntentionalExitMethodCall extends ExitOrHaltMethodCall {
+ IntentionalExitMethodCall() {
+ this.getMethod().hasName("exit") and
+ this.getAnArgument() = this.getEnclosingCallable().getAParameter().getAnAccess()
+ }
+}
+
+/**
+ * A `Method` that is defined in source code and is not a `MainMethod` or a `LikelyTestMethod`.
+ */
+class SourceMethodNotMainOrTest extends Method {
+ SourceMethodNotMainOrTest() {
+ this.fromSource() and
+ not this instanceof MainMethod and
+ not this instanceof LikelyTestMethod and
+ not this.getEnclosingCallable() instanceof LikelyTestMethod
+ }
+}
+
+from ExitOrHaltMethodCall mc
+where not mc instanceof IntentionalExitMethodCall
+select mc,
+ "Avoid calls to " + mc.getMethod().getDeclaringType().getName() + "." + mc.getMethod().getName() +
+ "() as this prevents runtime cleanup and makes code harder to reuse."
diff --git a/java/ql/test/query-tests/CallsToSystemExit/CallsToSystemExit.expected b/java/ql/test/query-tests/CallsToSystemExit/CallsToSystemExit.expected
index fa06d66fbc1..cad6d0097c7 100644
--- a/java/ql/test/query-tests/CallsToSystemExit/CallsToSystemExit.expected
+++ b/java/ql/test/query-tests/CallsToSystemExit/CallsToSystemExit.expected
@@ -1,8 +1,6 @@
-| ExampleRuntimeExit.java:22:17:22:44 | exit(...) | Avoid calls to Runtime.exit() as this makes code harder to reuse. |
-| ExampleRuntimeExit.java:25:17:25:44 | exit(...) | Avoid calls to Runtime.exit() as this makes code harder to reuse. |
-| ExampleRuntimeExit.java:35:9:35:43 | exit(...) | Avoid calls to Runtime.exit() as this makes code harder to reuse. |
-| ExampleRuntimeHalt.java:18:17:18:44 | halt(...) | Avoid calls to Runtime.halt() as this makes code harder to reuse. |
-| ExampleRuntimeHalt.java:21:17:21:44 | halt(...) | Avoid calls to Runtime.halt() as this makes code harder to reuse. |
-| ExampleSystemExit.java:22:17:22:30 | exit(...) | Avoid calls to System.exit() as this makes code harder to reuse. |
-| ExampleSystemExit.java:25:17:25:30 | exit(...) | Avoid calls to System.exit() as this makes code harder to reuse. |
-| ExampleSystemExit.java:35:9:35:29 | exit(...) | Avoid calls to System.exit() as this makes code harder to reuse. |
+| ExampleRuntimeExit.java:22:17:22:44 | exit(...) | Avoid calls to Runtime.exit() as this prevents runtime cleanup and makes code harder to reuse. |
+| ExampleRuntimeExit.java:25:17:25:44 | exit(...) | Avoid calls to Runtime.exit() as this prevents runtime cleanup and makes code harder to reuse. |
+| ExampleRuntimeHalt.java:18:17:18:44 | halt(...) | Avoid calls to Runtime.halt() as this prevents runtime cleanup and makes code harder to reuse. |
+| ExampleRuntimeHalt.java:21:17:21:44 | halt(...) | Avoid calls to Runtime.halt() as this prevents runtime cleanup and makes code harder to reuse. |
+| ExampleSystemExit.java:22:17:22:30 | exit(...) | Avoid calls to System.exit() as this prevents runtime cleanup and makes code harder to reuse. |
+| ExampleSystemExit.java:25:17:25:30 | exit(...) | Avoid calls to System.exit() as this prevents runtime cleanup and makes code harder to reuse. |
diff --git a/java/ql/test/query-tests/CallsToSystemExit/ExampleRuntimeExit.java b/java/ql/test/query-tests/CallsToSystemExit/ExampleRuntimeExit.java
index 78603ea2bd9..13fd53b1141 100644
--- a/java/ql/test/query-tests/CallsToSystemExit/ExampleRuntimeExit.java
+++ b/java/ql/test/query-tests/CallsToSystemExit/ExampleRuntimeExit.java
@@ -32,6 +32,6 @@ public class ExampleRuntimeExit {
protected static void printUsageAndExit(final String message, final int exitCode) {
System.err.println("Usage: : " + message);
- Runtime.getRuntime().exit(exitCode); // $ SPURIOUS: Alert
+ Runtime.getRuntime().exit(exitCode); // COMPLIANT
}
}
diff --git a/java/ql/test/query-tests/CallsToSystemExit/ExampleSystemExit.java b/java/ql/test/query-tests/CallsToSystemExit/ExampleSystemExit.java
index fbb383550b6..dece6e689ba 100644
--- a/java/ql/test/query-tests/CallsToSystemExit/ExampleSystemExit.java
+++ b/java/ql/test/query-tests/CallsToSystemExit/ExampleSystemExit.java
@@ -32,6 +32,6 @@ public class ExampleSystemExit {
protected static void printUsageAndExit(final String message, final int exitCode) {
System.err.println("Usage: : " + message);
- System.exit(exitCode); // $ SPURIOUS: Alert
+ System.exit(exitCode); // COMPLIANT
}
}
From f6aad965049f7b6b6e1a9bb50e9a4692b59a6263 Mon Sep 17 00:00:00 2001
From: Napalys Klicius
Date: Fri, 8 Aug 2025 09:54:27 +0200
Subject: [PATCH 090/984] Java: Update docs and promote to quality
`java/jvm-exit`
---
.../java-code-quality-extended.qls.expected | 1 +
.../query-suite/not_included_in_qls.expected | 1 -
.../Undesirable Calls/CallsToSystemExit.java | 29 ++++++++++++++++---
.../Undesirable Calls/CallsToSystemExit.qhelp | 27 ++++++++++-------
.../Undesirable Calls/CallsToSystemExit.ql | 27 +++++++----------
5 files changed, 53 insertions(+), 32 deletions(-)
diff --git a/java/ql/integration-tests/java/query-suite/java-code-quality-extended.qls.expected b/java/ql/integration-tests/java/query-suite/java-code-quality-extended.qls.expected
index 7a1a986b2aa..8e79288d940 100644
--- a/java/ql/integration-tests/java/query-suite/java-code-quality-extended.qls.expected
+++ b/java/ql/integration-tests/java/query-suite/java-code-quality-extended.qls.expected
@@ -81,6 +81,7 @@ ql/java/ql/src/Violations of Best Practice/Naming Conventions/SameNameAsSuper.ql
ql/java/ql/src/Violations of Best Practice/Records/IgnoredSerializationMembersOfRecordClass.ql
ql/java/ql/src/Violations of Best Practice/SpecialCharactersInLiterals/NonExplicitControlAndWhitespaceCharsInLiterals.ql
ql/java/ql/src/Violations of Best Practice/Undesirable Calls/CallsToStringToString.ql
+ql/java/ql/src/Violations of Best Practice/Undesirable Calls/CallsToSystemExit.ql
ql/java/ql/src/Violations of Best Practice/Undesirable Calls/DefaultToString.ql
ql/java/ql/src/Violations of Best Practice/Undesirable Calls/DoNotCallFinalize.ql
ql/java/ql/src/Violations of Best Practice/Undesirable Calls/PrintLnArray.ql
diff --git a/java/ql/integration-tests/java/query-suite/not_included_in_qls.expected b/java/ql/integration-tests/java/query-suite/not_included_in_qls.expected
index 1f58e51ad80..a0bc27b8051 100644
--- a/java/ql/integration-tests/java/query-suite/not_included_in_qls.expected
+++ b/java/ql/integration-tests/java/query-suite/not_included_in_qls.expected
@@ -187,7 +187,6 @@ ql/java/ql/src/Violations of Best Practice/Magic Constants/MagicNumbersUseConsta
ql/java/ql/src/Violations of Best Practice/Magic Constants/MagicStringsUseConstant.ql
ql/java/ql/src/Violations of Best Practice/Naming Conventions/ConfusingOverridesNames.ql
ql/java/ql/src/Violations of Best Practice/Naming Conventions/LocalShadowsField.ql
-ql/java/ql/src/Violations of Best Practice/Undesirable Calls/CallsToSystemExit.ql
ql/java/ql/src/Violations of Best Practice/Undesirable Calls/GarbageCollection.ql
ql/java/ql/src/Violations of Best Practice/legacy/AutoBoxing.ql
ql/java/ql/src/Violations of Best Practice/legacy/FinallyMayNotComplete.ql
diff --git a/java/ql/src/Violations of Best Practice/Undesirable Calls/CallsToSystemExit.java b/java/ql/src/Violations of Best Practice/Undesirable Calls/CallsToSystemExit.java
index da7277aa25c..50e0fb1cbda 100644
--- a/java/ql/src/Violations of Best Practice/Undesirable Calls/CallsToSystemExit.java
+++ b/java/ql/src/Violations of Best Practice/Undesirable Calls/CallsToSystemExit.java
@@ -4,7 +4,7 @@ class FileOutput {
try {
output.write(s.getBytes());
} catch (IOException e) {
- System.exit(1);
+ System.exit(1); // BAD: Should handle or propagate error instead of exiting
}
return true;
}
@@ -16,9 +16,30 @@ class Action {
// ...
// Perform tasks ...
// ...
- System.exit(0);
+ System.exit(0); // BAD: Should return status or throw exception
}
public static void main(String[] args) {
- new Action(args).run();
+ new Action().run();
}
-}
\ No newline at end of file
+}
+
+// Good example: Proper error handling
+class BetterAction {
+ public int run() throws Exception {
+ // ...
+ // Perform tasks ...
+ // ...
+ return 0; // Return status instead of calling System.exit
+ }
+
+ public static void main(String[] args) {
+ try {
+ BetterAction action = new BetterAction();
+ int exitCode = action.run();
+ System.exit(exitCode); // GOOD: Exit from main method
+ } catch (Exception e) {
+ System.err.println("Error: " + e.getMessage());
+ System.exit(1); // GOOD: Exit from main method on error
+ }
+ }
+}
diff --git a/java/ql/src/Violations of Best Practice/Undesirable Calls/CallsToSystemExit.qhelp b/java/ql/src/Violations of Best Practice/Undesirable Calls/CallsToSystemExit.qhelp
index e4d4fa7a7f0..6992a734607 100644
--- a/java/ql/src/Violations of Best Practice/Undesirable Calls/CallsToSystemExit.qhelp
+++ b/java/ql/src/Violations of Best Practice/Undesirable Calls/CallsToSystemExit.qhelp
@@ -13,17 +13,20 @@ program state from being written to disk consistently.
It is sometimes considered acceptable to call System.exit
from a program's main method in order to indicate the overall exit status
-of the program. Such calls are an exception to this rule.
+of the program. The main method should be the primary place
+where exit conditions are handled, as it represents the natural termination point
+of the application. Such calls are an exception to this rule.
-
It is usually preferable to use a different mechanism for reporting
-failure conditions. Consider returning a special value (perhaps
-null) that users of the current method check for and
-recover from appropriately. Alternatively, throw a suitable exception, which
-unwinds the stack and allows properly written code to clean up after itself,
-while leaving other threads undisturbed.
+
Instead of calling System.exit from non-main methods, prefer to propagate
+errors upward to the main method where they can be handled appropriately.
+Consider returning a special value (perhaps null) that users of the current
+method check for and recover from appropriately. Alternatively, throw a suitable exception,
+which unwinds the stack and allows properly written code to clean up after itself,
+while leaving other threads undisturbed. The main method can then catch
+these exceptions and decide whether to exit the program and with what exit code.
@@ -38,12 +41,14 @@ upwards and be handled by a method that knows how to recover.
Problem 2 is more subtle. In this example, there is just one entry point to
the program (the main method), which constructs an
Action and performs it. Action.run calls
-System.exit to indicate successful completion. Consider,
-however, how this code might be integrated in an application server that
-constructs Action instances and calls
+System.exit to indicate successful completion. Instead, the
+run method should return a status code or throw an exception
+on failure, allowing the main method to decide whether to exit
+and with what exit code. Consider how this code might be integrated in an
+application server that constructs Action instances and calls
run on them without going through main.
The fact that run terminates the JVM instead of returning its
-exit code as an integer makes that use-case impossible.
+exit code makes that use-case impossible.
diff --git a/java/ql/src/Violations of Best Practice/Undesirable Calls/CallsToSystemExit.ql b/java/ql/src/Violations of Best Practice/Undesirable Calls/CallsToSystemExit.ql
index 3760edf5663..c17141122d1 100644
--- a/java/ql/src/Violations of Best Practice/Undesirable Calls/CallsToSystemExit.ql
+++ b/java/ql/src/Violations of Best Practice/Undesirable Calls/CallsToSystemExit.ql
@@ -4,10 +4,11 @@
* reuse and prevent important cleanup steps from running.
* @kind problem
* @problem.severity warning
- * @precision low
+ * @precision medium
* @id java/jvm-exit
- * @tags reliability
- * maintainability
+ * @tags quality
+ * reliability
+ * correctness
* external/cwe/cwe-382
*/
@@ -22,18 +23,12 @@ import java
*/
class ExitOrHaltMethod extends Method {
ExitOrHaltMethod() {
- exists(Class system |
- this.getDeclaringType() = system and
- (
- this.hasName("exit") and
- (
- system.hasQualifiedName("java.lang", "System") or
- system.hasQualifiedName("java.lang", "Runtime")
- )
- or
- this.hasName("halt") and
- system.hasQualifiedName("java.lang", "Runtime")
- )
+ exists(Class system | this.getDeclaringType() = system |
+ this.hasName("exit") and
+ system.hasQualifiedName("java.lang", ["System", "Runtime"])
+ or
+ this.hasName("halt") and
+ system.hasQualifiedName("java.lang", "Runtime")
)
}
}
@@ -48,7 +43,7 @@ class ExitOrHaltMethodCall extends MethodCall {
}
/**
- * Represents an intentional `MethodCall` to a system or runtime "exit" method, such as for
+ * An intentional `MethodCall` to a system or runtime "exit" method, such as for
* functions which exist for the purpose of exiting the program. Assumes that a an exit method
* call within a method is intentional if the exit code is passed from a parameter of the
* enclosing method.
From 50c7160819ca68858072af60c46b1f186bd0faa9 Mon Sep 17 00:00:00 2001
From: Napalys Klicius
Date: Mon, 11 Aug 2025 10:36:02 +0200
Subject: [PATCH 091/984] Java: port
`java/mocking-all-non-private-methods-means-unit-test-is-too-big` query
---
...lNonPrivateMethodsMeansUnitTestIsTooBig.md | 56 +++
...lNonPrivateMethodsMeansUnitTestIsTooBig.ql | 73 +++
.../Employee.java | 10 +
.../EmployeeRecord.java | 26 +
...ivateMethodsMeansUnitTestIsTooBig.expected | 2 +
...nPrivateMethodsMeansUnitTestIsTooBig.qlref | 1 +
.../TestORM.java | 55 ++
.../options | 1 +
.../test/stubs/junit-4.13/LICENSE-junit.txt | 214 ++++++++
.../stubs/junit-4.13/org/junit/Assert.java | 472 ++++++++++++++++++
.../test/stubs/junit-4.13/org/junit/Test.java | 28 ++
.../org/junit/function/ThrowingRunnable.java | 14 +
.../org/mockito/ArgumentMatchers.java | 4 +
.../org/mockito/MockSettings.java | 10 +
.../mockito-5.14/org/mockito/Mockito.java | 216 ++++++++
.../org/mockito/internal/MockitoCore.java | 28 ++
.../internal/creation/MockSettingsImpl.java | 14 +
.../internal/handler/MockHandlerFactory.java | 14 +
.../internal/handler/MockHandlerImpl.java | 10 +
.../internal/progress/MockingProgress.java | 11 +
.../org/mockito/internal/util/MockUtil.java | 18 +
.../org/mockito/invocation/MockHandler.java | 10 +
.../mockito/mock/MockCreationSettings.java | 9 +
.../org/mockito/plugins/MockMaker.java | 8 +
.../org/mockito/stubbing/Answer.java | 7 +
.../org/mockito/stubbing/OngoingStubbing.java | 9 +
.../org/mockito/stubbing/Stubber.java | 5 +
27 files changed, 1325 insertions(+)
create mode 100644 java/ql/src/Likely Bugs/Frameworks/JUnit/MockingAllNonPrivateMethodsMeansUnitTestIsTooBig.md
create mode 100644 java/ql/src/Likely Bugs/Frameworks/JUnit/MockingAllNonPrivateMethodsMeansUnitTestIsTooBig.ql
create mode 100644 java/ql/test/query-tests/MockingAllNonPrivateMethodsMeansUnitTestIsTooBig/Employee.java
create mode 100644 java/ql/test/query-tests/MockingAllNonPrivateMethodsMeansUnitTestIsTooBig/EmployeeRecord.java
create mode 100644 java/ql/test/query-tests/MockingAllNonPrivateMethodsMeansUnitTestIsTooBig/MockingAllNonPrivateMethodsMeansUnitTestIsTooBig.expected
create mode 100644 java/ql/test/query-tests/MockingAllNonPrivateMethodsMeansUnitTestIsTooBig/MockingAllNonPrivateMethodsMeansUnitTestIsTooBig.qlref
create mode 100644 java/ql/test/query-tests/MockingAllNonPrivateMethodsMeansUnitTestIsTooBig/TestORM.java
create mode 100644 java/ql/test/query-tests/MockingAllNonPrivateMethodsMeansUnitTestIsTooBig/options
create mode 100644 java/ql/test/stubs/junit-4.13/LICENSE-junit.txt
create mode 100644 java/ql/test/stubs/junit-4.13/org/junit/Assert.java
create mode 100644 java/ql/test/stubs/junit-4.13/org/junit/Test.java
create mode 100644 java/ql/test/stubs/junit-4.13/org/junit/function/ThrowingRunnable.java
create mode 100644 java/ql/test/stubs/mockito-5.14/org/mockito/ArgumentMatchers.java
create mode 100644 java/ql/test/stubs/mockito-5.14/org/mockito/MockSettings.java
create mode 100644 java/ql/test/stubs/mockito-5.14/org/mockito/Mockito.java
create mode 100644 java/ql/test/stubs/mockito-5.14/org/mockito/internal/MockitoCore.java
create mode 100644 java/ql/test/stubs/mockito-5.14/org/mockito/internal/creation/MockSettingsImpl.java
create mode 100644 java/ql/test/stubs/mockito-5.14/org/mockito/internal/handler/MockHandlerFactory.java
create mode 100644 java/ql/test/stubs/mockito-5.14/org/mockito/internal/handler/MockHandlerImpl.java
create mode 100644 java/ql/test/stubs/mockito-5.14/org/mockito/internal/progress/MockingProgress.java
create mode 100644 java/ql/test/stubs/mockito-5.14/org/mockito/internal/util/MockUtil.java
create mode 100644 java/ql/test/stubs/mockito-5.14/org/mockito/invocation/MockHandler.java
create mode 100644 java/ql/test/stubs/mockito-5.14/org/mockito/mock/MockCreationSettings.java
create mode 100644 java/ql/test/stubs/mockito-5.14/org/mockito/plugins/MockMaker.java
create mode 100644 java/ql/test/stubs/mockito-5.14/org/mockito/stubbing/Answer.java
create mode 100644 java/ql/test/stubs/mockito-5.14/org/mockito/stubbing/OngoingStubbing.java
create mode 100644 java/ql/test/stubs/mockito-5.14/org/mockito/stubbing/Stubber.java
diff --git a/java/ql/src/Likely Bugs/Frameworks/JUnit/MockingAllNonPrivateMethodsMeansUnitTestIsTooBig.md b/java/ql/src/Likely Bugs/Frameworks/JUnit/MockingAllNonPrivateMethodsMeansUnitTestIsTooBig.md
new file mode 100644
index 00000000000..80a0c7e00f0
--- /dev/null
+++ b/java/ql/src/Likely Bugs/Frameworks/JUnit/MockingAllNonPrivateMethodsMeansUnitTestIsTooBig.md
@@ -0,0 +1,56 @@
+# J-T-001: Mocking all non-private methods of a class may indicate the unit test is testing too much
+
+Mocking too many non-private methods of a class may indicate that the test is too complicated, possibly because it is trying to test multiple things at once.
+
+## Overview
+
+Mocking methods of a class is necessary for a unit test to run without overhead caused by expensive I/O operations necessary to compute their values. However, if a unit test ends up mocking all of them, it is likely a signal that the scope of the unit test is reaching beyond a single unit of functionality.
+
+## Recommendation
+
+It is best to contain the scope of a single unit test to a single unit of functionality. For example, a unit test may aim to test a series of data-transforming functions that depends on an ORM class. Even though the functions may be semantically related with one another, it is better to create a unit test for each function.
+
+## Example
+
+The following example mocks all methods of an ORM class named `EmployeeRecord`, and tests four functions against them. Since the scope of the unit test harbors all four of them, all of the methods provided by the class are mocked.
+
+```java
+public class EmployeeRecord {
+ public int add(Employee employee) { ... }
+
+ public Employee get(String name) { ... }
+
+ public int update(Employee employee, String newName) { ... }
+
+ public int delete(Employee employee) { ... }
+}
+
+public class TestORM {
+ @Test
+ public void nonCompliant() {
+ Employee sampleEmployee = new Employee("John Doe");
+ EmployeeRecord employeeRecordMock = mock(EmployeeRecord.class); // NON_COMPLIANT: Mocked class has all of its public methods used in the test
+ when(employeeRecordMock.add(Employee.class)).thenReturn(0); // Mocked EmployeeRecord.add
+ when(employeeRecordMock.get(String.class)).thenReturn(sampleEmployee); // Mocked EmployeeRecord.get
+ when(employeeRecordMock.update(Employee.class, String.class)).thenReturn(0); // Mocked EmployeeRecord.update
+ when(employeeRecordMock.delete(Employee.class)).thenReturn(0); // Mocked EmployeeRecord.delete
+ }
+
+ @Test
+ public void compliant1() {
+ Employee sampleEmployee = new Employee("John Doe");
+ EmployeeRecord employeeRecordMock = mock(EmployeeRecord.class); // COMPLIANT: Only some of the public methods belonging to the mocked object are used
+ when(employeeRecordMock.add(sampleEmployee)).thenReturn(0); // Mocked EmployeeRecord.add
+ when(employeeRecordMock.update(sampleEmployee, "Jane Doe")).thenReturn(0); // Mocked EmployeeRecord.update
+ }
+
+}
+```
+
+## Implementation Notes
+
+JUnit provides two different ways of mocking a method call: `when(mockedObject.methodToMock(...)).thenReturn(...)` and `doReturn(...).when(mockedObject).methodToMock(...)`. Both forms are taken into account by the query.
+
+## References
+
+- Baeldung: [Best Practices for Unit Testing in Java](https://www.baeldung.com/java-unit-testing-best-practices).
diff --git a/java/ql/src/Likely Bugs/Frameworks/JUnit/MockingAllNonPrivateMethodsMeansUnitTestIsTooBig.ql b/java/ql/src/Likely Bugs/Frameworks/JUnit/MockingAllNonPrivateMethodsMeansUnitTestIsTooBig.ql
new file mode 100644
index 00000000000..817763494c6
--- /dev/null
+++ b/java/ql/src/Likely Bugs/Frameworks/JUnit/MockingAllNonPrivateMethodsMeansUnitTestIsTooBig.ql
@@ -0,0 +1,73 @@
+/**
+ * @id java/mocking-all-non-private-methods-means-unit-test-is-too-big
+ * @name J-T-001: Mocking all non-private methods of a class may indicate the unit test is testing too much
+ * @description Mocking all non-private methods provided by a class might indicate the unit test
+ * aims to test too many things.
+ * @kind problem
+ * @precision high
+ * @problem.severity recommendation
+ * @tags maintainability
+ * readability
+ */
+
+import java
+
+class MockitoMockCall extends MethodCall {
+ MockitoMockCall() { this.getMethod().hasQualifiedName("org.mockito", "Mockito", "mock") }
+
+ /**
+ * Gets the type that this call intends to mock. e.g.
+ * ``` java
+ * EmployeeRecord employeeRecordMock = mock(EmployeeRecord.class);
+ * ```
+ * This predicate gets the class `EmployeeRecord` in the above example.
+ */
+ Type getMockedType() { result = this.getAnArgument().(TypeLiteral).getReferencedType() }
+}
+
+/**
+ * A method call that mocks a target method in a JUnit test. e.g.
+ * ``` java
+ * EmployeeRecord employeeRecordMock = mock(EmployeeRecord.class);
+ * when(employeeRecordMock.add(sampleEmployee)).thenReturn(0); // Mocked EmployeeRecord.add
+ * doReturn(0).when(employeeRecordMock).add(sampleEmployee); // Mocked EmployeeRecord.add
+ * ```
+ * This class captures the call to `add` which mocks the equivalent method of the class `EmployeeRecord`.
+ */
+class MockitoMockingMethodCall extends MethodCall {
+ MockitoMockCall mockCall;
+
+ MockitoMockingMethodCall() {
+ /* 1. The qualifier originates from the mock call. */
+ this.getQualifier().getControlFlowNode().getAPredecessor+() = mockCall.getControlFlowNode() and
+ /* 2. The mocked method can be found in the class being mocked with the mock call. */
+ mockCall.getMockedType().(ClassOrInterface).getAMethod() = this.getMethod()
+ }
+
+ /**
+ * Gets the call to Mockito's `mock` from which the qualifier, the mocked object, originates.
+ */
+ MockitoMockCall getMockitoMockCall() { result = mockCall }
+}
+
+/*
+ * The following from-which-select embodies this pseudocode:
+ * - Find a JUnit4TestMethod which:
+ * - for a class that it mocks with a call to `mock`,
+ * - for all methods that the class has, there is a method that this test method mocks.
+ */
+
+from JUnit4TestMethod testMethod, ClassOrInterface mockedClassOrInterface
+where
+ exists(MockitoMockCall mockCall |
+ mockCall.getParent+().(Stmt) = testMethod.getBody().getAStmt() and
+ mockedClassOrInterface = mockCall.getMockedType() and
+ forex(Method method | method = mockedClassOrInterface.getAMethod() and method.isPublic() |
+ exists(MockitoMockingMethodCall mockedMethod |
+ mockedMethod.getMockitoMockCall() = mockCall and
+ mockedMethod.getMethod() = method
+ )
+ )
+ )
+select testMethod, "This test method mocks all public methods of a $@.", mockedClassOrInterface,
+ "class or an interface"
diff --git a/java/ql/test/query-tests/MockingAllNonPrivateMethodsMeansUnitTestIsTooBig/Employee.java b/java/ql/test/query-tests/MockingAllNonPrivateMethodsMeansUnitTestIsTooBig/Employee.java
new file mode 100644
index 00000000000..70a0091ec37
--- /dev/null
+++ b/java/ql/test/query-tests/MockingAllNonPrivateMethodsMeansUnitTestIsTooBig/Employee.java
@@ -0,0 +1,10 @@
+/**
+ * Underlying data type of the ORM class and functions.
+ */
+public class Employee {
+ Employee(String name) {
+ this.name = name;
+ }
+
+ String name;
+}
diff --git a/java/ql/test/query-tests/MockingAllNonPrivateMethodsMeansUnitTestIsTooBig/EmployeeRecord.java b/java/ql/test/query-tests/MockingAllNonPrivateMethodsMeansUnitTestIsTooBig/EmployeeRecord.java
new file mode 100644
index 00000000000..4aa40d97ec3
--- /dev/null
+++ b/java/ql/test/query-tests/MockingAllNonPrivateMethodsMeansUnitTestIsTooBig/EmployeeRecord.java
@@ -0,0 +1,26 @@
+/**
+ * Sample ORM class for the type `Employee`.
+ */
+public class EmployeeRecord {
+ public int add(Employee employee) {
+ return 1;
+ }
+
+ public Employee get(String name) {
+ return new Employee("Sample");
+ }
+
+ public int update(Employee employee, String newName) {
+ return 1;
+ }
+
+ public int delete(Employee employee) {
+ return 1;
+ }
+
+ private void f() { }
+
+ private void g() { }
+
+ private void h() { }
+}
diff --git a/java/ql/test/query-tests/MockingAllNonPrivateMethodsMeansUnitTestIsTooBig/MockingAllNonPrivateMethodsMeansUnitTestIsTooBig.expected b/java/ql/test/query-tests/MockingAllNonPrivateMethodsMeansUnitTestIsTooBig/MockingAllNonPrivateMethodsMeansUnitTestIsTooBig.expected
new file mode 100644
index 00000000000..d3e95329380
--- /dev/null
+++ b/java/ql/test/query-tests/MockingAllNonPrivateMethodsMeansUnitTestIsTooBig/MockingAllNonPrivateMethodsMeansUnitTestIsTooBig.expected
@@ -0,0 +1,2 @@
+| TestORM.java:34:15:34:27 | nonCompliant1 | This test method mocks all public methods of a $@. | EmployeeRecord.java:4:14:4:27 | EmployeeRecord | class or an interface |
+| TestORM.java:47:15:47:27 | nonCompliant2 | This test method mocks all public methods of a $@. | EmployeeRecord.java:4:14:4:27 | EmployeeRecord | class or an interface |
diff --git a/java/ql/test/query-tests/MockingAllNonPrivateMethodsMeansUnitTestIsTooBig/MockingAllNonPrivateMethodsMeansUnitTestIsTooBig.qlref b/java/ql/test/query-tests/MockingAllNonPrivateMethodsMeansUnitTestIsTooBig/MockingAllNonPrivateMethodsMeansUnitTestIsTooBig.qlref
new file mode 100644
index 00000000000..de4f9a9055f
--- /dev/null
+++ b/java/ql/test/query-tests/MockingAllNonPrivateMethodsMeansUnitTestIsTooBig/MockingAllNonPrivateMethodsMeansUnitTestIsTooBig.qlref
@@ -0,0 +1 @@
+Likely Bugs/Frameworks/JUnit/MockingAllNonPrivateMethodsMeansUnitTestIsTooBig.ql
\ No newline at end of file
diff --git a/java/ql/test/query-tests/MockingAllNonPrivateMethodsMeansUnitTestIsTooBig/TestORM.java b/java/ql/test/query-tests/MockingAllNonPrivateMethodsMeansUnitTestIsTooBig/TestORM.java
new file mode 100644
index 00000000000..9cb0d4720c1
--- /dev/null
+++ b/java/ql/test/query-tests/MockingAllNonPrivateMethodsMeansUnitTestIsTooBig/TestORM.java
@@ -0,0 +1,55 @@
+import org.junit.Test;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.mockito.Mockito.doReturn;
+
+public class TestORM {
+ /**
+ * Test of form `when(mockedObject.methodToBeMocked()).thenReturn(someVal)`.
+ */
+ @Test
+ public void compliant1() {
+ Employee sampleEmployee = new Employee("John Doe");
+ EmployeeRecord employeeRecordMock = mock(EmployeeRecord.class); // COMPLIANT: Only some of the public methods belonging to the mocked object are used
+ when(employeeRecordMock.add(sampleEmployee)).thenReturn(0); // Mocked EmployeeRecord.add
+ when(employeeRecordMock.update(sampleEmployee, "Jane Doe")).thenReturn(0); // Mocked EmployeeRecord.update
+ }
+
+ /**
+ * Test of form `doReturn(someVal).when(mockedObject).methodToBeMocked()`.
+ */
+ @Test
+ public void compliant2() {
+ Employee sampleEmployee = new Employee("John Doe");
+ EmployeeRecord employeeRecordMock = mock(EmployeeRecord.class); // COMPLIANT: Only some of the public methods belonging to the mocked object are used
+ doReturn(0).when(employeeRecordMock).add(sampleEmployee); // Mocked EmployeeRecord.add
+ doReturn(0).when(employeeRecordMock).get("John Doe"); // Mocked EmployeeRecord.get
+ doReturn(0).when(employeeRecordMock).delete(sampleEmployee); // Mocked EmployeeRecord.delete
+ }
+
+ /**
+ * Test of form `when(mockedObject.methodToBeMocked()).thenReturn(someVal)`.
+ */
+ @Test
+ public void nonCompliant1() {
+ Employee sampleEmployee = new Employee("John Doe");
+ EmployeeRecord employeeRecordMock = mock(EmployeeRecord.class); // NON_COMPLIANT: All public methods of the mocked object are used
+ when(employeeRecordMock.add(sampleEmployee)).thenReturn(0); // Mocked EmployeeRecord.add
+ when(employeeRecordMock.get("John Doe")).thenReturn(sampleEmployee); // Mocked EmployeeRecord.get
+ when(employeeRecordMock.update(sampleEmployee, "Jane Doe")).thenReturn(0); // Mocked EmployeeRecord.update
+ when(employeeRecordMock.delete(sampleEmployee)).thenReturn(0); // Mocked EmployeeRecord.delete
+ }
+
+ /**
+ * Test of form `doReturn(someVal).when(mockedObject).methodToBeMocked()`.
+ */
+ @Test
+ public void nonCompliant2() {
+ Employee sampleEmployee = new Employee("John Doe");
+ EmployeeRecord employeeRecordMock = mock(EmployeeRecord.class); // NON_COMPLIANT: All public methods of the mocked object are used
+ doReturn(0).when(employeeRecordMock).add(sampleEmployee); // Mocked EmployeeRecord.add
+ doReturn(0).when(employeeRecordMock).get("John Doe"); // Mocked EmployeeRecord.get
+ doReturn(0).when(employeeRecordMock).update(sampleEmployee, "Jane Doe"); // Mocked EmployeeRecord.update
+ doReturn(0).when(employeeRecordMock).delete(sampleEmployee); // Mocked EmployeeRecord.delete
+ }
+}
diff --git a/java/ql/test/query-tests/MockingAllNonPrivateMethodsMeansUnitTestIsTooBig/options b/java/ql/test/query-tests/MockingAllNonPrivateMethodsMeansUnitTestIsTooBig/options
new file mode 100644
index 00000000000..8dbd14d41c1
--- /dev/null
+++ b/java/ql/test/query-tests/MockingAllNonPrivateMethodsMeansUnitTestIsTooBig/options
@@ -0,0 +1 @@
+//semmle-extractor-options: --javac-args -cp ${testdir}/../../stubs/junit-4.13:${testdir}/../../stubs/mockito-5.14
diff --git a/java/ql/test/stubs/junit-4.13/LICENSE-junit.txt b/java/ql/test/stubs/junit-4.13/LICENSE-junit.txt
new file mode 100644
index 00000000000..fb686291a05
--- /dev/null
+++ b/java/ql/test/stubs/junit-4.13/LICENSE-junit.txt
@@ -0,0 +1,214 @@
+JUnit
+
+Eclipse Public License - v 1.0
+
+THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE PUBLIC
+LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THE PROGRAM
+CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT.
+
+1. DEFINITIONS
+
+"Contribution" means:
+
+ a) in the case of the initial Contributor, the initial code and
+ documentation distributed under this Agreement, and
+ b) in the case of each subsequent Contributor:
+
+ i) changes to the Program, and
+
+ ii) additions to the Program;
+
+ where such changes and/or additions to the Program originate from and are
+distributed by that particular Contributor. A Contribution 'originates' from a
+Contributor if it was added to the Program by such Contributor itself or anyone
+acting on such Contributor's behalf. Contributions do not include additions to
+the Program which: (i) are separate modules of software distributed in
+conjunction with the Program under their own license agreement, and (ii) are
+not derivative works of the Program.
+
+"Contributor" means any person or entity that distributes the Program.
+
+"Licensed Patents " mean patent claims licensable by a Contributor which are
+necessarily infringed by the use or sale of its Contribution alone or when
+combined with the Program.
+
+"Program" means the Contributions distributed in accordance with this Agreement.
+
+"Recipient" means anyone who receives the Program under this Agreement,
+including all Contributors.
+
+2. GRANT OF RIGHTS
+
+ a) Subject to the terms of this Agreement, each Contributor hereby grants
+Recipient a non-exclusive, worldwide, royalty-free copyright license to
+reproduce, prepare derivative works of, publicly display, publicly perform,
+distribute and sublicense the Contribution of such Contributor, if any, and
+such derivative works, in source code and object code form.
+
+ b) Subject to the terms of this Agreement, each Contributor hereby grants
+Recipient a non-exclusive, worldwide, royalty-free patent license under
+Licensed Patents to make, use, sell, offer to sell, import and otherwise
+transfer the Contribution of such Contributor, if any, in source code and
+object code form. This patent license shall apply to the combination of the
+Contribution and the Program if, at the time the Contribution is added by the
+Contributor, such addition of the Contribution causes such combination to be
+covered by the Licensed Patents. The patent license shall not apply to any
+other combinations which include the Contribution. No hardware per se is
+licensed hereunder.
+
+ c) Recipient understands that although each Contributor grants the
+licenses to its Contributions set forth herein, no assurances are provided by
+any Contributor that the Program does not infringe the patent or other
+intellectual property rights of any other entity. Each Contributor disclaims
+any liability to Recipient for claims brought by any other entity based on
+infringement of intellectual property rights or otherwise. As a condition to
+exercising the rights and licenses granted hereunder, each Recipient hereby
+assumes sole responsibility to secure any other intellectual property rights
+needed, if any. For example, if a third party patent license is required to
+allow Recipient to distribute the Program, it is Recipient's responsibility to
+acquire that license before distributing the Program.
+
+ d) Each Contributor represents that to its knowledge it has sufficient
+copyright rights in its Contribution, if any, to grant the copyright license
+set forth in this Agreement.
+
+3. REQUIREMENTS
+
+A Contributor may choose to distribute the Program in object code form under
+its own license agreement, provided that:
+
+ a) it complies with the terms and conditions of this Agreement; and
+
+ b) its license agreement:
+
+ i) effectively disclaims on behalf of all Contributors all warranties and
+conditions, express and implied, including warranties or conditions of title
+and non-infringement, and implied warranties or conditions of merchantability
+and fitness for a particular purpose;
+
+ ii) effectively excludes on behalf of all Contributors all liability for
+damages, including direct, indirect, special, incidental and consequential
+damages, such as lost profits;
+
+ iii) states that any provisions which differ from this Agreement are
+offered by that Contributor alone and not by any other party; and
+
+ iv) states that source code for the Program is available from such
+Contributor, and informs licensees how to obtain it in a reasonable manner on
+or through a medium customarily used for software exchange.
+
+When the Program is made available in source code form:
+
+ a) it must be made available under this Agreement; and
+
+ b) a copy of this Agreement must be included with each copy of the
+Program.
+
+Contributors may not remove or alter any copyright notices contained within the
+Program.
+
+Each Contributor must identify itself as the originator of its Contribution, if
+any, in a manner that reasonably allows subsequent Recipients to identify the
+originator of the Contribution.
+
+4. COMMERCIAL DISTRIBUTION
+
+Commercial distributors of software may accept certain responsibilities with
+respect to end users, business partners and the like. While this license is
+intended to facilitate the commercial use of the Program, the Contributor who
+includes the Program in a commercial product offering should do so in a manner
+which does not create potential liability for other Contributors. Therefore, if
+a Contributor includes the Program in a commercial product offering, such
+Contributor ("Commercial Contributor") hereby agrees to defend and indemnify
+every other Contributor ("Indemnified Contributor") against any losses, damages
+and costs (collectively "Losses") arising from claims, lawsuits and other legal
+actions brought by a third party against the Indemnified Contributor to the
+extent caused by the acts or omissions of such Commercial Contributor in
+connection with its distribution of the Program in a commercial product
+offering. The obligations in this section do not apply to any claims or Losses
+relating to any actual or alleged intellectual property infringement. In order
+to qualify, an Indemnified Contributor must: a) promptly notify the Commercial
+Contributor in writing of such claim, and b) allow the Commercial Contributor
+to control, and cooperate with the Commercial Contributor in, the defense and
+any related settlement negotiations. The Indemnified Contributor may
+participate in any such claim at its own expense.
+
+For example, a Contributor might include the Program in a commercial product
+offering, Product X. That Contributor is then a Commercial Contributor. If that
+Commercial Contributor then makes performance claims, or offers warranties
+related to Product X, those performance claims and warranties are such
+Commercial Contributor's responsibility alone. Under this section, the
+Commercial Contributor would have to defend claims against the other
+Contributors related to those performance claims and warranties, and if a court
+requires any other Contributor to pay any damages as a result, the Commercial
+Contributor must pay those damages.
+
+5. NO WARRANTY
+
+EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS PROVIDED ON AN
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR
+IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF TITLE,
+NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Each
+Recipient is solely responsible for determining the appropriateness of using
+and distributing the Program and assumes all risks associated with its exercise
+of rights under this Agreement, including but not limited to the risks and
+costs of program errors, compliance with applicable laws, damage to or loss of
+data, programs or equipment, and unavailability or interruption of operations.
+
+6. DISCLAIMER OF LIABILITY
+
+EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR ANY
+CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION LOST
+PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY
+WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE EXERCISE OF ANY RIGHTS
+GRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
+
+7. GENERAL
+
+If any provision of this Agreement is invalid or unenforceable under applicable
+law, it shall not affect the validity or enforceability of the remainder of the
+terms of this Agreement, and without further action by the parties hereto, such
+provision shall be reformed to the minimum extent necessary to make such
+provision valid and enforceable.
+
+If Recipient institutes patent litigation against any
+entity (including a cross-claim or counterclaim in a lawsuit) alleging that the
+Program itself (excluding combinations of the Program with other software or
+hardware) infringes such Recipient's patent(s), then such Recipient's rights
+granted under Section 2(b) shall terminate as of the date such litigation is
+filed.
+
+All Recipient's rights under this Agreement shall terminate if it fails to
+comply with any of the material terms or conditions of this Agreement and does
+not cure such failure in a reasonable period of time after becoming aware of
+such noncompliance. If all Recipient's rights under this Agreement terminate,
+Recipient agrees to cease use and distribution of the Program as soon as
+reasonably practicable. However, Recipient's obligations under this Agreement
+and any licenses granted by Recipient relating to the Program shall continue
+and survive.
+
+Everyone is permitted to copy and distribute copies of this Agreement, but in
+order to avoid inconsistency the Agreement is copyrighted and may only be
+modified in the following manner. The Agreement Steward reserves the right to
+publish new versions (including revisions) of this Agreement from time to time.
+No one other than the Agreement Steward has the right to modify this Agreement.
+The Eclipse Foundation is the initial Agreement Steward. The Eclipse Foundation may assign the responsibility to
+serve as the Agreement Steward to a suitable separate entity. Each new version
+of the Agreement will be given a distinguishing version number. The Program
+(including Contributions) may always be distributed subject to the version of
+the Agreement under which it was received. In addition, after a new version of
+the Agreement is published, Contributor may elect to distribute the Program
+(including its Contributions) under the new version. Except as expressly stated
+in Sections 2(a) and 2(b) above, Recipient receives no rights or licenses to
+the intellectual property of any Contributor under this Agreement, whether
+expressly, by implication, estoppel or otherwise. All rights in the Program not
+expressly granted under this Agreement are reserved.
+
+This Agreement is governed by the laws of the State of New York and the
+intellectual property laws of the United States of America. No party to this
+Agreement will bring a legal action under this Agreement more than one year
+after the cause of action arose. Each party waives its rights to a jury trial
+in any resulting litigation.
+
diff --git a/java/ql/test/stubs/junit-4.13/org/junit/Assert.java b/java/ql/test/stubs/junit-4.13/org/junit/Assert.java
new file mode 100644
index 00000000000..cbd86acdb81
--- /dev/null
+++ b/java/ql/test/stubs/junit-4.13/org/junit/Assert.java
@@ -0,0 +1,472 @@
+package org.junit;
+
+import org.junit.function.ThrowingRunnable;
+
+//BSD License
+//
+//Copyright (c) 2000-2006, www.hamcrest.org
+//All rights reserved.
+//
+//Redistribution and use in source and binary forms, with or without
+//modification, are permitted provided that the following conditions are met:
+//
+//Redistributions of source code must retain the above copyright notice, this list of
+//conditions and the following disclaimer. Redistributions in binary form must reproduce
+//the above copyright notice, this list of conditions and the following disclaimer in
+//the documentation and/or other materials provided with the distribution.
+//
+//Neither the name of Hamcrest nor the names of its contributors may be used to endorse
+//or promote products derived from this software without specific prior written
+//permission.
+//
+//THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
+//EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+//OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT
+//SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+//INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
+//TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
+//BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+//CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY
+//WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
+//DAMAGE.
+
+/*
+ * MODIFIED version of JUnit 4.13 as available at
+ * https://search.maven.org/remotecontent?filepath=junit/junit/4.13/junit-4.13-sources.jar
+ * Only parts of this file have been retained for test purposes.
+ */
+
+public class Assert {
+ /**
+ * Asserts that a condition is true. If it isn't it throws an
+ * {@link AssertionError} with the given message.
+ *
+ * @param message the identifying message for the {@link AssertionError}
+ * (null
+ * okay)
+ * @param condition condition to be checked
+ */
+ static public void assertTrue(String message, boolean condition) {
+ return;
+ }
+
+ /**
+ * Asserts that a condition is true. If it isn't it throws an
+ * {@link AssertionError} without a message.
+ *
+ * @param condition condition to be checked
+ */
+ static public void assertTrue(boolean condition) {
+ return;
+ }
+
+ /**
+ * Asserts that a condition is false. If it isn't it throws an
+ * {@link AssertionError} with the given message.
+ *
+ * @param message the identifying message for the {@link AssertionError}
+ * (null
+ * okay)
+ * @param condition condition to be checked
+ */
+ static public void assertFalse(String message, boolean condition) {
+ return;
+ }
+
+ /**
+ * Asserts that a condition is false. If it isn't it throws an
+ * {@link AssertionError} without a message.
+ *
+ * @param condition condition to be checked
+ */
+ static public void assertFalse(boolean condition) {
+ return;
+ }
+
+ /**
+ * Fails a test with the given message.
+ *
+ * @param message the identifying message for the {@link AssertionError}
+ * (null
+ * okay)
+ * @see AssertionError
+ */
+ static public void fail(String message) {
+ if (message == null) {
+ throw new AssertionError();
+ }
+ throw new AssertionError(message);
+ }
+
+ /**
+ * Asserts that an object isn't null. If it is an {@link AssertionError} is
+ * thrown with the given message.
+ *
+ * @param message the identifying message for the {@link AssertionError}
+ * (null
+ * okay)
+ * @param object Object to check or null
+ */
+ static public void assertNotNull(String message, Object object) {
+ return;
+ }
+
+ /**
+ * Asserts that an object isn't null. If it is an {@link AssertionError} is
+ * thrown.
+ *
+ * @param object Object to check or null
+ */
+ static public void assertNotNull(Object object) {
+ return;
+ }
+
+ /**
+ * Asserts that an object is null. If it is not, an {@link AssertionError}
+ * is thrown with the given message.
+ *
+ * @param message the identifying message for the {@link AssertionError}
+ * (null
+ * okay)
+ * @param object Object to check or null
+ */
+ static public void assertNull(String message, Object object) {
+ return;
+ }
+
+ /**
+ * Asserts that an object is null. If it isn't an {@link AssertionError} is
+ * thrown.
+ *
+ * @param object Object to check or null
+ */
+ static public void assertNull(Object object) {
+ return;
+ }
+
+ private static boolean equalsRegardingNull(Object expected, Object actual) {
+ if (expected == null) {
+ return actual == null;
+ }
+
+ return isEquals(expected, actual);
+ }
+
+ private static boolean isEquals(Object expected, Object actual) {
+ return expected.equals(actual);
+ }
+
+ /**
+ * Asserts that two doubles are equal to within a positive delta.
+ * If they are not, an {@link AssertionError} is thrown with the given
+ * message. If the expected value is infinity then the delta value is
+ * ignored. NaNs are considered equal:
+ * assertEquals(Double.NaN, Double.NaN, *) passes
+ *
+ * @param message the identifying message for the {@link AssertionError}
+ * (null
+ * okay)
+ * @param expected expected value
+ * @param actual the value to check against expected
+ * @param delta the maximum delta between expected and
+ * actual for which both numbers are still
+ * considered equal.
+ */
+ public static void assertEquals(String message, double expected,
+ double actual, double delta) {
+ return;
+ }
+
+ private static void failNotEquals(String message, Object expected,
+ Object actual) {
+ fail(format(message, expected, actual));
+ }
+
+ static String format(String message, Object expected, Object actual) {
+ String formatted = "";
+ if (message != null && !"".equals(message)) {
+ formatted = message + " ";
+ }
+ String expectedString = String.valueOf(expected);
+ String actualString = String.valueOf(actual);
+ if (equalsRegardingNull(expectedString, actualString)) {
+ return formatted + "expected: "
+ + formatClassAndValue(expected, expectedString)
+ + " but was: " + formatClassAndValue(actual, actualString);
+ } else {
+ return formatted + "expected:<" + expectedString + "> but was:<"
+ + actualString + ">";
+ }
+ }
+
+ private static String formatClass(Class> value) {
+ String className = value.getCanonicalName();
+ return className == null ? value.getName() : className;
+ }
+
+ private static String formatClassAndValue(Object value, String valueString) {
+ String className = value == null ? "null" : value.getClass().getName();
+ return className + "<" + valueString + ">";
+ }
+
+ /**
+ * Asserts that two floats are equal to within a positive delta.
+ * If they are not, an {@link AssertionError} is thrown with the given
+ * message. If the expected value is infinity then the delta value is
+ * ignored. NaNs are considered equal:
+ * assertEquals(Float.NaN, Float.NaN, *) passes
+ *
+ * @param message the identifying message for the {@link AssertionError}
+ * (null
+ * okay)
+ * @param expected expected value
+ * @param actual the value to check against expected
+ * @param delta the maximum delta between expected and
+ * actual for which both numbers are still
+ * considered equal.
+ */
+ public static void assertEquals(String message, float expected, float actual,
+ float delta) {
+ if (floatIsDifferent(expected, actual, delta)) {
+ failNotEquals(message, Float.valueOf(expected), Float.valueOf(actual));
+ }
+ }
+
+ private static boolean doubleIsDifferent(double d1, double d2, double delta) {
+ if (Double.compare(d1, d2) == 0) {
+ return false;
+ }
+ if ((Math.abs(d1 - d2) <= delta)) {
+ return false;
+ }
+
+ return true;
+ }
+
+ private static boolean floatIsDifferent(float f1, float f2, float delta) {
+ if (Float.compare(f1, f2) == 0) {
+ return false;
+ }
+ if ((Math.abs(f1 - f2) <= delta)) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Asserts that two longs are equal. If they are not, an
+ * {@link AssertionError} is thrown.
+ *
+ * @param expected expected long value.
+ * @param actual actual long value
+ */
+ public static void assertEquals(long expected, long actual) {
+ assertEquals(null, expected, actual);
+ }
+
+ /**
+ * Asserts that two longs are equal. If they are not, an
+ * {@link AssertionError} is thrown with the given message.
+ *
+ * @param message the identifying message for the {@link AssertionError}
+ * (null
+ * okay)
+ * @param expected long expected value.
+ * @param actual long actual value
+ */
+ public static void assertEquals(String message, long expected, long actual) {
+ if (expected != actual) {
+ failNotEquals(message, Long.valueOf(expected), Long.valueOf(actual));
+ }
+ }
+
+ /**
+ * @deprecated Use
+ * assertEquals(double expected, double actual, double
+ * delta) instead
+ */
+ @Deprecated
+ public static void assertEquals(double expected, double actual) {
+ assertEquals(null, expected, actual);
+ }
+
+ /**
+ * @deprecated Use
+ * assertEquals(String message, double expected, double
+ * actual, double delta) instead
+ */
+ @Deprecated
+ public static void assertEquals(String message, double expected,
+ double actual) {
+ fail("Use assertEquals(expected, actual, delta) to compare " +
+ "floating-point numbers");
+ }
+
+ /**
+ * Asserts that two doubles are equal to within a positive delta.
+ * If they are not, an {@link AssertionError} is thrown. If the expected
+ * value is infinity then the delta value is ignored.NaNs are considered
+ * equal: assertEquals(Double.NaN, Double.NaN, *) passes
+ *
+ * @param expected expected value
+ * @param actual the value to check against expected
+ * @param delta the maximum delta between expected and
+ * actual for which both numbers are still
+ * considered equal.
+ */
+ public static void assertEquals(double expected, double actual,
+ double delta) {
+ assertEquals(null, expected, actual, delta);
+ }
+
+ /**
+ * Asserts that two floats are equal to within a positive delta.
+ * If they are not, an {@link AssertionError} is thrown. If the expected
+ * value is infinity then the delta value is ignored. NaNs are considered
+ * equal: assertEquals(Float.NaN, Float.NaN, *) passes
+ *
+ * @param expected expected value
+ * @param actual the value to check against expected
+ * @param delta the maximum delta between expected and
+ * actual for which both numbers are still
+ * considered equal.
+ */
+ public static void assertEquals(float expected, float actual, float delta) {
+ assertEquals(null, expected, actual, delta);
+ }
+
+ /**
+ * Asserts that two objects are equal. If they are not, an
+ * {@link AssertionError} without a message is thrown. If
+ * expected and actual are null,
+ * they are considered equal.
+ *
+ * @param expected expected value
+ * @param actual the value to check against expected
+ */
+ public static void assertEquals(Object expected, Object actual) {
+ assertEquals(null, expected, actual);
+ }
+
+ public static void assertEquals(String message, Object expected,
+ Object actual) {
+ }
+
+ public static void assertNotEquals(String message, Object unexpected, Object actual) {
+ return;
+ }
+
+ public static void assertNotEquals(Object unexpected, Object actual) {
+ assertNotEquals(null, unexpected, actual);
+ }
+
+ public static void assertNotEquals(String message, long unexpected, long actual) {
+ return;
+ }
+
+ public static void assertNotEquals(long unexpected, long actual) {
+ assertNotEquals(null, unexpected, actual);
+ }
+
+ public static void assertNotEquals(String message, double unexpected, double actual, double delta) {
+ return;
+ }
+
+ public static void assertNotEquals(double unexpected, double actual, double delta) {
+ assertNotEquals(null, unexpected, actual, delta);
+ }
+
+ public static void assertNotEquals(String message, float unexpected, float actual, float delta) {
+ return;
+ }
+
+ public static void assertNotEquals(float unexpected, float actual, float delta) {
+ assertNotEquals(null, unexpected, actual, delta);
+ }
+
+ public static void assertNotSame(String message, Object unexpected, Object actual) {
+ return;
+ }
+
+ public static void assertNotSame(Object unexpected, Object actual) {
+ assertNotSame(null, unexpected, actual);
+ }
+
+ public static void assertSame(String message, Object expected, Object actual) {
+ return;
+ }
+
+ public static void assertSame(Object expected, Object actual) {
+ assertSame(null, expected, actual);
+ }
+
+ /**
+ * Asserts that {@code runnable} throws an exception of type {@code expectedThrowable} when
+ * executed. If it does, the exception object is returned. If it does not throw an exception, an
+ * {@link AssertionError} is thrown. If it throws the wrong type of exception, an {@code
+ * AssertionError} is thrown describing the mismatch; the exception that was actually thrown can
+ * be obtained by calling {@link AssertionError#getCause}.
+ *
+ * @param expectedThrowable the expected type of the exception
+ * @param runnable a function that is expected to throw an exception when executed
+ * @return the exception thrown by {@code runnable}
+ * @since 4.13
+ */
+ public static T assertThrows(Class expectedThrowable,
+ ThrowingRunnable runnable) {
+ return assertThrows(null, expectedThrowable, runnable);
+ }
+
+ /**
+ * Asserts that {@code runnable} throws an exception of type {@code expectedThrowable} when
+ * executed. If it does, the exception object is returned. If it does not throw an exception, an
+ * {@link AssertionError} is thrown. If it throws the wrong type of exception, an {@code
+ * AssertionError} is thrown describing the mismatch; the exception that was actually thrown can
+ * be obtained by calling {@link AssertionError#getCause}.
+ *
+ * @param message the identifying message for the {@link AssertionError} (null
+ * okay)
+ * @param expectedThrowable the expected type of the exception
+ * @param runnable a function that is expected to throw an exception when executed
+ * @return the exception thrown by {@code runnable}
+ * @since 4.13
+ */
+ public static T assertThrows(String message, Class expectedThrowable,
+ ThrowingRunnable runnable) {
+ try {
+ runnable.run();
+ } catch (Throwable actualThrown) {
+ if (expectedThrowable.isInstance(actualThrown)) {
+ @SuppressWarnings("unchecked") T retVal = (T) actualThrown;
+ return retVal;
+ } else {
+ String expected = formatClass(expectedThrowable);
+ Class extends Throwable> actualThrowable = actualThrown.getClass();
+ String actual = formatClass(actualThrowable);
+ if (expected.equals(actual)) {
+ // There must be multiple class loaders. Add the identity hash code so the message
+ // doesn't say "expected: java.lang.String ..."
+ expected += "@" + Integer.toHexString(System.identityHashCode(expectedThrowable));
+ actual += "@" + Integer.toHexString(System.identityHashCode(actualThrowable));
+ }
+ String mismatchMessage = buildPrefix(message)
+ + format("unexpected exception type thrown;", expected, actual);
+
+ // The AssertionError(String, Throwable) ctor is only available on JDK7.
+ AssertionError assertionError = new AssertionError(mismatchMessage);
+ assertionError.initCause(actualThrown);
+ throw assertionError;
+ }
+ }
+ String notThrownMessage = buildPrefix(message) + String
+ .format("expected %s to be thrown, but nothing was thrown",
+ formatClass(expectedThrowable));
+ throw new AssertionError(notThrownMessage);
+ }
+
+ private static String buildPrefix(String message) {
+ return message != null && message.length() != 0 ? message + ": " : "";
+ }
+
+}
diff --git a/java/ql/test/stubs/junit-4.13/org/junit/Test.java b/java/ql/test/stubs/junit-4.13/org/junit/Test.java
new file mode 100644
index 00000000000..8356b546d79
--- /dev/null
+++ b/java/ql/test/stubs/junit-4.13/org/junit/Test.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2015-2018 the original author or authors.
+ *
+ * All rights reserved. This program and the accompanying materials are
+ * made available under the terms of the Eclipse Public License v2.0 which
+ * accompanies this distribution and is available at
+ *
+ * http://www.eclipse.org/legal/epl-v20.html
+ */
+
+/*
+ * MODIFIED version of junit-jupiter-api 5.2.0 as available at
+ * https://search.maven.org/classic/remotecontent?filepath=org/junit/jupiter/junit-jupiter-api/5.2.0/junit-jupiter-api-5.2.0-sources.jar
+ * Only parts of this file have been retained for test purposes.
+ */
+
+package org.junit;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Target({ElementType.ANNOTATION_TYPE, ElementType.METHOD})
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface Test {}
diff --git a/java/ql/test/stubs/junit-4.13/org/junit/function/ThrowingRunnable.java b/java/ql/test/stubs/junit-4.13/org/junit/function/ThrowingRunnable.java
new file mode 100644
index 00000000000..d0eb782ccd3
--- /dev/null
+++ b/java/ql/test/stubs/junit-4.13/org/junit/function/ThrowingRunnable.java
@@ -0,0 +1,14 @@
+package org.junit.function;
+
+/**
+ * This interface facilitates the use of
+ * {@link org.junit.Assert#assertThrows(Class, ThrowingRunnable)} from Java 8. It allows method
+ * references to void methods (that declare checked exceptions) to be passed directly into
+ * {@code assertThrows}
+ * without wrapping. It is not meant to be implemented directly.
+ *
+ * @since 4.13
+ */
+public interface ThrowingRunnable {
+ void run() throws Throwable;
+}
diff --git a/java/ql/test/stubs/mockito-5.14/org/mockito/ArgumentMatchers.java b/java/ql/test/stubs/mockito-5.14/org/mockito/ArgumentMatchers.java
new file mode 100644
index 00000000000..84004de2ac3
--- /dev/null
+++ b/java/ql/test/stubs/mockito-5.14/org/mockito/ArgumentMatchers.java
@@ -0,0 +1,4 @@
+package org.mockito;
+
+public class ArgumentMatchers {
+}
diff --git a/java/ql/test/stubs/mockito-5.14/org/mockito/MockSettings.java b/java/ql/test/stubs/mockito-5.14/org/mockito/MockSettings.java
new file mode 100644
index 00000000000..f6e46d610e7
--- /dev/null
+++ b/java/ql/test/stubs/mockito-5.14/org/mockito/MockSettings.java
@@ -0,0 +1,10 @@
+/*
+ * Copyright (c) 2007 Mockito contributors
+ * This program is made available under the terms of the MIT License.
+ */
+package org.mockito;
+
+import java.io.Serializable;
+
+public interface MockSettings extends Serializable {
+}
\ No newline at end of file
diff --git a/java/ql/test/stubs/mockito-5.14/org/mockito/Mockito.java b/java/ql/test/stubs/mockito-5.14/org/mockito/Mockito.java
new file mode 100644
index 00000000000..e4d5a06a247
--- /dev/null
+++ b/java/ql/test/stubs/mockito-5.14/org/mockito/Mockito.java
@@ -0,0 +1,216 @@
+/*
+ * Copyright (c) 2007 Mockito contributors
+ * This program is made available under the terms of the MIT License.
+ */
+package org.mockito;
+
+import org.mockito.ArgumentMatchers;
+import org.mockito.MockSettings;
+import org.mockito.internal.creation.MockSettingsImpl;
+import org.mockito.stubbing.Answer;
+import org.mockito.stubbing.OngoingStubbing;
+import org.mockito.internal.MockitoCore;
+import org.mockito.MockSettings;
+import org.mockito.stubbing.Stubber;
+
+public class Mockito extends ArgumentMatchers {
+ static final MockitoCore MOCKITO_CORE = new MockitoCore();
+
+ public static MockSettings withSettings() {
+ return new MockSettings() {
+ };
+ }
+
+ /**
+ * Creates a mock object of the requested class or interface.
+ *
+ * See examples in javadoc for the {@link Mockito} class.
+ *
+ * @param reified don't pass any values to it. It's a trick to detect the
+ * class/interface you
+ * want to mock.
+ * @return the mock object.
+ * @since 4.10.0
+ */
+ @SafeVarargs
+ public static T mock(T... reified) {
+ return mock(withSettings(), reified);
+ }
+
+ /**
+ * Creates a mock object of the requested class or interface with the given
+ * default answer.
+ *
+ * See examples in javadoc for the {@link Mockito} class.
+ *
+ * @param defaultAnswer the default answer to use.
+ * @param reified don't pass any values to it. It's a trick to detect the
+ * class/interface you
+ * want to mock.
+ * @return the mock object.
+ * @since 5.1.0
+ */
+ @SafeVarargs
+ public static T mock(@SuppressWarnings("rawtypes") Answer defaultAnswer, T... reified) {
+ return mock(new Answer() {
+ }, reified);
+ }
+
+ /**
+ * Creates a mock object of the requested class or interface with the given
+ * name.
+ *
+ * See examples in javadoc for the {@link Mockito} class.
+ *
+ * @param name the mock name to use.
+ * @param reified don't pass any values to it. It's a trick to detect the
+ * class/interface you
+ * want to mock.
+ * @return the mock object.
+ * @since 5.1.0
+ */
+ @SafeVarargs
+ public static T mock(String name, T... reified) {
+ return mock(withSettings(), reified);
+ }
+
+ /**
+ * Creates a mock object of the requested class or interface with the given
+ * settings.
+ *
+ * See examples in javadoc for the {@link Mockito} class.
+ *
+ * @param settings the mock settings to use.
+ * @param reified don't pass any values to it. It's a trick to detect the
+ * class/interface you
+ * want to mock.
+ * @return the mock object.
+ * @since 5.1.0
+ */
+ @SafeVarargs
+ public static T mock(MockSettings settings, T... reified) {
+ if (reified == null || reified.length > 0) {
+ throw new IllegalArgumentException(
+ "Please don't pass any values here. Java will detect class automagically.");
+ }
+
+ return mock(getClassOf(reified), settings);
+ }
+
+ /**
+ * Creates mock object of given class or interface.
+ *
+ * See examples in javadoc for {@link Mockito} class
+ *
+ * @param classToMock class or interface to mock
+ * @return mock object
+ */
+ public static T mock(Class classToMock) {
+ return mock(classToMock, withSettings());
+ }
+
+ /**
+ * Specifies mock name. Naming mocks can be helpful for debugging - the name is
+ * used in all verification errors.
+ *
+ * Beware that naming mocks is not a solution for complex code which uses too
+ * many mocks or collaborators.
+ * If you have too many mocks then refactor the code so that it's easy to
+ * test/debug without necessity of naming mocks.
+ *
+ * If you use @Mock annotation then you've got naming mocks
+ * for free!@Mock uses field name as mock name.
+ * {@link Mock Read more.}
+ *
+ *
+ * See examples in javadoc for {@link Mockito} class
+ *
+ * @param classToMock class or interface to mock
+ * @param name of the mock
+ * @return mock object
+ */
+ public static T mock(Class classToMock, String name) {
+ return mock(classToMock, new Answer() {
+ });
+ }
+
+ /**
+ * Creates mock with a specified strategy for its answers to interactions.
+ * It's quite an advanced feature and typically you don't need it to write
+ * decent tests.
+ * However it can be helpful when working with legacy systems.
+ *
+ * It is the default answer so it will be used only when you don't stub
+ * the method call.
+ *
+ *
+ * See examples in javadoc for {@link Mockito} class
+ *
+ *
+ * @param classToMock class or interface to mock
+ * @param defaultAnswer default answer for un-stubbed methods
+ *
+ * @return mock object
+ */
+ public static T mock(Class classToMock, Answer defaultAnswer) {
+ return mock(classToMock, new Answer() {
+ });
+ }
+
+ /**
+ * Creates a mock with some non-standard settings.
+ *
+ * The number of configuration points for a mock will grow,
+ * so we need a fluent way to introduce new configuration without adding more
+ * and more overloaded Mockito.mock() methods.
+ * Hence {@link MockSettings}.
+ *
+ *
+ *
+ * Use it carefully and occasionally. What might be reason your test
+ * needs non-standard mocks?
+ * Is the code under test so complicated that it requires non-standard mocks?
+ * Wouldn't you prefer to refactor the code under test, so that it is testable
+ * in a simple way?
+ *
User input should be suitably sanitized before it is logged.
-If the log entries are in plain text then line breaks should be removed from user input, using
+If the log entries are in plain text, then line breaks should be removed from user input using
String::replace or similar. Care should also be taken that user input is clearly marked
in log entries.
From d8215a35c0268367775c371fe00b1ca0e0cca271 Mon Sep 17 00:00:00 2001
From: Michael Nebel
Date: Fri, 18 Jul 2025 13:01:05 +0200
Subject: [PATCH 118/984] C#: Add example of failing taint flow for collections
in sinks.
---
.../collections/CollectionTaintTracking.cs | 13 +++++++++++++
.../collections/CollectionTaintTracking.expected | 7 +++++++
.../collections/CollectionTaintTracking.ql | 12 ++++++++++++
.../library-tests/tainttracking/collections/options | 2 ++
4 files changed, 34 insertions(+)
create mode 100644 csharp/ql/test/library-tests/tainttracking/collections/CollectionTaintTracking.cs
create mode 100644 csharp/ql/test/library-tests/tainttracking/collections/CollectionTaintTracking.expected
create mode 100644 csharp/ql/test/library-tests/tainttracking/collections/CollectionTaintTracking.ql
create mode 100644 csharp/ql/test/library-tests/tainttracking/collections/options
diff --git a/csharp/ql/test/library-tests/tainttracking/collections/CollectionTaintTracking.cs b/csharp/ql/test/library-tests/tainttracking/collections/CollectionTaintTracking.cs
new file mode 100644
index 00000000000..d4177a57661
--- /dev/null
+++ b/csharp/ql/test/library-tests/tainttracking/collections/CollectionTaintTracking.cs
@@ -0,0 +1,13 @@
+public class CollectionTaintTracking
+{
+ public void ImplicitCollectionReadAtSink()
+ {
+ var tainted = Source
-
In this example the except Exception: will handle AttributeError preventing the
+
In the following example, the except Exception: will handle AttributeError preventing the
subsequent handler from ever executing.
@@ -37,8 +37,8 @@ subsequent handler from ever executing.
-
diff --git a/python/ql/src/Exceptions/IncorrectExceptOrder.ql b/python/ql/src/Exceptions/IncorrectExceptOrder.ql
index 3c0c90b36d3..6838a9bc2ae 100644
--- a/python/ql/src/Exceptions/IncorrectExceptOrder.ql
+++ b/python/ql/src/Exceptions/IncorrectExceptOrder.ql
@@ -14,22 +14,27 @@
*/
import python
+import semmle.python.dataflow.new.internal.DataFlowDispatch
-predicate incorrect_except_order(ExceptStmt ex1, ClassValue cls1, ExceptStmt ex2, ClassValue cls2) {
+predicate incorrectExceptOrder(ExceptStmt ex1, Class cls1, ExceptStmt ex2, Class cls2) {
exists(int i, int j, Try t |
ex1 = t.getHandler(i) and
ex2 = t.getHandler(j) and
i < j and
- cls1 = except_class(ex1) and
- cls2 = except_class(ex2) and
- cls1 = cls2.getASuperType()
+ cls1 = exceptClass(ex1) and
+ cls2 = exceptClass(ex2) and
+ cls1 = getADirectSuperclass*(cls2)
)
}
-ClassValue except_class(ExceptStmt ex) { ex.getType().pointsTo(result) }
+Class exceptClass(ExceptStmt ex) { ex.getType() = classTracker(result).asExpr() }
-from ExceptStmt ex1, ClassValue cls1, ExceptStmt ex2, ClassValue cls2
-where incorrect_except_order(ex1, cls1, ex2, cls2)
-select ex2,
- "Except block for $@ is unreachable; the more general $@ for $@ will always be executed in preference.",
- cls2, cls2.getName(), ex1, "except block", cls1, cls1.getName()
+from ExceptStmt ex1, Class cls1, ExceptStmt ex2, Class cls2, string msg
+where
+ incorrectExceptOrder(ex1, cls1, ex2, cls2) and
+ if cls1 = cls2
+ then msg = "This except block handling $@ is unreachable; as $@ also handles $@."
+ else
+ msg =
+ "This except block handling $@ is unreachable; as $@ for the more general $@ always subsumes it."
+select ex2, msg, cls2, cls2.getName(), ex1, "this except block", cls1, cls1.getName()
From 49ef6939d41ebdb5e6875ca33354c9f7442f6638 Mon Sep 17 00:00:00 2001
From: Kristen Newbury
Date: Tue, 19 Aug 2025 14:49:31 -0400
Subject: [PATCH 155/984] Add extra Customizations files
---
cpp/ql/lib/Customizations.qll | 12 ++++++++++++
cpp/ql/lib/cpp.qll | 1 +
rust/ql/lib/Customizations.qll | 12 ++++++++++++
rust/ql/lib/rust.qll | 1 +
swift/ql/lib/Customizations.qll | 12 ++++++++++++
swift/ql/lib/swift.qll | 1 +
6 files changed, 39 insertions(+)
create mode 100644 cpp/ql/lib/Customizations.qll
create mode 100644 rust/ql/lib/Customizations.qll
create mode 100644 swift/ql/lib/Customizations.qll
diff --git a/cpp/ql/lib/Customizations.qll b/cpp/ql/lib/Customizations.qll
new file mode 100644
index 00000000000..76a4355b69c
--- /dev/null
+++ b/cpp/ql/lib/Customizations.qll
@@ -0,0 +1,12 @@
+/**
+ * Contains customizations to the standard library.
+ *
+ * This module is imported by `cpp.qll`, so any customizations defined here automatically
+ * apply to all queries.
+ *
+ * Typical examples of customizations include adding new subclasses of abstract classes such as
+ * the `RemoteFlowSource` class
+ * to model frameworks that are not covered by the standard library.
+ */
+
+import cpp
diff --git a/cpp/ql/lib/cpp.qll b/cpp/ql/lib/cpp.qll
index ccd32c368e4..c8afac1c7ae 100644
--- a/cpp/ql/lib/cpp.qll
+++ b/cpp/ql/lib/cpp.qll
@@ -13,6 +13,7 @@
* https://github.com/cplusplus/draft/raw/master/papers/n4140.pdf
*/
+import Customizations
import semmle.code.cpp.File
import semmle.code.cpp.Linkage
import semmle.code.cpp.Location
diff --git a/rust/ql/lib/Customizations.qll b/rust/ql/lib/Customizations.qll
new file mode 100644
index 00000000000..4f67cb91049
--- /dev/null
+++ b/rust/ql/lib/Customizations.qll
@@ -0,0 +1,12 @@
+/**
+ * Contains customizations to the standard library.
+ *
+ * This module is imported by `rust.qll`, so any customizations defined here automatically
+ * apply to all queries.
+ *
+ * Typical examples of customizations include adding new subclasses of abstract classes such as
+ * the `RemoteFlowSource` class
+ * to model frameworks that are not covered by the standard library.
+ */
+
+import rust
diff --git a/rust/ql/lib/rust.qll b/rust/ql/lib/rust.qll
index e7d02adea32..b46e96868f6 100644
--- a/rust/ql/lib/rust.qll
+++ b/rust/ql/lib/rust.qll
@@ -1,5 +1,6 @@
/** Top-level import for the Rust language pack */
+import Customizations
import codeql.rust.elements
import codeql.Locations
import codeql.files.FileSystem
diff --git a/swift/ql/lib/Customizations.qll b/swift/ql/lib/Customizations.qll
new file mode 100644
index 00000000000..71684ba1f75
--- /dev/null
+++ b/swift/ql/lib/Customizations.qll
@@ -0,0 +1,12 @@
+/**
+ * Contains customizations to the standard library.
+ *
+ * This module is imported by `swift.qll`, so any customizations defined here automatically
+ * apply to all queries.
+ *
+ * Typical examples of customizations include adding new subclasses of abstract classes such as
+ * the `RemoteFlowSource` class
+ * to model frameworks that are not covered by the standard library.
+ */
+
+import swift
diff --git a/swift/ql/lib/swift.qll b/swift/ql/lib/swift.qll
index 901d9e895e0..54f2abf9092 100644
--- a/swift/ql/lib/swift.qll
+++ b/swift/ql/lib/swift.qll
@@ -1,5 +1,6 @@
/** Top-level import for the Swift language pack */
+import Customizations
import codeql.swift.elements
import codeql.swift.elements.expr.ArithmeticOperation
import codeql.swift.elements.expr.Assignment
From d630e32ce9f714674a99a3cbb881157f4ac37216 Mon Sep 17 00:00:00 2001
From: Kristen Newbury
Date: Tue, 19 Aug 2025 15:27:29 -0400
Subject: [PATCH 156/984] Format Customizations.qll
---
cpp/ql/lib/Customizations.qll | 2 +-
rust/ql/lib/Customizations.qll | 2 +-
swift/ql/lib/Customizations.qll | 2 +-
3 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/cpp/ql/lib/Customizations.qll b/cpp/ql/lib/Customizations.qll
index 76a4355b69c..88e83346ef1 100644
--- a/cpp/ql/lib/Customizations.qll
+++ b/cpp/ql/lib/Customizations.qll
@@ -8,5 +8,5 @@
* the `RemoteFlowSource` class
* to model frameworks that are not covered by the standard library.
*/
-
+
import cpp
diff --git a/rust/ql/lib/Customizations.qll b/rust/ql/lib/Customizations.qll
index 4f67cb91049..822792df6c7 100644
--- a/rust/ql/lib/Customizations.qll
+++ b/rust/ql/lib/Customizations.qll
@@ -8,5 +8,5 @@
* the `RemoteFlowSource` class
* to model frameworks that are not covered by the standard library.
*/
-
+
import rust
diff --git a/swift/ql/lib/Customizations.qll b/swift/ql/lib/Customizations.qll
index 71684ba1f75..001628f2110 100644
--- a/swift/ql/lib/Customizations.qll
+++ b/swift/ql/lib/Customizations.qll
@@ -8,5 +8,5 @@
* the `RemoteFlowSource` class
* to model frameworks that are not covered by the standard library.
*/
-
+
import swift
From c9f0e3a37744c1868781d915590d62970d9d9e03 Mon Sep 17 00:00:00 2001
From: Jeroen Ketema <93738568+jketema@users.noreply.github.com>
Date: Wed, 20 Aug 2025 08:07:10 +0200
Subject: [PATCH 157/984] Apply suggestions from code review
---
cpp/ql/lib/Customizations.qll | 3 +--
rust/ql/lib/Customizations.qll | 3 +--
swift/ql/lib/Customizations.qll | 3 +--
3 files changed, 3 insertions(+), 6 deletions(-)
diff --git a/cpp/ql/lib/Customizations.qll b/cpp/ql/lib/Customizations.qll
index 88e83346ef1..c9d899e07e0 100644
--- a/cpp/ql/lib/Customizations.qll
+++ b/cpp/ql/lib/Customizations.qll
@@ -5,8 +5,7 @@
* apply to all queries.
*
* Typical examples of customizations include adding new subclasses of abstract classes such as
- * the `RemoteFlowSource` class
- * to model frameworks that are not covered by the standard library.
+ * the `RemoteFlowSource` class to model frameworks that are not covered by the standard library.
*/
import cpp
diff --git a/rust/ql/lib/Customizations.qll b/rust/ql/lib/Customizations.qll
index 822792df6c7..8fc6bbea911 100644
--- a/rust/ql/lib/Customizations.qll
+++ b/rust/ql/lib/Customizations.qll
@@ -5,8 +5,7 @@
* apply to all queries.
*
* Typical examples of customizations include adding new subclasses of abstract classes such as
- * the `RemoteFlowSource` class
- * to model frameworks that are not covered by the standard library.
+ * the `RemoteFlowSource` class to model frameworks that are not covered by the standard library.
*/
import rust
diff --git a/swift/ql/lib/Customizations.qll b/swift/ql/lib/Customizations.qll
index 001628f2110..bf9e66de70b 100644
--- a/swift/ql/lib/Customizations.qll
+++ b/swift/ql/lib/Customizations.qll
@@ -5,8 +5,7 @@
* apply to all queries.
*
* Typical examples of customizations include adding new subclasses of abstract classes such as
- * the `RemoteFlowSource` class
- * to model frameworks that are not covered by the standard library.
+ * the `RemoteFlowSource` class to model frameworks that are not covered by the standard library.
*/
import swift
From b42c366250be3a2d929e8a78dfc003a8bbf4a942 Mon Sep 17 00:00:00 2001
From: Michael Nebel
Date: Wed, 20 Aug 2025 08:50:23 +0200
Subject: [PATCH 158/984] C#: Address review comments.
---
.../code/csharp/dataflow/internal/TaintTrackingPrivate.qll | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/csharp/ql/lib/semmle/code/csharp/dataflow/internal/TaintTrackingPrivate.qll b/csharp/ql/lib/semmle/code/csharp/dataflow/internal/TaintTrackingPrivate.qll
index 908877c359b..dbfda21c09c 100644
--- a/csharp/ql/lib/semmle/code/csharp/dataflow/internal/TaintTrackingPrivate.qll
+++ b/csharp/ql/lib/semmle/code/csharp/dataflow/internal/TaintTrackingPrivate.qll
@@ -31,8 +31,7 @@ predicate defaultTaintSanitizer(DataFlow::Node node) {
*/
bindingset[node]
predicate defaultImplicitTaintRead(DataFlow::Node node, DataFlow::ContentSet c) {
- node instanceof ArgumentNode and
- Collections::isCollectionType(node.getType()) and
+ exists(node) and
c.isElement()
}
From 70d3e69ce5f1c7d3b819b8c60a39923ddd0d7366 Mon Sep 17 00:00:00 2001
From: Mathias Vorreiter Pedersen
Date: Wed, 20 Aug 2025 10:38:22 +0200
Subject: [PATCH 159/984] C++: Rename 'lambda' to 'virtual'.
---
.../ir/dataflow/internal/DataFlowDispatch.qll | 40 ++++++++++---------
1 file changed, 21 insertions(+), 19 deletions(-)
diff --git a/cpp/ql/lib/semmle/code/cpp/ir/dataflow/internal/DataFlowDispatch.qll b/cpp/ql/lib/semmle/code/cpp/ir/dataflow/internal/DataFlowDispatch.qll
index 899dae69589..0d63558c956 100644
--- a/cpp/ql/lib/semmle/code/cpp/ir/dataflow/internal/DataFlowDispatch.qll
+++ b/cpp/ql/lib/semmle/code/cpp/ir/dataflow/internal/DataFlowDispatch.qll
@@ -105,7 +105,7 @@ private predicate ignoreConstructor(Expr e) {
* constructs an object containing at least one virtual function.
* - a node which represents a derived-to-base instruction that converts from `c`.
*/
-private predicate lambdaSourceImpl(RelevantNode n, Class c) {
+private predicate qualifierSourceImpl(RelevantNode n, Class c) {
// Object construction
exists(CallInstruction call, ThisArgumentOperand qualifier, Call e |
qualifier = call.getThisArgumentOperand() and
@@ -131,14 +131,14 @@ private predicate lambdaSourceImpl(RelevantNode n, Class c) {
)
}
-private module TrackVirtualDispatch {
+private module TrackVirtualDispatch {
/**
* Gets a possible runtime target of `c` using both static call-target
- * information, and call-target resolution from `lambdaDispatch0`.
+ * information, and call-target resolution from `virtualDispatch0`.
*/
private DataFlowPrivate::DataFlowCallable dispatch(DataFlowPrivate::DataFlowCall c) {
result = nonVirtualDispatch(c) or
- result = lambdaDispatch0(c)
+ result = virtualDispatch0(c)
}
private module TtInput implements TypeTrackingInput {
@@ -156,7 +156,7 @@ private module TrackVirtualDispatch {
or
DataFlowPrivate::jumpStep(_, this)
or
- lambdaSourceImpl(this, _)
+ qualifierSourceImpl(this, _)
}
}
@@ -220,21 +220,23 @@ private module TrackVirtualDispatch {
predicate hasFeatureBacktrackStoreTarget() { none() }
}
- private predicate lambdaSource(RelevantNode n) { lambdaSourceImpl(n, _) }
+ private predicate qualifierSource(RelevantNode n) { qualifierSourceImpl(n, _) }
/**
* Holds if `n` is the qualifier of `call` which targets the virtual member
* function `mf`.
*/
- private predicate lambdaSinkImpl(RelevantNode n, CallInstruction call, MemberFunction mf) {
+ private predicate qualifierOfVirtualCallImpl(
+ RelevantNode n, CallInstruction call, MemberFunction mf
+ ) {
n.asOperand() = call.getThisArgumentOperand() and
call.getStaticCallTarget() = mf and
mf.isVirtual()
}
- private predicate lambdaSink(RelevantNode n) { lambdaSinkImpl(n, _, _) }
+ private predicate qualifierOfVirtualCall(RelevantNode n) { qualifierOfVirtualCallImpl(n, _, _) }
- private import TypeTracking::TypeTrack::Graph
+ private import TypeTracking::TypeTrack::Graph
private predicate edgePlus(PathNode n1, PathNode n2) = fastTC(edges/2)(n1, n2)
@@ -243,7 +245,7 @@ private module TrackVirtualDispatch {
* qualifier has runtime type `c`.
*/
private MemberFunction mostSpecific(MemberFunction mf, Class c) {
- lambdaSinkImpl(_, _, mf) and
+ qualifierOfVirtualCallImpl(_, _, mf) and
mf.getAnOverridingFunction*() = result and
(
result.getDeclaringType() = c
@@ -267,8 +269,8 @@ private module TrackVirtualDispatch {
DataFlowPrivate::DataFlowCall call
) {
exists(Class derived, MemberFunction mf |
- lambdaSourceImpl(p1.getNode(), derived) and
- lambdaSinkImpl(p2.getNode(), call.asCallInstruction(), mf) and
+ qualifierSourceImpl(p1.getNode(), derived) and
+ qualifierOfVirtualCallImpl(p2.getNode(), call.asCallInstruction(), mf) and
p1.isSource() and
p2.isSink() and
callable.asSourceCallable() = mostSpecific(mf, derived)
@@ -276,7 +278,7 @@ private module TrackVirtualDispatch {
}
/** Gets a possible run-time target of `call`. */
- DataFlowPrivate::DataFlowCallable lambdaDispatch(DataFlowPrivate::DataFlowCall call) {
+ DataFlowPrivate::DataFlowCallable virtualDispatch(DataFlowPrivate::DataFlowCall call) {
exists(PathNode p1, PathNode p2 | p1 = p2 or edgePlus(p1, p2) | pairCand(p1, p2, result, call))
}
}
@@ -285,32 +287,32 @@ private DataFlowPrivate::DataFlowCallable noDisp(DataFlowPrivate::DataFlowCall c
pragma[nomagic]
private DataFlowPrivate::DataFlowCallable d1(DataFlowPrivate::DataFlowCall call) {
- result = TrackVirtualDispatch::lambdaDispatch(call)
+ result = TrackVirtualDispatch::virtualDispatch(call)
}
pragma[nomagic]
private DataFlowPrivate::DataFlowCallable d2(DataFlowPrivate::DataFlowCall call) {
- result = TrackVirtualDispatch::lambdaDispatch(call)
+ result = TrackVirtualDispatch::virtualDispatch(call)
}
pragma[nomagic]
private DataFlowPrivate::DataFlowCallable d3(DataFlowPrivate::DataFlowCall call) {
- result = TrackVirtualDispatch::lambdaDispatch(call)
+ result = TrackVirtualDispatch::virtualDispatch(call)
}
pragma[nomagic]
private DataFlowPrivate::DataFlowCallable d4(DataFlowPrivate::DataFlowCall call) {
- result = TrackVirtualDispatch::lambdaDispatch(call)
+ result = TrackVirtualDispatch::virtualDispatch(call)
}
pragma[nomagic]
private DataFlowPrivate::DataFlowCallable d5(DataFlowPrivate::DataFlowCall call) {
- result = TrackVirtualDispatch::lambdaDispatch(call)
+ result = TrackVirtualDispatch::virtualDispatch(call)
}
pragma[nomagic]
private DataFlowPrivate::DataFlowCallable d6(DataFlowPrivate::DataFlowCall call) {
- result = TrackVirtualDispatch::lambdaDispatch(call)
+ result = TrackVirtualDispatch::virtualDispatch(call)
}
/** Gets a function that might be called by `call`. */
From c475bedf73c19a0a50f1ebe6d18053f49b6eb482 Mon Sep 17 00:00:00 2001
From: Napalys Klicius
Date: Wed, 20 Aug 2025 12:58:54 +0200
Subject: [PATCH 160/984] CS: removed dead links from LDAPInjection qhelp
---
csharp/ql/src/Security Features/CWE-090/LDAPInjection.qhelp | 2 --
1 file changed, 2 deletions(-)
diff --git a/csharp/ql/src/Security Features/CWE-090/LDAPInjection.qhelp b/csharp/ql/src/Security Features/CWE-090/LDAPInjection.qhelp
index 04f01720ce6..34e9bee18ba 100644
--- a/csharp/ql/src/Security Features/CWE-090/LDAPInjection.qhelp
+++ b/csharp/ql/src/Security Features/CWE-090/LDAPInjection.qhelp
@@ -35,7 +35,5 @@ the query cannot be changed by a malicious user.
From 71a8e10f3d33f58f9bd53c4c6a7544ce3bb11298 Mon Sep 17 00:00:00 2001
From: Napalys Klicius
Date: Wed, 20 Aug 2025 12:59:59 +0200
Subject: [PATCH 161/984] CS: added extra guidance in recommendation section
for LDAPInjection
---
csharp/ql/src/Security Features/CWE-090/LDAPInjection.qhelp | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/csharp/ql/src/Security Features/CWE-090/LDAPInjection.qhelp b/csharp/ql/src/Security Features/CWE-090/LDAPInjection.qhelp
index 34e9bee18ba..4af37eadfd7 100644
--- a/csharp/ql/src/Security Features/CWE-090/LDAPInjection.qhelp
+++ b/csharp/ql/src/Security Features/CWE-090/LDAPInjection.qhelp
@@ -12,7 +12,7 @@ is likely to be able to run malicious LDAP queries.
If user input must be included in an LDAP query, it should be escaped to
avoid a malicious user providing special characters that change the meaning
of the query. If possible, use an existing library, such as the AntiXSS
-library.
+library. One may also make their own encoder filter `LdapEncode` following RFC 4515 standards.
@@ -35,5 +35,6 @@ the query cannot be changed by a malicious user.