+
+
+ Recursive functions are methods that call themselves either directly or indirectly through other functions.
+ While recursion can be a powerful programming technique, unbounded recursion on user inputs can lead
+ to stack overflow errors and program crashes, potentially enabling denial of service attacks.
+
+ This query detects recursive patterns up to order 4.
+
+
+
+
+
+ Review recursive functions and ensure that they are either:
+ - Not processing user-controlled data
+ - The data has been properly sanitized before recursing
+ - The recursion has an explicit depth limit
+
+
+ Consider replacing recursion with iterative alternatives where possible.
+
+
+
+
+ In this example, a binary stream reader processes tokens recursively.
+ For each new token `0x2`, the parser will create a new recursive call.
+ If this stream is user-controlled, an attacker can generate too many stackframes
+ and crash the application with a StackOverflow
error.
+
+
+
+ Trail Of Bits Blog: Low-effort denial of service with recursion
+
+
+ CWE-674: Uncontrolled Recursion
+
+
+
\ No newline at end of file
diff --git a/java/src/security/Recursion/Recursion.ql b/java/src/security/Recursion/Recursion.ql
new file mode 100644
index 0000000..55891d5
--- /dev/null
+++ b/java/src/security/Recursion/Recursion.ql
@@ -0,0 +1,91 @@
+/**
+ * @name Recursive functions
+ * @id tob/java/unbounded-recursion
+ * @description Detects possibly unbounded recursive calls
+ * @kind path-problem
+ * @tags security
+ * @precision low
+ * @problem.severity warning
+ * @security-severity 3.0
+ * @group security
+ */
+
+import java
+import semmle.code.java.dataflow.DataFlow
+
+predicate isTestPackage(RefType referenceType) {
+ referenceType.getPackage().getName().toLowerCase().matches("%test%") or
+ referenceType.getPackage().getName().toLowerCase().matches("%benchmark%") or
+ referenceType.getName().toLowerCase().matches("%test%")
+}
+
+class RecursionSource extends MethodCall {
+ RecursionSource() { not isTestPackage(this.getCaller().getDeclaringType()) }
+
+ override string toString() {
+ result = this.getCaller().toString() + " calls " + this.getCallee().toString()
+ }
+}
+
+/**
+ * Check if the Expr uses directly an argument of the enclosing function
+ */
+class ParameterOperation extends Expr {
+ ParameterOperation() {
+ (this instanceof BinaryExpr or this instanceof UnaryAssignExpr) and
+ exists(VarAccess va | va.getVariable() = this.getEnclosingCallable().getAParameter() |
+ this.getAChildExpr+() = va
+ )
+ }
+}
+
+module RecursiveConfig implements DataFlow::StateConfigSig {
+ class FlowState = Method;
+
+ predicate isSource(DataFlow::Node node, FlowState firstMethod) {
+ node.asExpr() instanceof RecursionSource and
+ firstMethod = node.asExpr().(MethodCall).getCaller()
+ }
+
+ predicate isSink(DataFlow::Node node, FlowState firstMethod) {
+ node.asExpr() instanceof RecursionSource and
+ firstMethod.calls+(node.asExpr().(MethodCall).getCaller()) and
+ node.asExpr().(MethodCall).getCallee().calls(firstMethod)
+ }
+
+ predicate isBarrier(DataFlow::Node node) {
+ exists(MethodCall ma |
+ ma = node.asExpr() and
+ exists(Expr e | e = ma.getAnArgument() and e instanceof ParameterOperation)
+ // or exists(
+ // VarAccess e|
+ // e = ma.getAnArgument() |
+ // e.getVariable().getAnAssignedValue().getAChildExpr() instanceof ParameterOperation
+ // )
+ )
+ }
+
+ /**
+ * Weird but useful deduplication logic
+ */
+ predicate isBarrierIn(DataFlow::Node node, FlowState state) {
+ not node.asExpr() instanceof MethodCall or
+ node.asExpr().(MethodCall).getCaller().getLocation().getStartLine() >
+ state.getLocation().getStartLine()
+ }
+}
+
+module RecursiveFlow = DataFlow::GlobalWithState