From 0c6925399d76ad479fb71c8c3fe0cb47595661fc Mon Sep 17 00:00:00 2001
From: Jami Cogswell
Date: Mon, 16 Dec 2024 18:44:55 -0500
Subject: [PATCH 01/38] Java: add qhelp
---
.../CWE-352/CsrfUnprotectedRequestType.qhelp | 43 +++++++++++++++++++
.../CsrfUnprotectedRequestTypeBad.java | 5 +++
.../CsrfUnprotectedRequestTypeGood.java | 5 +++
3 files changed, 53 insertions(+)
create mode 100644 java/ql/src/Security/CWE/CWE-352/CsrfUnprotectedRequestType.qhelp
create mode 100644 java/ql/src/Security/CWE/CWE-352/CsrfUnprotectedRequestTypeBad.java
create mode 100644 java/ql/src/Security/CWE/CWE-352/CsrfUnprotectedRequestTypeGood.java
diff --git a/java/ql/src/Security/CWE/CWE-352/CsrfUnprotectedRequestType.qhelp b/java/ql/src/Security/CWE/CWE-352/CsrfUnprotectedRequestType.qhelp
new file mode 100644
index 000000000000..7517fd5c7346
--- /dev/null
+++ b/java/ql/src/Security/CWE/CWE-352/CsrfUnprotectedRequestType.qhelp
@@ -0,0 +1,43 @@
+
+
+
+
+When you set up a web server to receive a request from a client without any mechanism
+for verifying that it was intentionally sent, then it is vulnerable to attack. An attacker can
+trick a client into making an unintended request to the web server that will be treated as
+an authentic request. This can be done via a URL, image load, XMLHttpRequest, etc. and can
+result in exposure of data or unintended code execution.
+
+
+
+When handling requests, make sure any requests that change application state are protected from
+Cross Site Request Forgery (CSRF). Some application frameworks, such as Spring, provide default CSRF
+protection for HTTP request types that may change application state, such as POST. Other HTTP request
+types, such as GET, should not be used for actions that change the state of the application, since these
+request types are not default-protected from CSRF by the framework.
+
+
+
+The following example shows a Spring request handler using a GET request for a state-changing action.
+Since a GET request does not have default CSRF protection in Spring, this type of request should
+not be used when modifying application state. Instead use one of Spring's default-protected request
+types, such as POST.
+
+
+
+
+
+
+
+
+OWASP:
+Cross-Site Request Forgery (CSRF) .
+
+
+Spring Security Reference:
+
+ Cross Site Request Forgery (CSRF)
+ .
+
+
+
diff --git a/java/ql/src/Security/CWE/CWE-352/CsrfUnprotectedRequestTypeBad.java b/java/ql/src/Security/CWE/CWE-352/CsrfUnprotectedRequestTypeBad.java
new file mode 100644
index 000000000000..c79d15cd828c
--- /dev/null
+++ b/java/ql/src/Security/CWE/CWE-352/CsrfUnprotectedRequestTypeBad.java
@@ -0,0 +1,5 @@
+// BAD - a GET request should not be used for a state-changing action like transfer
+@RequestMapping(value="transfer", method=RequestMethod.GET)
+public boolean transfer(HttpServletRequest request, HttpServletResponse response){
+ return doTransfer(request, response);
+}
diff --git a/java/ql/src/Security/CWE/CWE-352/CsrfUnprotectedRequestTypeGood.java b/java/ql/src/Security/CWE/CWE-352/CsrfUnprotectedRequestTypeGood.java
new file mode 100644
index 000000000000..40b54a800e12
--- /dev/null
+++ b/java/ql/src/Security/CWE/CWE-352/CsrfUnprotectedRequestTypeGood.java
@@ -0,0 +1,5 @@
+// GOOD - use a POST request for a state-changing action
+@RequestMapping(value="transfer", method=RequestMethod.POST)
+public boolean transfer(HttpServletRequest request, HttpServletResponse response){
+ return doTransfer(request, response);
+}
From 506d6682895e9e1c3b519435d91520e45206602d Mon Sep 17 00:00:00 2001
From: Jami Cogswell
Date: Tue, 26 Nov 2024 20:58:49 -0500
Subject: [PATCH 02/38] Java: add class for Spring request mapping methods that
are not default-protected from CSRF
---
.../CsrfUnprotectedRequestTypeQuery.qll | 29 +++++++++++++++++++
1 file changed, 29 insertions(+)
create mode 100644 java/ql/lib/semmle/code/java/security/CsrfUnprotectedRequestTypeQuery.qll
diff --git a/java/ql/lib/semmle/code/java/security/CsrfUnprotectedRequestTypeQuery.qll b/java/ql/lib/semmle/code/java/security/CsrfUnprotectedRequestTypeQuery.qll
new file mode 100644
index 000000000000..697461a30f1b
--- /dev/null
+++ b/java/ql/lib/semmle/code/java/security/CsrfUnprotectedRequestTypeQuery.qll
@@ -0,0 +1,29 @@
+/** Provides classes and predicates to reason about CSRF vulnerabilities due to use of unprotected HTTP request types. */
+
+import java
+private import semmle.code.java.frameworks.spring.SpringController
+
+/** A method that is not protected from CSRF by default. */
+abstract class CsrfUnprotectedMethod extends Method { }
+
+/**
+ * A Spring request mapping method that is not protected from CSRF by default.
+ *
+ * https://docs.spring.io/spring-security/reference/features/exploits/csrf.html#csrf-protection-read-only
+ */
+private class SpringCsrfUnprotectedMethod extends CsrfUnprotectedMethod instanceof SpringRequestMappingMethod
+{
+ SpringCsrfUnprotectedMethod() {
+ this.hasAnnotation("org.springframework.web.bind.annotation", "GetMapping")
+ or
+ this.hasAnnotation("org.springframework.web.bind.annotation", "RequestMapping") and
+ (
+ this.getAnAnnotation().getAnEnumConstantArrayValue("method").getName() =
+ ["GET", "HEAD", "OPTIONS", "TRACE"]
+ or
+ // If no request type is specified with `@RequestMapping`, then all request types
+ // are possible, so we treat this as unsafe; example: @RequestMapping(value = "test").
+ not exists(this.getAnAnnotation().getAnArrayValue("method"))
+ )
+ }
+}
From 8e9f21dc52228965019ae66863f7345903cf5324 Mon Sep 17 00:00:00 2001
From: Jami Cogswell
Date: Tue, 26 Nov 2024 21:14:32 -0500
Subject: [PATCH 03/38] Java: add a class for MyBatis Mapper methods that
update a database
---
.../CsrfUnprotectedRequestTypeQuery.qll | 17 +++++++++++++++++
1 file changed, 17 insertions(+)
diff --git a/java/ql/lib/semmle/code/java/security/CsrfUnprotectedRequestTypeQuery.qll b/java/ql/lib/semmle/code/java/security/CsrfUnprotectedRequestTypeQuery.qll
index 697461a30f1b..e33502174821 100644
--- a/java/ql/lib/semmle/code/java/security/CsrfUnprotectedRequestTypeQuery.qll
+++ b/java/ql/lib/semmle/code/java/security/CsrfUnprotectedRequestTypeQuery.qll
@@ -27,3 +27,20 @@ private class SpringCsrfUnprotectedMethod extends CsrfUnprotectedMethod instance
)
}
}
+
+/** A method that updates a database. */
+abstract class DatabaseUpdateMethod extends Method { }
+
+/** A MyBatis Mapper method that updates a database. */
+private class MyBatisMapperDatabaseUpdateMethod extends DatabaseUpdateMethod {
+ MyBatisMapperDatabaseUpdateMethod() {
+ exists(MyBatisMapperSqlOperation mapperXml |
+ (
+ mapperXml instanceof MyBatisMapperInsert or
+ mapperXml instanceof MyBatisMapperUpdate or
+ mapperXml instanceof MyBatisMapperDelete
+ ) and
+ this = mapperXml.getMapperMethod()
+ )
+ }
+}
From b88731df8004d42c0ebe996aa1d6bc8445c0123d Mon Sep 17 00:00:00 2001
From: Jami Cogswell
Date: Tue, 26 Nov 2024 21:18:40 -0500
Subject: [PATCH 04/38] Java: move contents of MyBatisMapperXML.qll in src to
MyBatis.qll in lib so importable, and fix experimental files broken by the
move
---
.../semmle/code/java/frameworks/MyBatis.qll | 111 ++++++++++++++++++
.../CsrfUnprotectedRequestTypeQuery.qll | 1 +
.../Security/CWE/CWE-089/MyBatisCommonLib.qll | 1 -
.../MyBatisMapperXmlSqlInjectionLib.qll | 2 +-
4 files changed, 113 insertions(+), 2 deletions(-)
diff --git a/java/ql/lib/semmle/code/java/frameworks/MyBatis.qll b/java/ql/lib/semmle/code/java/frameworks/MyBatis.qll
index 01c8b829de6b..c7fc09a33b4d 100644
--- a/java/ql/lib/semmle/code/java/frameworks/MyBatis.qll
+++ b/java/ql/lib/semmle/code/java/frameworks/MyBatis.qll
@@ -128,3 +128,114 @@ private class MyBatisProviderStep extends TaintTracking::AdditionalValueStep {
)
}
}
+
+/**
+ * A MyBatis Mapper XML file.
+ */
+class MyBatisMapperXmlFile extends XmlFile {
+ MyBatisMapperXmlFile() {
+ count(XmlElement e | e = this.getAChild()) = 1 and
+ this.getAChild().getName() = "mapper"
+ }
+}
+
+/**
+ * An XML element in a `MyBatisMapperXMLFile`.
+ */
+class MyBatisMapperXmlElement extends XmlElement {
+ MyBatisMapperXmlElement() { this.getFile() instanceof MyBatisMapperXmlFile }
+
+ /**
+ * Gets the value for this element, with leading and trailing whitespace trimmed.
+ */
+ string getValue() { result = this.allCharactersString().trim() }
+
+ /**
+ * Gets the reference type bound to MyBatis Mapper XML File.
+ */
+ RefType getNamespaceRefType() {
+ result.getQualifiedName() = this.getAttribute("namespace").getValue()
+ }
+}
+
+/**
+ * An MyBatis Mapper sql operation element.
+ */
+abstract class MyBatisMapperSqlOperation extends MyBatisMapperXmlElement {
+ /**
+ * Gets the value of the `id` attribute of MyBatis Mapper sql operation element.
+ */
+ string getId() { result = this.getAttribute("id").getValue() }
+
+ /**
+ * Gets the `` element in a `MyBatisMapperSqlOperation`.
+ */
+ MyBatisMapperInclude getInclude() { result = this.getAChild*() }
+
+ /**
+ * Gets the method bound to MyBatis Mapper XML File.
+ */
+ Method getMapperMethod() {
+ result.getName() = this.getId() and
+ result.getDeclaringType() = this.getParent().(MyBatisMapperXmlElement).getNamespaceRefType()
+ }
+}
+
+/**
+ * A `` element in a `MyBatisMapperSqlOperation`.
+ */
+class MyBatisMapperInsert extends MyBatisMapperSqlOperation {
+ MyBatisMapperInsert() { this.getName() = "insert" }
+}
+
+/**
+ * A `` element in a `MyBatisMapperSqlOperation`.
+ */
+class MyBatisMapperUpdate extends MyBatisMapperSqlOperation {
+ MyBatisMapperUpdate() { this.getName() = "update" }
+}
+
+/**
+ * A `` element in a `MyBatisMapperSqlOperation`.
+ */
+class MyBatisMapperDelete extends MyBatisMapperSqlOperation {
+ MyBatisMapperDelete() { this.getName() = "delete" }
+}
+
+/**
+ * A `` element in a `MyBatisMapperSqlOperation`.
+ */
+class MyBatisMapperSelect extends MyBatisMapperSqlOperation {
+ MyBatisMapperSelect() { this.getName() = "select" }
+}
+
+/**
+ * A `` element in a `MyBatisMapperXMLElement`.
+ */
+class MyBatisMapperSql extends MyBatisMapperXmlElement {
+ MyBatisMapperSql() { this.getName() = "sql" }
+
+ /**
+ * Gets the value of the `id` attribute of this ``.
+ */
+ string getId() { result = this.getAttribute("id").getValue() }
+}
+
+/**
+ * A `` element in a `MyBatisMapperXMLElement`.
+ */
+class MyBatisMapperInclude extends MyBatisMapperXmlElement {
+ MyBatisMapperInclude() { this.getName() = "include" }
+
+ /**
+ * Gets the value of the `refid` attribute of this ``.
+ */
+ string getRefid() { result = this.getAttribute("refid").getValue() }
+}
+
+/**
+ * A `` element in a `MyBatisMapperXMLElement`.
+ */
+class MyBatisMapperForeach extends MyBatisMapperXmlElement {
+ MyBatisMapperForeach() { this.getName() = "foreach" }
+}
diff --git a/java/ql/lib/semmle/code/java/security/CsrfUnprotectedRequestTypeQuery.qll b/java/ql/lib/semmle/code/java/security/CsrfUnprotectedRequestTypeQuery.qll
index e33502174821..ab222262b4e1 100644
--- a/java/ql/lib/semmle/code/java/security/CsrfUnprotectedRequestTypeQuery.qll
+++ b/java/ql/lib/semmle/code/java/security/CsrfUnprotectedRequestTypeQuery.qll
@@ -2,6 +2,7 @@
import java
private import semmle.code.java.frameworks.spring.SpringController
+private import semmle.code.java.frameworks.MyBatis
/** A method that is not protected from CSRF by default. */
abstract class CsrfUnprotectedMethod extends Method { }
diff --git a/java/ql/src/experimental/Security/CWE/CWE-089/MyBatisCommonLib.qll b/java/ql/src/experimental/Security/CWE/CWE-089/MyBatisCommonLib.qll
index be635b5987f2..257ba8e0d9a5 100644
--- a/java/ql/src/experimental/Security/CWE/CWE-089/MyBatisCommonLib.qll
+++ b/java/ql/src/experimental/Security/CWE/CWE-089/MyBatisCommonLib.qll
@@ -4,7 +4,6 @@
deprecated module;
import java
-import semmle.code.xml.MyBatisMapperXML
import semmle.code.java.dataflow.FlowSources
import semmle.code.java.frameworks.MyBatis
import semmle.code.java.frameworks.Properties
diff --git a/java/ql/src/experimental/Security/CWE/CWE-089/MyBatisMapperXmlSqlInjectionLib.qll b/java/ql/src/experimental/Security/CWE/CWE-089/MyBatisMapperXmlSqlInjectionLib.qll
index 41ededc7b93e..ce2102003d5e 100644
--- a/java/ql/src/experimental/Security/CWE/CWE-089/MyBatisMapperXmlSqlInjectionLib.qll
+++ b/java/ql/src/experimental/Security/CWE/CWE-089/MyBatisMapperXmlSqlInjectionLib.qll
@@ -4,7 +4,7 @@
deprecated module;
import java
-import semmle.code.xml.MyBatisMapperXML
+import semmle.code.java.frameworks.MyBatis
import semmle.code.java.dataflow.FlowSources
import semmle.code.java.frameworks.Properties
From 43a288070c03f88ea79fe6114196aaa9e84c2f9c Mon Sep 17 00:00:00 2001
From: Jami Cogswell
Date: Tue, 26 Nov 2024 21:47:34 -0500
Subject: [PATCH 05/38] Java: add a class for PreparedStatement methods that
update a database
---
java/ql/lib/semmle/code/java/frameworks/Jdbc.qll | 16 ++++++++++++++++
.../security/CsrfUnprotectedRequestTypeQuery.qll | 9 +++++++++
2 files changed, 25 insertions(+)
diff --git a/java/ql/lib/semmle/code/java/frameworks/Jdbc.qll b/java/ql/lib/semmle/code/java/frameworks/Jdbc.qll
index 2723f3f05f59..37be7dcf09a7 100644
--- a/java/ql/lib/semmle/code/java/frameworks/Jdbc.qll
+++ b/java/ql/lib/semmle/code/java/frameworks/Jdbc.qll
@@ -34,3 +34,19 @@ class ResultSetGetStringMethod extends Method {
this.getReturnType() instanceof TypeString
}
}
+
+/** A method with the name `executeUpdate` declared in `java.sql.PreparedStatement`. */
+class PreparedStatementExecuteUpdateMethod extends Method {
+ PreparedStatementExecuteUpdateMethod() {
+ this.getDeclaringType() instanceof TypePreparedStatement and
+ this.hasName("executeUpdate")
+ }
+}
+
+/** A method with the name `executeLargeUpdate` declared in `java.sql.PreparedStatement`. */
+class PreparedStatementExecuteLargeUpdateMethod extends Method {
+ PreparedStatementExecuteLargeUpdateMethod() {
+ this.getDeclaringType() instanceof TypePreparedStatement and
+ this.hasName("executeLargeUpdate")
+ }
+}
diff --git a/java/ql/lib/semmle/code/java/security/CsrfUnprotectedRequestTypeQuery.qll b/java/ql/lib/semmle/code/java/security/CsrfUnprotectedRequestTypeQuery.qll
index ab222262b4e1..5ba7a2b36aaa 100644
--- a/java/ql/lib/semmle/code/java/security/CsrfUnprotectedRequestTypeQuery.qll
+++ b/java/ql/lib/semmle/code/java/security/CsrfUnprotectedRequestTypeQuery.qll
@@ -3,6 +3,7 @@
import java
private import semmle.code.java.frameworks.spring.SpringController
private import semmle.code.java.frameworks.MyBatis
+private import semmle.code.java.frameworks.Jdbc
/** A method that is not protected from CSRF by default. */
abstract class CsrfUnprotectedMethod extends Method { }
@@ -45,3 +46,11 @@ private class MyBatisMapperDatabaseUpdateMethod extends DatabaseUpdateMethod {
)
}
}
+
+/** A method declared in `java.sql.PreparedStatement` that updates a database. */
+private class PreparedStatementDatabaseUpdateMethod extends DatabaseUpdateMethod {
+ PreparedStatementDatabaseUpdateMethod() {
+ this instanceof PreparedStatementExecuteUpdateMethod or
+ this instanceof PreparedStatementExecuteLargeUpdateMethod
+ }
+}
From 87a8746b22d12efeb571622c0cc1b9f60f15995d Mon Sep 17 00:00:00 2001
From: Jami Cogswell
Date: Tue, 26 Nov 2024 22:01:34 -0500
Subject: [PATCH 06/38] Java: add a class for methods that update a sql
database (found using sql-injection nodes)
---
.../CsrfUnprotectedRequestTypeQuery.qll | 17 +++++++++++++++++
1 file changed, 17 insertions(+)
diff --git a/java/ql/lib/semmle/code/java/security/CsrfUnprotectedRequestTypeQuery.qll b/java/ql/lib/semmle/code/java/security/CsrfUnprotectedRequestTypeQuery.qll
index 5ba7a2b36aaa..589df570a6b3 100644
--- a/java/ql/lib/semmle/code/java/security/CsrfUnprotectedRequestTypeQuery.qll
+++ b/java/ql/lib/semmle/code/java/security/CsrfUnprotectedRequestTypeQuery.qll
@@ -4,6 +4,8 @@ import java
private import semmle.code.java.frameworks.spring.SpringController
private import semmle.code.java.frameworks.MyBatis
private import semmle.code.java.frameworks.Jdbc
+private import semmle.code.java.dataflow.DataFlow
+private import semmle.code.java.dataflow.ExternalFlow
/** A method that is not protected from CSRF by default. */
abstract class CsrfUnprotectedMethod extends Method { }
@@ -54,3 +56,18 @@ private class PreparedStatementDatabaseUpdateMethod extends DatabaseUpdateMethod
this instanceof PreparedStatementExecuteLargeUpdateMethod
}
}
+
+/** A method that updates a SQL database. */
+private class SqlDatabaseUpdateMethod extends DatabaseUpdateMethod {
+ SqlDatabaseUpdateMethod() {
+ // TODO: constrain to only insert/update/delete for `execute%` methods; need to track the sql expression into the execute call.
+ exists(DataFlow::Node n | this = n.asExpr().(Argument).getCall().getCallee() |
+ sinkNode(n, "sql-injection") and
+ // do not include `executeQuery` since it is typically used with a select statement
+ this.hasName([
+ "delete", "insert", "update", "batchUpdate", "executeUpdate", "executeLargeUpdate",
+ "execute"
+ ])
+ )
+ }
+}
From c553e3132ecd91872d8f910e1272bbbb21a0b00c Mon Sep 17 00:00:00 2001
From: Jami Cogswell
Date: Tue, 26 Nov 2024 22:10:23 -0500
Subject: [PATCH 07/38] Java: add CallGraph module for displaying call graph
paths
---
.../CsrfUnprotectedRequestTypeQuery.qll | 45 +++++++++++++++++++
1 file changed, 45 insertions(+)
diff --git a/java/ql/lib/semmle/code/java/security/CsrfUnprotectedRequestTypeQuery.qll b/java/ql/lib/semmle/code/java/security/CsrfUnprotectedRequestTypeQuery.qll
index 589df570a6b3..8c4cb3beb5df 100644
--- a/java/ql/lib/semmle/code/java/security/CsrfUnprotectedRequestTypeQuery.qll
+++ b/java/ql/lib/semmle/code/java/security/CsrfUnprotectedRequestTypeQuery.qll
@@ -6,6 +6,7 @@ private import semmle.code.java.frameworks.MyBatis
private import semmle.code.java.frameworks.Jdbc
private import semmle.code.java.dataflow.DataFlow
private import semmle.code.java.dataflow.ExternalFlow
+private import semmle.code.java.dispatch.VirtualDispatch
/** A method that is not protected from CSRF by default. */
abstract class CsrfUnprotectedMethod extends Method { }
@@ -71,3 +72,47 @@ private class SqlDatabaseUpdateMethod extends DatabaseUpdateMethod {
)
}
}
+
+module CallGraph {
+ newtype TPathNode =
+ TMethod(Method m) or
+ TCall(Call c)
+
+ class PathNode extends TPathNode {
+ Method asMethod() { this = TMethod(result) }
+
+ Call asCall() { this = TCall(result) }
+
+ string toString() {
+ result = this.asMethod().toString()
+ or
+ result = this.asCall().toString()
+ }
+
+ private PathNode getACallee() {
+ [viableCallable(this.asCall()), this.asCall().getCallee()] = result.asMethod()
+ }
+
+ PathNode getASuccessor() {
+ this.asMethod() = result.asCall().getEnclosingCallable()
+ or
+ result = this.getACallee() and
+ (
+ exists(PathNode p |
+ p = this.getACallee() and
+ p.asMethod() instanceof DatabaseUpdateMethod
+ )
+ implies
+ result.asMethod() instanceof DatabaseUpdateMethod
+ )
+ }
+
+ Location getLocation() {
+ result = this.asMethod().getLocation()
+ or
+ result = this.asCall().getLocation()
+ }
+ }
+
+ query predicate edges(PathNode pred, PathNode succ) { pred.getASuccessor() = succ }
+}
From 178b032453a89c1ac68a82b50b791aaedd69907f Mon Sep 17 00:00:00 2001
From: Jami Cogswell
Date: Tue, 26 Nov 2024 23:06:56 -0500
Subject: [PATCH 08/38] Java: add query
---
.../CWE/CWE-352/CsrfUnprotectedRequestType.ql | 27 +++++++++++++++++++
1 file changed, 27 insertions(+)
create mode 100644 java/ql/src/Security/CWE/CWE-352/CsrfUnprotectedRequestType.ql
diff --git a/java/ql/src/Security/CWE/CWE-352/CsrfUnprotectedRequestType.ql b/java/ql/src/Security/CWE/CWE-352/CsrfUnprotectedRequestType.ql
new file mode 100644
index 000000000000..dfe01d64efc9
--- /dev/null
+++ b/java/ql/src/Security/CWE/CWE-352/CsrfUnprotectedRequestType.ql
@@ -0,0 +1,27 @@
+/**
+ * @name HTTP request type unprotected from CSRF
+ * @description Using an HTTP request type that is not default-protected from CSRF for a
+ * state-changing action makes the application vulnerable to a Cross-Site
+ * Request Forgery (CSRF) attack.
+ * @kind path-problem
+ * @problem.severity error
+ * @security-severity 8.8
+ * @precision low
+ * @id java/csrf-unprotected-request-type
+ * @tags security
+ * external/cwe/cwe-352
+ */
+
+import java
+import semmle.code.java.security.CsrfUnprotectedRequestTypeQuery
+import CallGraph
+
+from PathNode source, PathNode reachable, PathNode callsReachable
+where
+ source.asMethod() instanceof CsrfUnprotectedMethod and
+ reachable.asMethod() instanceof DatabaseUpdateMethod and
+ callsReachable.getASuccessor() = reachable and
+ source.getASuccessor+() = callsReachable
+select source.asMethod(), source, callsReachable,
+ "Potential CSRF vulnerability due to using an HTTP request type which is not default-protected from CSRF for an apparent $@.",
+ callsReachable, "state-changing action"
From df77d4914f3be81ce173d7dc7d1e1ec678cd2139 Mon Sep 17 00:00:00 2001
From: Jami Cogswell
Date: Tue, 3 Dec 2024 16:31:52 -0500
Subject: [PATCH 09/38] Java: initial tests
---
.../CsrfUnprotectedRequestTypeQuery.qll | 12 +-
.../CWE/CWE-352/CsrfUnprotectedRequestType.ql | 9 +-
.../CsrfUnprotectedRequestTypeTest.expected | 2 +
.../CsrfUnprotectedRequestTypeTest.java | 158 ++++++++++++++++++
.../CWE-352/CsrfUnprotectedRequestTypeTest.ql | 20 +++
.../security/CWE-352/MyBatisMapper.java | 31 ++++
.../security/CWE-352/MyBatisMapper.xml | 35 ++++
.../security/CWE-352/MyBatisProvider.java | 24 +++
.../security/CWE-352/MyBatisService.java | 25 +++
.../test/query-tests/security/CWE-352/options | 2 +-
10 files changed, 310 insertions(+), 8 deletions(-)
create mode 100644 java/ql/test/query-tests/security/CWE-352/CsrfUnprotectedRequestTypeTest.expected
create mode 100644 java/ql/test/query-tests/security/CWE-352/CsrfUnprotectedRequestTypeTest.java
create mode 100644 java/ql/test/query-tests/security/CWE-352/CsrfUnprotectedRequestTypeTest.ql
create mode 100644 java/ql/test/query-tests/security/CWE-352/MyBatisMapper.java
create mode 100644 java/ql/test/query-tests/security/CWE-352/MyBatisMapper.xml
create mode 100644 java/ql/test/query-tests/security/CWE-352/MyBatisProvider.java
create mode 100644 java/ql/test/query-tests/security/CWE-352/MyBatisService.java
diff --git a/java/ql/lib/semmle/code/java/security/CsrfUnprotectedRequestTypeQuery.qll b/java/ql/lib/semmle/code/java/security/CsrfUnprotectedRequestTypeQuery.qll
index 8c4cb3beb5df..b96a3c6f1a47 100644
--- a/java/ql/lib/semmle/code/java/security/CsrfUnprotectedRequestTypeQuery.qll
+++ b/java/ql/lib/semmle/code/java/security/CsrfUnprotectedRequestTypeQuery.qll
@@ -114,5 +114,15 @@ module CallGraph {
}
}
- query predicate edges(PathNode pred, PathNode succ) { pred.getASuccessor() = succ }
+ predicate edges(PathNode pred, PathNode succ) { pred.getASuccessor() = succ }
+}
+
+import CallGraph
+
+/** Holds if `src` is an unprotected request handler that reaches a state-changing `sink`. */
+predicate unprotectedStateChange(PathNode src, PathNode sink, PathNode sinkPred) {
+ src.asMethod() instanceof CsrfUnprotectedMethod and
+ sink.asMethod() instanceof DatabaseUpdateMethod and
+ sinkPred.getASuccessor() = sink and
+ src.getASuccessor+() = sinkPred
}
diff --git a/java/ql/src/Security/CWE/CWE-352/CsrfUnprotectedRequestType.ql b/java/ql/src/Security/CWE/CWE-352/CsrfUnprotectedRequestType.ql
index dfe01d64efc9..61612198c9eb 100644
--- a/java/ql/src/Security/CWE/CWE-352/CsrfUnprotectedRequestType.ql
+++ b/java/ql/src/Security/CWE/CWE-352/CsrfUnprotectedRequestType.ql
@@ -14,14 +14,11 @@
import java
import semmle.code.java.security.CsrfUnprotectedRequestTypeQuery
-import CallGraph
+
+query predicate edges(PathNode pred, PathNode succ) { CallGraph::edges(pred, succ) }
from PathNode source, PathNode reachable, PathNode callsReachable
-where
- source.asMethod() instanceof CsrfUnprotectedMethod and
- reachable.asMethod() instanceof DatabaseUpdateMethod and
- callsReachable.getASuccessor() = reachable and
- source.getASuccessor+() = callsReachable
+where unprotectedStateChange(source, reachable, callsReachable)
select source.asMethod(), source, callsReachable,
"Potential CSRF vulnerability due to using an HTTP request type which is not default-protected from CSRF for an apparent $@.",
callsReachable, "state-changing action"
diff --git a/java/ql/test/query-tests/security/CWE-352/CsrfUnprotectedRequestTypeTest.expected b/java/ql/test/query-tests/security/CWE-352/CsrfUnprotectedRequestTypeTest.expected
new file mode 100644
index 000000000000..8ec8033d086e
--- /dev/null
+++ b/java/ql/test/query-tests/security/CWE-352/CsrfUnprotectedRequestTypeTest.expected
@@ -0,0 +1,2 @@
+testFailures
+failures
diff --git a/java/ql/test/query-tests/security/CWE-352/CsrfUnprotectedRequestTypeTest.java b/java/ql/test/query-tests/security/CWE-352/CsrfUnprotectedRequestTypeTest.java
new file mode 100644
index 000000000000..c7d83bca441a
--- /dev/null
+++ b/java/ql/test/query-tests/security/CWE-352/CsrfUnprotectedRequestTypeTest.java
@@ -0,0 +1,158 @@
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+import static org.springframework.web.bind.annotation.RequestMethod.*;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.beans.factory.annotation.Autowired;
+import java.sql.DriverManager;
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.sql.SQLException;
+
+@Controller
+public class CsrfUnprotectedRequestTypeTest {
+
+ // Test Spring sources with `PreparedStatement.executeUpdate()` as a default database update method call
+
+ // BAD: allows request type not default-protected from CSRF when updating a database
+ @RequestMapping("/")
+ public void bad1() { // $ hasCsrfUnprotectedRequestType
+ try {
+ String sql = "DELETE";
+ Connection conn = DriverManager.getConnection("url");
+ PreparedStatement ps = conn.prepareStatement(sql);
+ ps.executeUpdate(); // database update method call
+ } catch (SQLException e) { }
+ }
+
+ // BAD: uses GET request when updating a database
+ @RequestMapping(value = "", method = RequestMethod.GET)
+ public void bad2() { // $ hasCsrfUnprotectedRequestType
+ try {
+ String sql = "DELETE";
+ Connection conn = DriverManager.getConnection("url");
+ PreparedStatement ps = conn.prepareStatement(sql);
+ ps.executeUpdate(); // database update method call
+ } catch (SQLException e) { }
+ }
+
+ // BAD: uses GET request when updating a database
+ @GetMapping(value = "")
+ public void bad3() { // $ hasCsrfUnprotectedRequestType
+ try {
+ String sql = "DELETE";
+ Connection conn = DriverManager.getConnection("url");
+ PreparedStatement ps = conn.prepareStatement(sql);
+ ps.executeUpdate(); // database update method call
+ } catch (SQLException e) { }
+ }
+
+ // BAD: allows GET request when updating a database
+ @RequestMapping(value = "", method = { RequestMethod.GET, RequestMethod.POST })
+ public void bad4() { // $ hasCsrfUnprotectedRequestType
+ try {
+ String sql = "DELETE";
+ Connection conn = DriverManager.getConnection("url");
+ PreparedStatement ps = conn.prepareStatement(sql);
+ ps.executeUpdate(); // database update method call
+ } catch (SQLException e) { }
+ }
+
+ // BAD: uses request type not default-protected from CSRF when updating a database
+ @RequestMapping(value = "", method = { GET, HEAD, OPTIONS, TRACE })
+ public void bad5() { // $ hasCsrfUnprotectedRequestType
+ try {
+ String sql = "DELETE";
+ Connection conn = DriverManager.getConnection("url");
+ PreparedStatement ps = conn.prepareStatement(sql);
+ ps.executeUpdate(); // database update method call
+ } catch (SQLException e) { }
+ }
+
+ // GOOD: uses POST request when updating a database
+ @RequestMapping(value = "", method = RequestMethod.POST)
+ public void good1() {
+ try {
+ String sql = "DELETE";
+ Connection conn = DriverManager.getConnection("url");
+ PreparedStatement ps = conn.prepareStatement(sql);
+ ps.executeUpdate(); // database update method call
+ } catch (SQLException e) { }
+ }
+
+ // GOOD: uses POST request when updating a database
+ @RequestMapping(value = "", method = POST)
+ public void good2() {
+ try {
+ String sql = "DELETE";
+ Connection conn = DriverManager.getConnection("url");
+ PreparedStatement ps = conn.prepareStatement(sql);
+ ps.executeUpdate(); // database update method call
+ } catch (SQLException e) { }
+ }
+
+ // GOOD: uses POST request when updating a database
+ @PostMapping(value = "")
+ public void good3() {
+ try {
+ String sql = "DELETE";
+ Connection conn = DriverManager.getConnection("url");
+ PreparedStatement ps = conn.prepareStatement(sql);
+ ps.executeUpdate(); // database update method call
+ } catch (SQLException e) { }
+ }
+
+ // GOOD: uses a request type that is default-protected from CSRF when updating a database
+ @RequestMapping(value = "", method = { POST, PUT, PATCH, DELETE })
+ public void good4() {
+ try {
+ String sql = "DELETE";
+ Connection conn = DriverManager.getConnection("url");
+ PreparedStatement ps = conn.prepareStatement(sql);
+ ps.executeUpdate(); // database update method call
+ } catch (SQLException e) { }
+ }
+
+ // Test database update method calls other than `PreparedStatement.executeUpdate()`
+
+ // BAD: allows request type not default-protected from CSRF when
+ // updating a database using `PreparedStatement.executeLargeUpdate()`
+ @RequestMapping("/")
+ public void bad6() { // $ hasCsrfUnprotectedRequestType
+ try {
+ String sql = "DELETE";
+ Connection conn = DriverManager.getConnection("url");
+ PreparedStatement ps = conn.prepareStatement(sql);
+ ps.executeLargeUpdate(); // database update method call
+ } catch (SQLException e) { }
+ }
+
+ @Autowired
+ private MyBatisService myBatisService;
+
+ // BAD: uses GET request when updating a database with MyBatis XML mapper method
+ @GetMapping(value = "")
+ public void bad7(@RequestParam String name) { // $ hasCsrfUnprotectedRequestType
+ myBatisService.bad7(name);
+ }
+
+ // BAD: uses GET request when updating a database with `@DeleteProvider`
+ @GetMapping(value = "badDelete")
+ public void badDelete(@RequestParam String name) { // $ hasCsrfUnprotectedRequestType
+ myBatisService.badDelete(name);
+ }
+
+ // BAD: uses GET request when updating a database with `@UpdateProvider`
+ @GetMapping(value = "badUpdate")
+ public void badUpdate(@RequestParam String name) { // $ hasCsrfUnprotectedRequestType
+ myBatisService.badUpdate(name);
+ }
+
+ // BAD: uses GET request when updating a database with `@InsertProvider`
+ @GetMapping(value = "badInsert")
+ public void badInsert(@RequestParam String name) { // $ hasCsrfUnprotectedRequestType
+ myBatisService.badInsert(name);
+ }
+}
diff --git a/java/ql/test/query-tests/security/CWE-352/CsrfUnprotectedRequestTypeTest.ql b/java/ql/test/query-tests/security/CWE-352/CsrfUnprotectedRequestTypeTest.ql
new file mode 100644
index 000000000000..f7e5f0c9db87
--- /dev/null
+++ b/java/ql/test/query-tests/security/CWE-352/CsrfUnprotectedRequestTypeTest.ql
@@ -0,0 +1,20 @@
+import java
+import semmle.code.java.security.CsrfUnprotectedRequestTypeQuery
+import TestUtilities.InlineExpectationsTest
+
+module CsrfUnprotectedRequestTypeTest implements TestSig {
+ string getARelevantTag() { result = "hasCsrfUnprotectedRequestType" }
+
+ predicate hasActualResult(Location location, string element, string tag, string value) {
+ tag = "hasCsrfUnprotectedRequestType" and
+ exists(PathNode src, PathNode sink, PathNode sinkPred |
+ unprotectedStateChange(src, sink, sinkPred)
+ |
+ src.getLocation() = location and
+ element = src.toString() and
+ value = ""
+ )
+ }
+}
+
+import MakeTest
diff --git a/java/ql/test/query-tests/security/CWE-352/MyBatisMapper.java b/java/ql/test/query-tests/security/CWE-352/MyBatisMapper.java
new file mode 100644
index 000000000000..a49e9f997ff2
--- /dev/null
+++ b/java/ql/test/query-tests/security/CWE-352/MyBatisMapper.java
@@ -0,0 +1,31 @@
+import org.apache.ibatis.annotations.Mapper;
+import org.springframework.stereotype.Repository;
+import org.apache.ibatis.annotations.DeleteProvider;
+import org.apache.ibatis.annotations.UpdateProvider;
+import org.apache.ibatis.annotations.InsertProvider;
+
+@Mapper
+@Repository
+public interface MyBatisMapper {
+
+ void bad7(String name);
+
+ //using providers
+ @DeleteProvider(
+ type = MyBatisProvider.class,
+ method = "badDelete"
+ )
+ void badDelete(String input);
+
+ @UpdateProvider(
+ type = MyBatisProvider.class,
+ method = "badUpdate"
+ )
+ void badUpdate(String input);
+
+ @InsertProvider(
+ type = MyBatisProvider.class,
+ method = "badInsert"
+ )
+ void badInsert(String input);
+}
diff --git a/java/ql/test/query-tests/security/CWE-352/MyBatisMapper.xml b/java/ql/test/query-tests/security/CWE-352/MyBatisMapper.xml
new file mode 100644
index 000000000000..be8efe9422f5
--- /dev/null
+++ b/java/ql/test/query-tests/security/CWE-352/MyBatisMapper.xml
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ and name = ${ test . name , jdbcType = VARCHAR }
+
+
+ and id = #{test.id}
+
+
+
+
+
+ insert into test (name, pass)
+
+
+ name = ${name,jdbcType=VARCHAR},
+
+
+ pass = ${pass},
+
+
+
+
+
diff --git a/java/ql/test/query-tests/security/CWE-352/MyBatisProvider.java b/java/ql/test/query-tests/security/CWE-352/MyBatisProvider.java
new file mode 100644
index 000000000000..53b1ca723bdf
--- /dev/null
+++ b/java/ql/test/query-tests/security/CWE-352/MyBatisProvider.java
@@ -0,0 +1,24 @@
+import org.apache.ibatis.annotations.Param;
+import org.apache.ibatis.jdbc.SQL;
+
+public class MyBatisProvider {
+
+ public String badDelete(@Param("input") final String input) {
+ return "DELETE FROM users WHERE username = '" + input + "';";
+ }
+
+ public String badUpdate(@Param("input") final String input) {
+ String s = (new SQL() {
+ {
+ this.UPDATE("users");
+ this.SET("balance = 0");
+ this.WHERE("username = '" + input + "'");
+ }
+ }).toString();
+ return s;
+ }
+
+ public String badInsert(@Param("input") final String input) {
+ return "INSERT INTO users VALUES (1, '" + input + "', 'hunter2');";
+ }
+}
diff --git a/java/ql/test/query-tests/security/CWE-352/MyBatisService.java b/java/ql/test/query-tests/security/CWE-352/MyBatisService.java
new file mode 100644
index 000000000000..78ea02399b2b
--- /dev/null
+++ b/java/ql/test/query-tests/security/CWE-352/MyBatisService.java
@@ -0,0 +1,25 @@
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+@Service
+public class MyBatisService {
+
+ @Autowired
+ private MyBatisMapper myBatisMapper;
+
+ public void bad7(String name) {
+ myBatisMapper.bad7(name);
+ }
+
+ public void badDelete(String input) {
+ myBatisMapper.badDelete(input);
+ }
+
+ public void badUpdate(String input) {
+ myBatisMapper.badUpdate(input);
+ }
+
+ public void badInsert(String input) {
+ myBatisMapper.badInsert(input);
+ }
+}
diff --git a/java/ql/test/query-tests/security/CWE-352/options b/java/ql/test/query-tests/security/CWE-352/options
index 595ccc6b812b..339a048c3200 100644
--- a/java/ql/test/query-tests/security/CWE-352/options
+++ b/java/ql/test/query-tests/security/CWE-352/options
@@ -1 +1 @@
-semmle-extractor-options: --javac-args -cp ${testdir}/../../../stubs/springframework-5.3.8
\ No newline at end of file
+semmle-extractor-options: --javac-args -cp ${testdir}/../../../stubs/springframework-5.3.8/:${testdir}/../../../stubs/org.mybatis-3.5.4/
From 97aaf4c011d90b8ac68e851561f64cdac06a2884 Mon Sep 17 00:00:00 2001
From: Jami Cogswell
Date: Tue, 3 Dec 2024 19:10:26 -0500
Subject: [PATCH 10/38] Java: handle MyBatis annotations for
insert/update/delete
---
.../CsrfUnprotectedRequestTypeQuery.qll | 14 ++++++++---
.../CsrfUnprotectedRequestTypeTest.java | 24 ++++++++++++++++---
.../security/CWE-352/MyBatisMapper.java | 12 ++++++++++
.../security/CWE-352/MyBatisService.java | 12 ++++++++++
.../org/apache/ibatis/annotations/Delete.java | 14 +++++++++++
.../org/apache/ibatis/annotations/Insert.java | 14 +++++++++++
.../org/apache/ibatis/annotations/Update.java | 14 +++++++++++
7 files changed, 98 insertions(+), 6 deletions(-)
create mode 100644 java/ql/test/stubs/org.mybatis-3.5.4/org/apache/ibatis/annotations/Delete.java
create mode 100644 java/ql/test/stubs/org.mybatis-3.5.4/org/apache/ibatis/annotations/Insert.java
create mode 100644 java/ql/test/stubs/org.mybatis-3.5.4/org/apache/ibatis/annotations/Update.java
diff --git a/java/ql/lib/semmle/code/java/security/CsrfUnprotectedRequestTypeQuery.qll b/java/ql/lib/semmle/code/java/security/CsrfUnprotectedRequestTypeQuery.qll
index b96a3c6f1a47..633a352785a5 100644
--- a/java/ql/lib/semmle/code/java/security/CsrfUnprotectedRequestTypeQuery.qll
+++ b/java/ql/lib/semmle/code/java/security/CsrfUnprotectedRequestTypeQuery.qll
@@ -36,9 +36,9 @@ private class SpringCsrfUnprotectedMethod extends CsrfUnprotectedMethod instance
/** A method that updates a database. */
abstract class DatabaseUpdateMethod extends Method { }
-/** A MyBatis Mapper method that updates a database. */
-private class MyBatisMapperDatabaseUpdateMethod extends DatabaseUpdateMethod {
- MyBatisMapperDatabaseUpdateMethod() {
+/** A MyBatis method that updates a database. */
+private class MyBatisDatabaseUpdateMethod extends DatabaseUpdateMethod {
+ MyBatisDatabaseUpdateMethod() {
exists(MyBatisMapperSqlOperation mapperXml |
(
mapperXml instanceof MyBatisMapperInsert or
@@ -47,6 +47,14 @@ private class MyBatisMapperDatabaseUpdateMethod extends DatabaseUpdateMethod {
) and
this = mapperXml.getMapperMethod()
)
+ or
+ exists(MyBatisSqlOperationAnnotationMethod m | this = m |
+ not m.getAnAnnotation().getType().hasQualifiedName("org.apache.ibatis.annotations", "Select")
+ )
+ or
+ exists(Method m | this = m |
+ m.hasAnnotation("org.apache.ibatis.annotations", ["Delete", "Update", "Insert"] + "Provider")
+ )
}
}
diff --git a/java/ql/test/query-tests/security/CWE-352/CsrfUnprotectedRequestTypeTest.java b/java/ql/test/query-tests/security/CWE-352/CsrfUnprotectedRequestTypeTest.java
index c7d83bca441a..16aab568b7d7 100644
--- a/java/ql/test/query-tests/security/CWE-352/CsrfUnprotectedRequestTypeTest.java
+++ b/java/ql/test/query-tests/security/CWE-352/CsrfUnprotectedRequestTypeTest.java
@@ -138,21 +138,39 @@ public void bad7(@RequestParam String name) { // $ hasCsrfUnprotectedRequestType
myBatisService.bad7(name);
}
- // BAD: uses GET request when updating a database with `@DeleteProvider`
+ // BAD: uses GET request when updating a database with MyBatis `@DeleteProvider`
@GetMapping(value = "badDelete")
public void badDelete(@RequestParam String name) { // $ hasCsrfUnprotectedRequestType
myBatisService.badDelete(name);
}
- // BAD: uses GET request when updating a database with `@UpdateProvider`
+ // BAD: uses GET request when updating a database with MyBatis `@UpdateProvider`
@GetMapping(value = "badUpdate")
public void badUpdate(@RequestParam String name) { // $ hasCsrfUnprotectedRequestType
myBatisService.badUpdate(name);
}
- // BAD: uses GET request when updating a database with `@InsertProvider`
+ // BAD: uses GET request when updating a database with MyBatis `@InsertProvider`
@GetMapping(value = "badInsert")
public void badInsert(@RequestParam String name) { // $ hasCsrfUnprotectedRequestType
myBatisService.badInsert(name);
}
+
+ // BAD: uses GET request when updating a database with MyBatis `@Delete`
+ @GetMapping(value = "bad8")
+ public void bad8(@RequestParam int id) { // $ hasCsrfUnprotectedRequestType
+ myBatisService.bad8(id);
+ }
+
+ // BAD: uses GET request when updating a database with MyBatis `@Insert`
+ @GetMapping(value = "bad9")
+ public void bad9(@RequestParam String user) { // $ hasCsrfUnprotectedRequestType
+ myBatisService.bad9(user);
+ }
+
+ // BAD: uses GET request when updating a database with MyBatis `@Update`
+ @GetMapping(value = "bad10")
+ public void bad10(@RequestParam String user) { // $ hasCsrfUnprotectedRequestType
+ myBatisService.bad10(user);
+ }
}
diff --git a/java/ql/test/query-tests/security/CWE-352/MyBatisMapper.java b/java/ql/test/query-tests/security/CWE-352/MyBatisMapper.java
index a49e9f997ff2..bf0b9cd5890a 100644
--- a/java/ql/test/query-tests/security/CWE-352/MyBatisMapper.java
+++ b/java/ql/test/query-tests/security/CWE-352/MyBatisMapper.java
@@ -3,6 +3,9 @@
import org.apache.ibatis.annotations.DeleteProvider;
import org.apache.ibatis.annotations.UpdateProvider;
import org.apache.ibatis.annotations.InsertProvider;
+import org.apache.ibatis.annotations.Delete;
+import org.apache.ibatis.annotations.Update;
+import org.apache.ibatis.annotations.Insert;
@Mapper
@Repository
@@ -28,4 +31,13 @@ public interface MyBatisMapper {
method = "badInsert"
)
void badInsert(String input);
+
+ @Delete("DELETE FROM users WHERE id = #{id}")
+ boolean bad8(int id);
+
+ @Insert("INSERT INTO users (id, name) VALUES(#{id}, #{name})")
+ void bad9(String user);
+
+ @Update("UPDATE users SET name = #{name} WHERE id = #{id}")
+ boolean bad10(String user);
}
diff --git a/java/ql/test/query-tests/security/CWE-352/MyBatisService.java b/java/ql/test/query-tests/security/CWE-352/MyBatisService.java
index 78ea02399b2b..20c6c3578b0b 100644
--- a/java/ql/test/query-tests/security/CWE-352/MyBatisService.java
+++ b/java/ql/test/query-tests/security/CWE-352/MyBatisService.java
@@ -22,4 +22,16 @@ public void badUpdate(String input) {
public void badInsert(String input) {
myBatisMapper.badInsert(input);
}
+
+ public void bad8(int id){
+ myBatisMapper.bad8(id);
+ }
+
+ public void bad9(String user){
+ myBatisMapper.bad9(user);
+ }
+
+ public void bad10(String user){
+ myBatisMapper.bad10(user);
+ }
}
diff --git a/java/ql/test/stubs/org.mybatis-3.5.4/org/apache/ibatis/annotations/Delete.java b/java/ql/test/stubs/org.mybatis-3.5.4/org/apache/ibatis/annotations/Delete.java
new file mode 100644
index 000000000000..e2681e31d959
--- /dev/null
+++ b/java/ql/test/stubs/org.mybatis-3.5.4/org/apache/ibatis/annotations/Delete.java
@@ -0,0 +1,14 @@
+package org.apache.ibatis.annotations;
+
+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;
+
+@Documented
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.METHOD})
+public @interface Delete {
+ String[] value();
+}
diff --git a/java/ql/test/stubs/org.mybatis-3.5.4/org/apache/ibatis/annotations/Insert.java b/java/ql/test/stubs/org.mybatis-3.5.4/org/apache/ibatis/annotations/Insert.java
new file mode 100644
index 000000000000..52aad4172d19
--- /dev/null
+++ b/java/ql/test/stubs/org.mybatis-3.5.4/org/apache/ibatis/annotations/Insert.java
@@ -0,0 +1,14 @@
+package org.apache.ibatis.annotations;
+
+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;
+
+@Documented
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.METHOD})
+public @interface Insert {
+ String[] value();
+}
diff --git a/java/ql/test/stubs/org.mybatis-3.5.4/org/apache/ibatis/annotations/Update.java b/java/ql/test/stubs/org.mybatis-3.5.4/org/apache/ibatis/annotations/Update.java
new file mode 100644
index 000000000000..6566245971af
--- /dev/null
+++ b/java/ql/test/stubs/org.mybatis-3.5.4/org/apache/ibatis/annotations/Update.java
@@ -0,0 +1,14 @@
+package org.apache.ibatis.annotations;
+
+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;
+
+@Documented
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.METHOD})
+public @interface Update {
+ String[] value();
+}
From 0f39011122c91e3d46bc3e129ccad87c39f76fd2 Mon Sep 17 00:00:00 2001
From: Jami Cogswell
Date: Thu, 12 Dec 2024 16:19:36 -0500
Subject: [PATCH 11/38] Java: add taint-tracking config for execute to exclude
FPs from non-update queries like select
---
.../CsrfUnprotectedRequestTypeQuery.qll | 56 ++++++++++++++-----
.../CWE/CWE-352/CsrfUnprotectedRequestType.ql | 4 +-
.../CsrfUnprotectedRequestTypeTest.java | 38 +++++++++++++
.../CWE-352/CsrfUnprotectedRequestTypeTest.ql | 2 +-
4 files changed, 84 insertions(+), 16 deletions(-)
diff --git a/java/ql/lib/semmle/code/java/security/CsrfUnprotectedRequestTypeQuery.qll b/java/ql/lib/semmle/code/java/security/CsrfUnprotectedRequestTypeQuery.qll
index 633a352785a5..8175c03123b1 100644
--- a/java/ql/lib/semmle/code/java/security/CsrfUnprotectedRequestTypeQuery.qll
+++ b/java/ql/lib/semmle/code/java/security/CsrfUnprotectedRequestTypeQuery.qll
@@ -4,9 +4,9 @@ import java
private import semmle.code.java.frameworks.spring.SpringController
private import semmle.code.java.frameworks.MyBatis
private import semmle.code.java.frameworks.Jdbc
-private import semmle.code.java.dataflow.DataFlow
private import semmle.code.java.dataflow.ExternalFlow
private import semmle.code.java.dispatch.VirtualDispatch
+private import semmle.code.java.dataflow.TaintTracking
/** A method that is not protected from CSRF by default. */
abstract class CsrfUnprotectedMethod extends Method { }
@@ -66,10 +66,9 @@ private class PreparedStatementDatabaseUpdateMethod extends DatabaseUpdateMethod
}
}
-/** A method that updates a SQL database. */
-private class SqlDatabaseUpdateMethod extends DatabaseUpdateMethod {
- SqlDatabaseUpdateMethod() {
- // TODO: constrain to only insert/update/delete for `execute%` methods; need to track the sql expression into the execute call.
+/** A method found via the sql-injection models which may update a SQL database. */
+private class SqlInjectionMethod extends DatabaseUpdateMethod {
+ SqlInjectionMethod() {
exists(DataFlow::Node n | this = n.asExpr().(Argument).getCall().getCallee() |
sinkNode(n, "sql-injection") and
// do not include `executeQuery` since it is typically used with a select statement
@@ -81,12 +80,33 @@ private class SqlDatabaseUpdateMethod extends DatabaseUpdateMethod {
}
}
+/**
+ * A taint-tracking configuration for reasoning about SQL queries that update a database.
+ */
+module SqlExecuteConfig implements DataFlow::ConfigSig {
+ predicate isSource(DataFlow::Node source) {
+ exists(StringLiteral sl | source.asExpr() = sl |
+ sl.getValue().regexpMatch("^(?i)(insert|update|delete).*")
+ )
+ }
+
+ predicate isSink(DataFlow::Node sink) {
+ exists(Method m | m = sink.asExpr().(Argument).getCall().getCallee() |
+ m instanceof SqlInjectionMethod and
+ m.hasName("execute")
+ )
+ }
+}
+
+/** Tracks flow from SQL queries that update a database to the argument of an execute method call. */
+module SqlExecuteFlow = TaintTracking::Global;
+
module CallGraph {
- newtype TPathNode =
+ newtype TCallPathNode =
TMethod(Method m) or
TCall(Call c)
- class PathNode extends TPathNode {
+ class CallPathNode extends TCallPathNode {
Method asMethod() { this = TMethod(result) }
Call asCall() { this = TCall(result) }
@@ -97,16 +117,16 @@ module CallGraph {
result = this.asCall().toString()
}
- private PathNode getACallee() {
+ private CallPathNode getACallee() {
[viableCallable(this.asCall()), this.asCall().getCallee()] = result.asMethod()
}
- PathNode getASuccessor() {
+ CallPathNode getASuccessor() {
this.asMethod() = result.asCall().getEnclosingCallable()
or
result = this.getACallee() and
(
- exists(PathNode p |
+ exists(CallPathNode p |
p = this.getACallee() and
p.asMethod() instanceof DatabaseUpdateMethod
)
@@ -122,15 +142,25 @@ module CallGraph {
}
}
- predicate edges(PathNode pred, PathNode succ) { pred.getASuccessor() = succ }
+ predicate edges(CallPathNode pred, CallPathNode succ) { pred.getASuccessor() = succ }
}
import CallGraph
/** Holds if `src` is an unprotected request handler that reaches a state-changing `sink`. */
-predicate unprotectedStateChange(PathNode src, PathNode sink, PathNode sinkPred) {
+predicate unprotectedStateChange(CallPathNode src, CallPathNode sink, CallPathNode sinkPred) {
src.asMethod() instanceof CsrfUnprotectedMethod and
sink.asMethod() instanceof DatabaseUpdateMethod and
sinkPred.getASuccessor() = sink and
- src.getASuccessor+() = sinkPred
+ src.getASuccessor+() = sinkPred and
+ if
+ sink.asMethod() instanceof SqlInjectionMethod and
+ sink.asMethod().hasName("execute")
+ then
+ exists(SqlExecuteFlow::PathNode executeSrc, SqlExecuteFlow::PathNode executeSink |
+ SqlExecuteFlow::flowPath(executeSrc, executeSink)
+ |
+ sinkPred.asCall() = executeSink.getNode().asExpr().(Argument).getCall()
+ )
+ else any()
}
diff --git a/java/ql/src/Security/CWE/CWE-352/CsrfUnprotectedRequestType.ql b/java/ql/src/Security/CWE/CWE-352/CsrfUnprotectedRequestType.ql
index 61612198c9eb..db19c6efc852 100644
--- a/java/ql/src/Security/CWE/CWE-352/CsrfUnprotectedRequestType.ql
+++ b/java/ql/src/Security/CWE/CWE-352/CsrfUnprotectedRequestType.ql
@@ -15,9 +15,9 @@
import java
import semmle.code.java.security.CsrfUnprotectedRequestTypeQuery
-query predicate edges(PathNode pred, PathNode succ) { CallGraph::edges(pred, succ) }
+query predicate edges(CallPathNode pred, CallPathNode succ) { CallGraph::edges(pred, succ) }
-from PathNode source, PathNode reachable, PathNode callsReachable
+from CallPathNode source, CallPathNode reachable, CallPathNode callsReachable
where unprotectedStateChange(source, reachable, callsReachable)
select source.asMethod(), source, callsReachable,
"Potential CSRF vulnerability due to using an HTTP request type which is not default-protected from CSRF for an apparent $@.",
diff --git a/java/ql/test/query-tests/security/CWE-352/CsrfUnprotectedRequestTypeTest.java b/java/ql/test/query-tests/security/CWE-352/CsrfUnprotectedRequestTypeTest.java
index 16aab568b7d7..ec2732d6aa04 100644
--- a/java/ql/test/query-tests/security/CWE-352/CsrfUnprotectedRequestTypeTest.java
+++ b/java/ql/test/query-tests/security/CWE-352/CsrfUnprotectedRequestTypeTest.java
@@ -10,9 +10,13 @@
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
+import java.sql.Connection;
+import java.sql.ResultSet;
+import java.sql.Statement;
@Controller
public class CsrfUnprotectedRequestTypeTest {
+ public static Connection connection;
// Test Spring sources with `PreparedStatement.executeUpdate()` as a default database update method call
@@ -129,6 +133,40 @@ public void bad6() { // $ hasCsrfUnprotectedRequestType
} catch (SQLException e) { }
}
+ @RequestMapping("/")
+ public void badStatementExecuteUpdate() { // $ hasCsrfUnprotectedRequestType
+ try {
+ String item = "item";
+ String price = "price";
+ Statement statement = connection.createStatement();
+ String query = "UPDATE PRODUCT SET PRICE='" + price + "' WHERE ITEM='" + item + "'";
+ int count = statement.executeUpdate(query);
+ } catch (SQLException e) { }
+ }
+
+ @RequestMapping("/")
+ public void badStatementExecute() { // $ hasCsrfUnprotectedRequestType
+ try {
+ String item = "item";
+ String price = "price";
+ Statement statement = connection.createStatement();
+ String query = "UPDATE PRODUCT SET PRICE='" + price + "' WHERE ITEM='" + item + "'";
+ boolean bool = statement.execute(query);
+ } catch (SQLException e) { }
+ }
+
+ // GOOD: select not insert/update/delete
+ @RequestMapping("/")
+ public void goodStatementExecute() {
+ try {
+ String category = "category";
+ Statement statement = connection.createStatement();
+ String query = "SELECT ITEM,PRICE FROM PRODUCT WHERE ITEM_CATEGORY='"
+ + category + "' ORDER BY PRICE";
+ boolean bool = statement.execute(query);
+ } catch (SQLException e) { }
+ }
+
@Autowired
private MyBatisService myBatisService;
diff --git a/java/ql/test/query-tests/security/CWE-352/CsrfUnprotectedRequestTypeTest.ql b/java/ql/test/query-tests/security/CWE-352/CsrfUnprotectedRequestTypeTest.ql
index f7e5f0c9db87..94ecf55516e8 100644
--- a/java/ql/test/query-tests/security/CWE-352/CsrfUnprotectedRequestTypeTest.ql
+++ b/java/ql/test/query-tests/security/CWE-352/CsrfUnprotectedRequestTypeTest.ql
@@ -7,7 +7,7 @@ module CsrfUnprotectedRequestTypeTest implements TestSig {
predicate hasActualResult(Location location, string element, string tag, string value) {
tag = "hasCsrfUnprotectedRequestType" and
- exists(PathNode src, PathNode sink, PathNode sinkPred |
+ exists(CallPathNode src, CallPathNode sink, CallPathNode sinkPred |
unprotectedStateChange(src, sink, sinkPred)
|
src.getLocation() = location and
From 286c655264233f2f661d1c9a41bb23b36277b296 Mon Sep 17 00:00:00 2001
From: Jami Cogswell
Date: Fri, 13 Dec 2024 16:33:14 -0500
Subject: [PATCH 12/38] Java: add class for Stapler web methods that are not
default-protected from CSRF
---
.../code/java/frameworks/stapler/Stapler.qll | 37 +++++++++++++++++++
.../CsrfUnprotectedRequestTypeQuery.qll | 16 ++++++++
2 files changed, 53 insertions(+)
diff --git a/java/ql/lib/semmle/code/java/frameworks/stapler/Stapler.qll b/java/ql/lib/semmle/code/java/frameworks/stapler/Stapler.qll
index f17090ed307e..599a08094dd4 100644
--- a/java/ql/lib/semmle/code/java/frameworks/stapler/Stapler.qll
+++ b/java/ql/lib/semmle/code/java/frameworks/stapler/Stapler.qll
@@ -122,3 +122,40 @@ private class PostConstructDataBoundMethod extends Method {
this.getAnAnnotation() instanceof PostConstructAnnotation
}
}
+
+/**
+ * A method intended for Stapler request routing.
+ *
+ * From: https://www.jenkins.io/doc/developer/handling-requests/actions/
+ * Web methods need to provide some indication that they are intended for Stapler routing:
+ * - Any applicable annotation recognized by Stapler, e.g., @RequirePOST.
+ * - Any inferable parameter type, e.g., StaplerRequest.
+ * - Any applicable parameter annotation, recognized by Stapler, e.g., @AncestorInPath.
+ * - Any declared exception type implementing HttpResponse, e.g., HttpResponseException.
+ * - A return type implementing HttpResponse.
+ */
+class StaplerWebMethod extends Method {
+ StaplerWebMethod() {
+ // Any applicable annotation recognized by Stapler, e.g., @RequirePOST.
+ this.hasAnnotation("org.kohsuke.stapler", "WebMethod")
+ or
+ this.hasAnnotation("org.kohsuke.stapler.interceptor", ["RequirePOST", "RespondSuccess"])
+ or
+ this.hasAnnotation("org.kohsuke.stapler.verb", ["DELETE", "GET", "POST", "PUT"])
+ or
+ // Any inferable parameter type, e.g., StaplerRequest.
+ this.getAParamType()
+ .(RefType)
+ .hasQualifiedName("org.kohsuke.stapler", ["StaplerRequest", "StaplerRequest2"])
+ or
+ // Any applicable parameter annotation, recognized by Stapler, e.g., @AncestorInPath
+ this.getAParameter()
+ .hasAnnotation("org.kohsuke.stapler", ["AncestorInPath", "QueryParameter", "Header"])
+ or
+ // A return type implementing HttpResponse
+ this.getReturnType().(RefType).getASourceSupertype*() instanceof HttpResponse
+ or
+ // Any declared exception type implementing HttpResponse, e.g., HttpResponseException
+ this.getAThrownExceptionType().getASourceSupertype*() instanceof HttpResponse
+ }
+}
diff --git a/java/ql/lib/semmle/code/java/security/CsrfUnprotectedRequestTypeQuery.qll b/java/ql/lib/semmle/code/java/security/CsrfUnprotectedRequestTypeQuery.qll
index 8175c03123b1..a4633ae451d5 100644
--- a/java/ql/lib/semmle/code/java/security/CsrfUnprotectedRequestTypeQuery.qll
+++ b/java/ql/lib/semmle/code/java/security/CsrfUnprotectedRequestTypeQuery.qll
@@ -2,6 +2,7 @@
import java
private import semmle.code.java.frameworks.spring.SpringController
+private import semmle.code.java.frameworks.stapler.Stapler
private import semmle.code.java.frameworks.MyBatis
private import semmle.code.java.frameworks.Jdbc
private import semmle.code.java.dataflow.ExternalFlow
@@ -33,6 +34,21 @@ private class SpringCsrfUnprotectedMethod extends CsrfUnprotectedMethod instance
}
}
+/**
+ * A Stapler web method that is not protected from CSRF by default.
+ *
+ * https://www.jenkins.io/doc/developer/security/form-validation/#protecting-from-csrf
+ */
+private class StaplerCsrfUnprotectedMethod extends CsrfUnprotectedMethod instanceof StaplerWebMethod
+{
+ StaplerCsrfUnprotectedMethod() {
+ not (
+ this.hasAnnotation("org.kohsuke.stapler.interceptor", "RequirePOST") or
+ this.hasAnnotation("org.kohsuke.stapler.verb", "POST")
+ )
+ }
+}
+
/** A method that updates a database. */
abstract class DatabaseUpdateMethod extends Method { }
From 39ccde0c9dffe6d493d5b7962c2e9e4abe6f47f8 Mon Sep 17 00:00:00 2001
From: Jami Cogswell
Date: Sun, 15 Dec 2024 20:53:27 -0500
Subject: [PATCH 13/38] Java: add name-based heuristic
---
.../CsrfUnprotectedRequestTypeQuery.qll | 67 ++++++++++++++-----
.../CWE/CWE-352/CsrfUnprotectedRequestType.ql | 8 +--
.../CsrfUnprotectedRequestTypeTest.java | 6 ++
.../CWE-352/CsrfUnprotectedRequestTypeTest.ql | 4 +-
4 files changed, 62 insertions(+), 23 deletions(-)
diff --git a/java/ql/lib/semmle/code/java/security/CsrfUnprotectedRequestTypeQuery.qll b/java/ql/lib/semmle/code/java/security/CsrfUnprotectedRequestTypeQuery.qll
index a4633ae451d5..a5bd49bc62c3 100644
--- a/java/ql/lib/semmle/code/java/security/CsrfUnprotectedRequestTypeQuery.qll
+++ b/java/ql/lib/semmle/code/java/security/CsrfUnprotectedRequestTypeQuery.qll
@@ -49,6 +49,15 @@ private class StaplerCsrfUnprotectedMethod extends CsrfUnprotectedMethod instanc
}
}
+/** A method that appears to change application state based on its name. */
+private class NameStateChangeMethod extends Method {
+ NameStateChangeMethod() {
+ this.getName()
+ .regexpMatch("(^|\\w+(?=[A-Z]))((?i)post|put|patch|delete|remove|create|add|update|edit|(un|)publish|fill|move|transfer|log(out|in)|access|connect(|ion)|register|submit)($|(?![a-z])\\w+)") and
+ not this.getName().regexpMatch("^(get|show|view|list|query|find)(?![a-z])\\w*")
+ }
+}
+
/** A method that updates a database. */
abstract class DatabaseUpdateMethod extends Method { }
@@ -163,20 +172,46 @@ module CallGraph {
import CallGraph
-/** Holds if `src` is an unprotected request handler that reaches a state-changing `sink`. */
-predicate unprotectedStateChange(CallPathNode src, CallPathNode sink, CallPathNode sinkPred) {
- src.asMethod() instanceof CsrfUnprotectedMethod and
- sink.asMethod() instanceof DatabaseUpdateMethod and
- sinkPred.getASuccessor() = sink and
- src.getASuccessor+() = sinkPred and
- if
- sink.asMethod() instanceof SqlInjectionMethod and
- sink.asMethod().hasName("execute")
- then
- exists(SqlExecuteFlow::PathNode executeSrc, SqlExecuteFlow::PathNode executeSink |
- SqlExecuteFlow::flowPath(executeSrc, executeSink)
- |
- sinkPred.asCall() = executeSink.getNode().asExpr().(Argument).getCall()
- )
- else any()
+/**
+ * Holds if `src` is an unprotected request handler that reaches a
+ * `sink` that updates a database.
+ */
+predicate unprotectedDatabaseUpdate(CallPathNode sourceMethod, CallPathNode sinkMethodCall) {
+ sourceMethod.asMethod() instanceof CsrfUnprotectedMethod and
+ exists(CallPathNode sinkMethod |
+ sinkMethod.asMethod() instanceof DatabaseUpdateMethod and
+ sinkMethodCall.getASuccessor() = sinkMethod and
+ sourceMethod.getASuccessor+() = sinkMethodCall and
+ if
+ sinkMethod.asMethod() instanceof SqlInjectionMethod and
+ sinkMethod.asMethod().hasName("execute")
+ then
+ exists(SqlExecuteFlow::PathNode executeSrc, SqlExecuteFlow::PathNode executeSink |
+ SqlExecuteFlow::flowPath(executeSrc, executeSink)
+ |
+ sinkMethodCall.asCall() = executeSink.getNode().asExpr().(Argument).getCall()
+ )
+ else any()
+ )
+}
+
+/**
+ * Holds if `src` is an unprotected request handler that appears to
+ * change application state based on its name.
+ */
+private predicate unprotectedHeuristicStateChange(CallPathNode sourceMethod, CallPathNode sinkMethod) {
+ sourceMethod.asMethod() instanceof CsrfUnprotectedMethod and
+ sinkMethod.asMethod() instanceof NameStateChangeMethod and
+ sinkMethod = sourceMethod and
+ // exclude any alerts that update a database
+ not unprotectedDatabaseUpdate(sourceMethod, _)
+}
+
+/**
+ * Holds if `src` is an unprotected request handler that may
+ * change an application's state.
+ */
+predicate unprotectedStateChange(CallPathNode source, CallPathNode sink) {
+ unprotectedDatabaseUpdate(source, sink) or
+ unprotectedHeuristicStateChange(source, sink)
}
diff --git a/java/ql/src/Security/CWE/CWE-352/CsrfUnprotectedRequestType.ql b/java/ql/src/Security/CWE/CWE-352/CsrfUnprotectedRequestType.ql
index db19c6efc852..9caf29030077 100644
--- a/java/ql/src/Security/CWE/CWE-352/CsrfUnprotectedRequestType.ql
+++ b/java/ql/src/Security/CWE/CWE-352/CsrfUnprotectedRequestType.ql
@@ -17,8 +17,8 @@ import semmle.code.java.security.CsrfUnprotectedRequestTypeQuery
query predicate edges(CallPathNode pred, CallPathNode succ) { CallGraph::edges(pred, succ) }
-from CallPathNode source, CallPathNode reachable, CallPathNode callsReachable
-where unprotectedStateChange(source, reachable, callsReachable)
-select source.asMethod(), source, callsReachable,
+from CallPathNode source, CallPathNode sink
+where unprotectedStateChange(source, sink)
+select source.asMethod(), source, sink,
"Potential CSRF vulnerability due to using an HTTP request type which is not default-protected from CSRF for an apparent $@.",
- callsReachable, "state-changing action"
+ sink, "state-changing action"
diff --git a/java/ql/test/query-tests/security/CWE-352/CsrfUnprotectedRequestTypeTest.java b/java/ql/test/query-tests/security/CWE-352/CsrfUnprotectedRequestTypeTest.java
index ec2732d6aa04..cc581cfb222b 100644
--- a/java/ql/test/query-tests/security/CWE-352/CsrfUnprotectedRequestTypeTest.java
+++ b/java/ql/test/query-tests/security/CWE-352/CsrfUnprotectedRequestTypeTest.java
@@ -211,4 +211,10 @@ public void bad9(@RequestParam String user) { // $ hasCsrfUnprotectedRequestType
public void bad10(@RequestParam String user) { // $ hasCsrfUnprotectedRequestType
myBatisService.bad10(user);
}
+
+ // BAD: method name implies a state-change
+ @GetMapping(value = "delete")
+ public String delete(@RequestParam String user) { // $ hasCsrfUnprotectedRequestType
+ return "delete";
+ }
}
diff --git a/java/ql/test/query-tests/security/CWE-352/CsrfUnprotectedRequestTypeTest.ql b/java/ql/test/query-tests/security/CWE-352/CsrfUnprotectedRequestTypeTest.ql
index 94ecf55516e8..6da418767ee7 100644
--- a/java/ql/test/query-tests/security/CWE-352/CsrfUnprotectedRequestTypeTest.ql
+++ b/java/ql/test/query-tests/security/CWE-352/CsrfUnprotectedRequestTypeTest.ql
@@ -7,9 +7,7 @@ module CsrfUnprotectedRequestTypeTest implements TestSig {
predicate hasActualResult(Location location, string element, string tag, string value) {
tag = "hasCsrfUnprotectedRequestType" and
- exists(CallPathNode src, CallPathNode sink, CallPathNode sinkPred |
- unprotectedStateChange(src, sink, sinkPred)
- |
+ exists(CallPathNode src, CallPathNode sink | unprotectedStateChange(src, sink) |
src.getLocation() = location and
element = src.toString() and
value = ""
From c9ad15cc8361c66c0a52a0078ec674e9ba84ee33 Mon Sep 17 00:00:00 2001
From: Jami Cogswell
Date: Sun, 15 Dec 2024 23:00:02 -0500
Subject: [PATCH 14/38] Java: update .expected file contents
---
.../security/CWE-352/CsrfUnprotectedRequestTypeTest.expected | 2 --
1 file changed, 2 deletions(-)
diff --git a/java/ql/test/query-tests/security/CWE-352/CsrfUnprotectedRequestTypeTest.expected b/java/ql/test/query-tests/security/CWE-352/CsrfUnprotectedRequestTypeTest.expected
index 8ec8033d086e..e69de29bb2d1 100644
--- a/java/ql/test/query-tests/security/CWE-352/CsrfUnprotectedRequestTypeTest.expected
+++ b/java/ql/test/query-tests/security/CWE-352/CsrfUnprotectedRequestTypeTest.expected
@@ -1,2 +0,0 @@
-testFailures
-failures
From 48d1fe062b9e82bbcd7351ba3c3e9f438a2de0ad Mon Sep 17 00:00:00 2001
From: Jami Cogswell
Date: Sun, 15 Dec 2024 23:08:24 -0500
Subject: [PATCH 15/38] Java: remove exists variable
---
.../code/java/security/CsrfUnprotectedRequestTypeQuery.qll | 4 +---
1 file changed, 1 insertion(+), 3 deletions(-)
diff --git a/java/ql/lib/semmle/code/java/security/CsrfUnprotectedRequestTypeQuery.qll b/java/ql/lib/semmle/code/java/security/CsrfUnprotectedRequestTypeQuery.qll
index a5bd49bc62c3..9581dc385f9d 100644
--- a/java/ql/lib/semmle/code/java/security/CsrfUnprotectedRequestTypeQuery.qll
+++ b/java/ql/lib/semmle/code/java/security/CsrfUnprotectedRequestTypeQuery.qll
@@ -186,9 +186,7 @@ predicate unprotectedDatabaseUpdate(CallPathNode sourceMethod, CallPathNode sink
sinkMethod.asMethod() instanceof SqlInjectionMethod and
sinkMethod.asMethod().hasName("execute")
then
- exists(SqlExecuteFlow::PathNode executeSrc, SqlExecuteFlow::PathNode executeSink |
- SqlExecuteFlow::flowPath(executeSrc, executeSink)
- |
+ exists(SqlExecuteFlow::PathNode executeSink | SqlExecuteFlow::flowPath(_, executeSink) |
sinkMethodCall.asCall() = executeSink.getNode().asExpr().(Argument).getCall()
)
else any()
From ede9e7864553dcd99cfc999732c5b96bf16acf1d Mon Sep 17 00:00:00 2001
From: Jami Cogswell
Date: Sun, 15 Dec 2024 23:19:05 -0500
Subject: [PATCH 16/38] Java: remove exists variable in test
---
.../security/CWE-352/CsrfUnprotectedRequestTypeTest.ql | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/java/ql/test/query-tests/security/CWE-352/CsrfUnprotectedRequestTypeTest.ql b/java/ql/test/query-tests/security/CWE-352/CsrfUnprotectedRequestTypeTest.ql
index 6da418767ee7..23ebb3af663a 100644
--- a/java/ql/test/query-tests/security/CWE-352/CsrfUnprotectedRequestTypeTest.ql
+++ b/java/ql/test/query-tests/security/CWE-352/CsrfUnprotectedRequestTypeTest.ql
@@ -7,7 +7,7 @@ module CsrfUnprotectedRequestTypeTest implements TestSig {
predicate hasActualResult(Location location, string element, string tag, string value) {
tag = "hasCsrfUnprotectedRequestType" and
- exists(CallPathNode src, CallPathNode sink | unprotectedStateChange(src, sink) |
+ exists(CallPathNode src | unprotectedStateChange(src, _) |
src.getLocation() = location and
element = src.toString() and
value = ""
From 48d55ec518327456c642537a3629eafc4c4f40c1 Mon Sep 17 00:00:00 2001
From: Jami Cogswell
Date: Mon, 16 Dec 2024 13:12:52 -0500
Subject: [PATCH 17/38] Java: performance fix
---
.../code/java/security/CsrfUnprotectedRequestTypeQuery.qll | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/java/ql/lib/semmle/code/java/security/CsrfUnprotectedRequestTypeQuery.qll b/java/ql/lib/semmle/code/java/security/CsrfUnprotectedRequestTypeQuery.qll
index 9581dc385f9d..8865fad1d431 100644
--- a/java/ql/lib/semmle/code/java/security/CsrfUnprotectedRequestTypeQuery.qll
+++ b/java/ql/lib/semmle/code/java/security/CsrfUnprotectedRequestTypeQuery.qll
@@ -180,8 +180,8 @@ predicate unprotectedDatabaseUpdate(CallPathNode sourceMethod, CallPathNode sink
sourceMethod.asMethod() instanceof CsrfUnprotectedMethod and
exists(CallPathNode sinkMethod |
sinkMethod.asMethod() instanceof DatabaseUpdateMethod and
- sinkMethodCall.getASuccessor() = sinkMethod and
- sourceMethod.getASuccessor+() = sinkMethodCall and
+ sinkMethodCall.getASuccessor() = pragma[only_bind_into](sinkMethod) and
+ sourceMethod.getASuccessor+() = pragma[only_bind_into](sinkMethodCall) and
if
sinkMethod.asMethod() instanceof SqlInjectionMethod and
sinkMethod.asMethod().hasName("execute")
From fa276897190abcbc4c7e974d4fae2a1132e8cded Mon Sep 17 00:00:00 2001
From: Jami Cogswell
Date: Mon, 16 Dec 2024 15:41:59 -0500
Subject: [PATCH 18/38] Java: update InlineExpectationsTest import for new
location
---
.../security/CWE-352/CsrfUnprotectedRequestTypeTest.ql | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/java/ql/test/query-tests/security/CWE-352/CsrfUnprotectedRequestTypeTest.ql b/java/ql/test/query-tests/security/CWE-352/CsrfUnprotectedRequestTypeTest.ql
index 23ebb3af663a..5fe26ec04670 100644
--- a/java/ql/test/query-tests/security/CWE-352/CsrfUnprotectedRequestTypeTest.ql
+++ b/java/ql/test/query-tests/security/CWE-352/CsrfUnprotectedRequestTypeTest.ql
@@ -1,6 +1,6 @@
import java
import semmle.code.java.security.CsrfUnprotectedRequestTypeQuery
-import TestUtilities.InlineExpectationsTest
+import utils.test.InlineExpectationsTest
module CsrfUnprotectedRequestTypeTest implements TestSig {
string getARelevantTag() { result = "hasCsrfUnprotectedRequestType" }
From 27aa9c97a45e4f645aafeedb3e85c280d427eab7 Mon Sep 17 00:00:00 2001
From: Jami Cogswell
Date: Mon, 16 Dec 2024 18:52:21 -0500
Subject: [PATCH 19/38] Java: add change note
---
.../change-notes/2024-12-16-csrf-unprotected-request-type.md | 4 ++++
1 file changed, 4 insertions(+)
create mode 100644 java/ql/src/change-notes/2024-12-16-csrf-unprotected-request-type.md
diff --git a/java/ql/src/change-notes/2024-12-16-csrf-unprotected-request-type.md b/java/ql/src/change-notes/2024-12-16-csrf-unprotected-request-type.md
new file mode 100644
index 000000000000..13dea758e8b4
--- /dev/null
+++ b/java/ql/src/change-notes/2024-12-16-csrf-unprotected-request-type.md
@@ -0,0 +1,4 @@
+---
+category: newQuery
+---
+* Added a new query, `java/csrf-unprotected-request-type`, to detect Cross-Site Request Forgery (CSRF) vulnerabilities due to using HTTP request types that are not default-protected from CSRF.
From 26b7c1a57251449965ae4ccb222882d17419acaf Mon Sep 17 00:00:00 2001
From: Jami Cogswell
Date: Mon, 16 Dec 2024 19:20:25 -0500
Subject: [PATCH 20/38] Java: qldocs for CallGraph module
---
.../java/security/CsrfUnprotectedRequestTypeQuery.qll | 10 +++++++++-
1 file changed, 9 insertions(+), 1 deletion(-)
diff --git a/java/ql/lib/semmle/code/java/security/CsrfUnprotectedRequestTypeQuery.qll b/java/ql/lib/semmle/code/java/security/CsrfUnprotectedRequestTypeQuery.qll
index 8865fad1d431..c89e6c442bff 100644
--- a/java/ql/lib/semmle/code/java/security/CsrfUnprotectedRequestTypeQuery.qll
+++ b/java/ql/lib/semmle/code/java/security/CsrfUnprotectedRequestTypeQuery.qll
@@ -126,16 +126,21 @@ module SqlExecuteConfig implements DataFlow::ConfigSig {
/** Tracks flow from SQL queries that update a database to the argument of an execute method call. */
module SqlExecuteFlow = TaintTracking::Global;
+/** Provides classes and predicates representing call paths. */
module CallGraph {
- newtype TCallPathNode =
+ private newtype TCallPathNode =
TMethod(Method m) or
TCall(Call c)
+ /** A node in a call path graph */
class CallPathNode extends TCallPathNode {
+ /** Gets the method corresponding to this `CallPathNode`, if any. */
Method asMethod() { this = TMethod(result) }
+ /** Gets the call corresponding to this `CallPathNode`, if any. */
Call asCall() { this = TCall(result) }
+ /** Gets the string representation of this `CallPathNode`. */
string toString() {
result = this.asMethod().toString()
or
@@ -146,6 +151,7 @@ module CallGraph {
[viableCallable(this.asCall()), this.asCall().getCallee()] = result.asMethod()
}
+ /** Gets a successor node of this `CallPathNode`, if any. */
CallPathNode getASuccessor() {
this.asMethod() = result.asCall().getEnclosingCallable()
or
@@ -160,6 +166,7 @@ module CallGraph {
)
}
+ /** Gets the location of this `CallPathNode`. */
Location getLocation() {
result = this.asMethod().getLocation()
or
@@ -167,6 +174,7 @@ module CallGraph {
}
}
+ /** Holds if `pred` has a successor node `succ`. */
predicate edges(CallPathNode pred, CallPathNode succ) { pred.getASuccessor() = succ }
}
From 3bf6dc24c12109869972f74b7b9c6bc4acad9665 Mon Sep 17 00:00:00 2001
From: Jami Cogswell
Date: Wed, 18 Dec 2024 10:52:33 -0500
Subject: [PATCH 21/38] Java: Stapler tests and stubs
---
.../CsrfUnprotectedRequestTypeTest.java | 71 +++++++++++++++++++
.../test/query-tests/security/CWE-352/options | 2 +-
.../kohsuke/stapler/AnnotationHandler.java | 13 ++++
.../org/kohsuke/stapler/QueryParameter.java | 28 ++++++++
.../org/kohsuke/stapler/WebMethod.java | 18 +++++
.../stapler/interceptor/Interceptor.java | 15 ++++
.../interceptor/InterceptorAnnotation.java | 21 ++++++
.../stapler/interceptor/RequirePOST.java | 25 +++++++
.../kohsuke/stapler/interceptor/Stage.java | 10 +++
.../org/kohsuke/stapler/verb/GET.java | 19 +++++
.../org/kohsuke/stapler/verb/POST.java | 19 +++++
.../org/kohsuke/stapler/verb/PUT.java | 19 +++++
12 files changed, 259 insertions(+), 1 deletion(-)
create mode 100644 java/ql/test/stubs/stapler-1.263/org/kohsuke/stapler/AnnotationHandler.java
create mode 100644 java/ql/test/stubs/stapler-1.263/org/kohsuke/stapler/QueryParameter.java
create mode 100644 java/ql/test/stubs/stapler-1.263/org/kohsuke/stapler/WebMethod.java
create mode 100644 java/ql/test/stubs/stapler-1.263/org/kohsuke/stapler/interceptor/Interceptor.java
create mode 100644 java/ql/test/stubs/stapler-1.263/org/kohsuke/stapler/interceptor/InterceptorAnnotation.java
create mode 100644 java/ql/test/stubs/stapler-1.263/org/kohsuke/stapler/interceptor/RequirePOST.java
create mode 100644 java/ql/test/stubs/stapler-1.263/org/kohsuke/stapler/interceptor/Stage.java
create mode 100644 java/ql/test/stubs/stapler-1.263/org/kohsuke/stapler/verb/GET.java
create mode 100644 java/ql/test/stubs/stapler-1.263/org/kohsuke/stapler/verb/POST.java
create mode 100644 java/ql/test/stubs/stapler-1.263/org/kohsuke/stapler/verb/PUT.java
diff --git a/java/ql/test/query-tests/security/CWE-352/CsrfUnprotectedRequestTypeTest.java b/java/ql/test/query-tests/security/CWE-352/CsrfUnprotectedRequestTypeTest.java
index cc581cfb222b..cd0bfe26a641 100644
--- a/java/ql/test/query-tests/security/CWE-352/CsrfUnprotectedRequestTypeTest.java
+++ b/java/ql/test/query-tests/security/CWE-352/CsrfUnprotectedRequestTypeTest.java
@@ -13,6 +13,15 @@
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.Statement;
+import org.kohsuke.stapler.WebMethod;
+import org.kohsuke.stapler.interceptor.RequirePOST;
+import org.kohsuke.stapler.verb.POST;
+import org.kohsuke.stapler.verb.GET;
+import org.kohsuke.stapler.verb.PUT;
+import org.kohsuke.stapler.StaplerRequest;
+import org.kohsuke.stapler.QueryParameter;
+import org.kohsuke.stapler.HttpRedirect;
+import org.kohsuke.stapler.HttpResponses;
@Controller
public class CsrfUnprotectedRequestTypeTest {
@@ -212,9 +221,71 @@ public void bad10(@RequestParam String user) { // $ hasCsrfUnprotectedRequestTyp
myBatisService.bad10(user);
}
+ // Test name-based heuristic
+
// BAD: method name implies a state-change
@GetMapping(value = "delete")
public String delete(@RequestParam String user) { // $ hasCsrfUnprotectedRequestType
return "delete";
}
+
+ // Test Stapler web methods with name-based heuristic
+
+ // BAD: Stapler web method annotated with `@WebMethod` and method name that implies a state-change
+ @WebMethod(name = "post")
+ public String doPost(String user) { // $ hasCsrfUnprotectedRequestType
+ return "post";
+ }
+
+ // GOOD: nothing to indicate that this is a Stapler web method
+ public String postNotAWebMethod(String user) {
+ return "post";
+ }
+
+ // GOOD: Stapler web method annotated with `@RequirePOST` and method name that implies a state-change
+ @RequirePOST
+ public String doPost1(String user) {
+ return "post";
+ }
+
+ // GOOD: Stapler web method annotated with `@POST` and method name that implies a state-change
+ @POST
+ public String doPost2(String user) {
+ return "post";
+ }
+
+ // BAD: Stapler web method annotated with `@GET` and method name that implies a state-change
+ @GET
+ public String doPost3(String user) { // $ hasCsrfUnprotectedRequestType
+ return "post";
+ }
+
+ // BAD: Stapler web method annotated with `@PUT` and method name that implies a state-change
+ // We treat this case as bad for Stapler since the Jenkins docs only say that @POST/@RequirePOST
+ // provide default protection against CSRF.
+ @PUT
+ public String doPut(String user) { // $ hasCsrfUnprotectedRequestType
+ return "put";
+ }
+
+ // BAD: Stapler web method parameter of type `StaplerRequest` and method name that implies a state-change
+ public String doPost4(StaplerRequest request) { // $ hasCsrfUnprotectedRequestType
+ return "post";
+ }
+
+ // BAD: Stapler web method parameter annotated with `@QueryParameter` and method name that implies a state-change
+ public String doPost5(@QueryParameter(value="user", fixEmpty=false, required=false) String user) { // $ hasCsrfUnprotectedRequestType
+ return "post";
+ }
+
+ // BAD: Stapler web method with declared exception type implementing HttpResponse and method name that implies a state-change
+ public String doPost6(String user) throws HttpResponses.HttpResponseException { // $ hasCsrfUnprotectedRequestType
+ return "post";
+ }
+
+ // BAD: Stapler web method with return type implementing HttpResponse and method name that implies a state-change
+ public HttpRedirect doPost7(String url) { // $ hasCsrfUnprotectedRequestType
+ HttpRedirect redirect = new HttpRedirect(url);
+ return redirect;
+ }
}
diff --git a/java/ql/test/query-tests/security/CWE-352/options b/java/ql/test/query-tests/security/CWE-352/options
index 339a048c3200..4a81432ec3e4 100644
--- a/java/ql/test/query-tests/security/CWE-352/options
+++ b/java/ql/test/query-tests/security/CWE-352/options
@@ -1 +1 @@
-semmle-extractor-options: --javac-args -cp ${testdir}/../../../stubs/springframework-5.3.8/:${testdir}/../../../stubs/org.mybatis-3.5.4/
+//semmle-extractor-options: --javac-args -cp ${testdir}/../../../stubs/springframework-5.3.8/:${testdir}/../../../stubs/org.mybatis-3.5.4/:${testdir}/../../../stubs/stapler-1.263/:${testdir}/../../../stubs/javax-servlet-2.5:${testdir}/../../../stubs/apache-commons-jelly-1.0.1:${testdir}/../../../stubs/apache-commons-fileupload-1.4:${testdir}/../../../stubs/saxon-xqj-9.x:${testdir}/../../../stubs/apache-commons-beanutils:${testdir}/../../../stubs/dom4j-2.1.1:${testdir}/../../../stubs/apache-commons-lang:${testdir}/../../../stubs/jaxen-1.2.0
diff --git a/java/ql/test/stubs/stapler-1.263/org/kohsuke/stapler/AnnotationHandler.java b/java/ql/test/stubs/stapler-1.263/org/kohsuke/stapler/AnnotationHandler.java
new file mode 100644
index 000000000000..014e5787bd46
--- /dev/null
+++ b/java/ql/test/stubs/stapler-1.263/org/kohsuke/stapler/AnnotationHandler.java
@@ -0,0 +1,13 @@
+// Generated automatically from org.kohsuke.stapler.AnnotationHandler for testing purposes
+
+package org.kohsuke.stapler;
+
+import java.lang.annotation.Annotation;
+import org.kohsuke.stapler.StaplerRequest;
+
+abstract public class AnnotationHandler
+{
+ protected final Object convert(Class p0, String p1){ return null; }
+ public AnnotationHandler(){}
+ public abstract Object parse(StaplerRequest p0, T p1, Class p2, String p3);
+}
diff --git a/java/ql/test/stubs/stapler-1.263/org/kohsuke/stapler/QueryParameter.java b/java/ql/test/stubs/stapler-1.263/org/kohsuke/stapler/QueryParameter.java
new file mode 100644
index 000000000000..5ffe3a8fdf5a
--- /dev/null
+++ b/java/ql/test/stubs/stapler-1.263/org/kohsuke/stapler/QueryParameter.java
@@ -0,0 +1,28 @@
+// Generated automatically from org.kohsuke.stapler.QueryParameter for testing purposes
+
+package org.kohsuke.stapler;
+
+import java.lang.annotation.Annotation;
+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;
+import org.kohsuke.stapler.AnnotationHandler;
+import org.kohsuke.stapler.InjectedParameter;
+import org.kohsuke.stapler.StaplerRequest;
+
+@Documented
+@Retention(value=java.lang.annotation.RetentionPolicy.RUNTIME)
+@Target(value={java.lang.annotation.ElementType.PARAMETER})
+public @interface QueryParameter
+{
+ String value();
+ boolean fixEmpty();
+ boolean required();
+ static public class HandlerImpl extends AnnotationHandler
+ {
+ public HandlerImpl(){}
+ public Object parse(StaplerRequest p0, QueryParameter p1, Class p2, String p3){ return null; }
+ }
+}
diff --git a/java/ql/test/stubs/stapler-1.263/org/kohsuke/stapler/WebMethod.java b/java/ql/test/stubs/stapler-1.263/org/kohsuke/stapler/WebMethod.java
new file mode 100644
index 000000000000..ab457f2d8244
--- /dev/null
+++ b/java/ql/test/stubs/stapler-1.263/org/kohsuke/stapler/WebMethod.java
@@ -0,0 +1,18 @@
+// Generated automatically from org.kohsuke.stapler.WebMethod for testing purposes
+
+package org.kohsuke.stapler;
+
+import java.lang.annotation.Annotation;
+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;
+
+@Documented
+@Retention(value=java.lang.annotation.RetentionPolicy.RUNTIME)
+@Target(value={java.lang.annotation.ElementType.METHOD})
+public @interface WebMethod
+{
+ String[] name();
+}
diff --git a/java/ql/test/stubs/stapler-1.263/org/kohsuke/stapler/interceptor/Interceptor.java b/java/ql/test/stubs/stapler-1.263/org/kohsuke/stapler/interceptor/Interceptor.java
new file mode 100644
index 000000000000..8148c1813fcb
--- /dev/null
+++ b/java/ql/test/stubs/stapler-1.263/org/kohsuke/stapler/interceptor/Interceptor.java
@@ -0,0 +1,15 @@
+// Generated automatically from org.kohsuke.stapler.interceptor.Interceptor for testing purposes
+
+package org.kohsuke.stapler.interceptor;
+
+import org.kohsuke.stapler.Function;
+import org.kohsuke.stapler.StaplerRequest;
+import org.kohsuke.stapler.StaplerResponse;
+
+abstract public class Interceptor
+{
+ protected Function target = null;
+ public Interceptor(){}
+ public abstract Object invoke(StaplerRequest p0, StaplerResponse p1, Object p2, Object[] p3);
+ public void setTarget(Function p0){}
+}
diff --git a/java/ql/test/stubs/stapler-1.263/org/kohsuke/stapler/interceptor/InterceptorAnnotation.java b/java/ql/test/stubs/stapler-1.263/org/kohsuke/stapler/interceptor/InterceptorAnnotation.java
new file mode 100644
index 000000000000..ca2c694b4144
--- /dev/null
+++ b/java/ql/test/stubs/stapler-1.263/org/kohsuke/stapler/interceptor/InterceptorAnnotation.java
@@ -0,0 +1,21 @@
+// Generated automatically from org.kohsuke.stapler.interceptor.InterceptorAnnotation for testing purposes
+
+package org.kohsuke.stapler.interceptor;
+
+import java.lang.annotation.Annotation;
+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;
+import org.kohsuke.stapler.interceptor.Interceptor;
+import org.kohsuke.stapler.interceptor.Stage;
+
+@Documented
+@Retention(value=java.lang.annotation.RetentionPolicy.RUNTIME)
+@Target(value={java.lang.annotation.ElementType.ANNOTATION_TYPE})
+public @interface InterceptorAnnotation
+{
+ Class extends Interceptor> value();
+ Stage stage();
+}
diff --git a/java/ql/test/stubs/stapler-1.263/org/kohsuke/stapler/interceptor/RequirePOST.java b/java/ql/test/stubs/stapler-1.263/org/kohsuke/stapler/interceptor/RequirePOST.java
new file mode 100644
index 000000000000..cda33abd4352
--- /dev/null
+++ b/java/ql/test/stubs/stapler-1.263/org/kohsuke/stapler/interceptor/RequirePOST.java
@@ -0,0 +1,25 @@
+// Generated automatically from org.kohsuke.stapler.interceptor.RequirePOST for testing purposes
+
+package org.kohsuke.stapler.interceptor;
+
+import java.lang.annotation.Annotation;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import org.kohsuke.stapler.StaplerRequest;
+import org.kohsuke.stapler.StaplerResponse;
+import org.kohsuke.stapler.interceptor.Interceptor;
+import org.kohsuke.stapler.interceptor.InterceptorAnnotation;
+import org.kohsuke.stapler.interceptor.Stage;
+
+@Retention(value=java.lang.annotation.RetentionPolicy.RUNTIME)
+@Target(value={java.lang.annotation.ElementType.METHOD,java.lang.annotation.ElementType.FIELD})
+public @interface RequirePOST
+{
+ static public class Processor extends Interceptor
+ {
+ public Object invoke(StaplerRequest p0, StaplerResponse p1, Object p2, Object[] p3){ return null; }
+ public Processor(){}
+ }
+}
diff --git a/java/ql/test/stubs/stapler-1.263/org/kohsuke/stapler/interceptor/Stage.java b/java/ql/test/stubs/stapler-1.263/org/kohsuke/stapler/interceptor/Stage.java
new file mode 100644
index 000000000000..23db10ee9b0d
--- /dev/null
+++ b/java/ql/test/stubs/stapler-1.263/org/kohsuke/stapler/interceptor/Stage.java
@@ -0,0 +1,10 @@
+// Generated automatically from org.kohsuke.stapler.interceptor.Stage for testing purposes
+
+package org.kohsuke.stapler.interceptor;
+
+
+public enum Stage
+{
+ PREINVOKE, SELECTION;
+ private Stage() {}
+}
diff --git a/java/ql/test/stubs/stapler-1.263/org/kohsuke/stapler/verb/GET.java b/java/ql/test/stubs/stapler-1.263/org/kohsuke/stapler/verb/GET.java
new file mode 100644
index 000000000000..73327d4e0f67
--- /dev/null
+++ b/java/ql/test/stubs/stapler-1.263/org/kohsuke/stapler/verb/GET.java
@@ -0,0 +1,19 @@
+// Generated automatically from org.kohsuke.stapler.verb.GET for testing purposes
+
+package org.kohsuke.stapler.verb;
+
+import java.lang.annotation.Annotation;
+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;
+import org.kohsuke.stapler.interceptor.InterceptorAnnotation;
+import org.kohsuke.stapler.interceptor.Stage;
+
+@Documented
+@Retention(value=java.lang.annotation.RetentionPolicy.RUNTIME)
+@Target(value={java.lang.annotation.ElementType.METHOD})
+public @interface GET
+{
+}
diff --git a/java/ql/test/stubs/stapler-1.263/org/kohsuke/stapler/verb/POST.java b/java/ql/test/stubs/stapler-1.263/org/kohsuke/stapler/verb/POST.java
new file mode 100644
index 000000000000..30462d6b0fae
--- /dev/null
+++ b/java/ql/test/stubs/stapler-1.263/org/kohsuke/stapler/verb/POST.java
@@ -0,0 +1,19 @@
+// Generated automatically from org.kohsuke.stapler.verb.POST for testing purposes
+
+package org.kohsuke.stapler.verb;
+
+import java.lang.annotation.Annotation;
+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;
+import org.kohsuke.stapler.interceptor.InterceptorAnnotation;
+import org.kohsuke.stapler.interceptor.Stage;
+
+@Documented
+@Retention(value=java.lang.annotation.RetentionPolicy.RUNTIME)
+@Target(value={java.lang.annotation.ElementType.METHOD})
+public @interface POST
+{
+}
diff --git a/java/ql/test/stubs/stapler-1.263/org/kohsuke/stapler/verb/PUT.java b/java/ql/test/stubs/stapler-1.263/org/kohsuke/stapler/verb/PUT.java
new file mode 100644
index 000000000000..d4d4338772c6
--- /dev/null
+++ b/java/ql/test/stubs/stapler-1.263/org/kohsuke/stapler/verb/PUT.java
@@ -0,0 +1,19 @@
+// Generated automatically from org.kohsuke.stapler.verb.PUT for testing purposes
+
+package org.kohsuke.stapler.verb;
+
+import java.lang.annotation.Annotation;
+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;
+import org.kohsuke.stapler.interceptor.InterceptorAnnotation;
+import org.kohsuke.stapler.interceptor.Stage;
+
+@Documented
+@Retention(value=java.lang.annotation.RetentionPolicy.RUNTIME)
+@Target(value={java.lang.annotation.ElementType.METHOD})
+public @interface PUT
+{
+}
From 0ab37684e1fb226a9e0115cbf88132df4be9eb01 Mon Sep 17 00:00:00 2001
From: Jami Cogswell
Date: Wed, 18 Dec 2024 17:46:08 -0500
Subject: [PATCH 22/38] Java: more database update tests and stubs
---
.../CsrfUnprotectedRequestTypeTest.java | 160 ++++++++++++++++-
.../test/query-tests/security/CWE-352/options | 2 +-
.../org/apache/ibatis/jdbc/SqlRunner.java | 37 ++++
.../NamedParameterJdbcTemplate.java | 169 ++++++++++++++++++
4 files changed, 362 insertions(+), 6 deletions(-)
create mode 100644 java/ql/test/stubs/org.mybatis-3.5.4/org/apache/ibatis/jdbc/SqlRunner.java
create mode 100644 java/ql/test/stubs/springframework-5.3.8/org/springframework/jdbc/core/namedparam/NamedParameterJdbcTemplate.java
diff --git a/java/ql/test/query-tests/security/CWE-352/CsrfUnprotectedRequestTypeTest.java b/java/ql/test/query-tests/security/CWE-352/CsrfUnprotectedRequestTypeTest.java
index cd0bfe26a641..79404e6b1052 100644
--- a/java/ql/test/query-tests/security/CWE-352/CsrfUnprotectedRequestTypeTest.java
+++ b/java/ql/test/query-tests/security/CWE-352/CsrfUnprotectedRequestTypeTest.java
@@ -22,6 +22,10 @@
import org.kohsuke.stapler.QueryParameter;
import org.kohsuke.stapler.HttpRedirect;
import org.kohsuke.stapler.HttpResponses;
+import org.apache.ibatis.jdbc.SqlRunner;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
+import java.util.Map;
@Controller
public class CsrfUnprotectedRequestTypeTest {
@@ -142,29 +146,46 @@ public void bad6() { // $ hasCsrfUnprotectedRequestType
} catch (SQLException e) { }
}
+ // BAD: allows request type not default-protected from CSRF when
+ // updating a database using `Statement.executeUpdate`
@RequestMapping("/")
public void badStatementExecuteUpdate() { // $ hasCsrfUnprotectedRequestType
try {
String item = "item";
String price = "price";
Statement statement = connection.createStatement();
- String query = "UPDATE PRODUCT SET PRICE='" + price + "' WHERE ITEM='" + item + "'";
- int count = statement.executeUpdate(query);
+ String sql = "UPDATE PRODUCT SET PRICE='" + price + "' WHERE ITEM='" + item + "'";
+ int count = statement.executeUpdate(sql);
} catch (SQLException e) { }
}
+ // BAD: allows request type not default-protected from CSRF when
+ // updating a database using `Statement.executeLargeUpdate`
+ @RequestMapping("/")
+ public void badStatementExecuteLargeUpdate() { // $ hasCsrfUnprotectedRequestType
+ try {
+ String item = "item";
+ String price = "price";
+ Statement statement = connection.createStatement();
+ String sql = "UPDATE PRODUCT SET PRICE='" + price + "' WHERE ITEM='" + item + "'";
+ long count = statement.executeLargeUpdate(sql);
+ } catch (SQLException e) { }
+ }
+
+ // BAD: allows request type not default-protected from CSRF when
+ // updating a database using `Statement.execute` with SQL UPDATE
@RequestMapping("/")
public void badStatementExecute() { // $ hasCsrfUnprotectedRequestType
try {
String item = "item";
String price = "price";
Statement statement = connection.createStatement();
- String query = "UPDATE PRODUCT SET PRICE='" + price + "' WHERE ITEM='" + item + "'";
- boolean bool = statement.execute(query);
+ String sql = "UPDATE PRODUCT SET PRICE='" + price + "' WHERE ITEM='" + item + "'";
+ boolean bool = statement.execute(sql);
} catch (SQLException e) { }
}
- // GOOD: select not insert/update/delete
+ // GOOD: does not update a database, queries with SELECT
@RequestMapping("/")
public void goodStatementExecute() {
try {
@@ -176,6 +197,135 @@ public void goodStatementExecute() {
} catch (SQLException e) { }
}
+ // BAD: allows request type not default-protected from CSRF when
+ // updating a database using `SqlRunner.insert`
+ @RequestMapping("/")
+ public void badSqlRunnerInsert() { // $ hasCsrfUnprotectedRequestType
+ try {
+ String item = "item";
+ String price = "price";
+ String sql = "INSERT PRODUCT SET PRICE='" + price + "' WHERE ITEM='" + item + "'";
+ SqlRunner sqlRunner = new SqlRunner(connection);
+ sqlRunner.insert(sql);
+ } catch (SQLException e) { }
+ }
+
+ // BAD: allows request type not default-protected from CSRF when
+ // updating a database using `SqlRunner.update`
+ @RequestMapping("/")
+ public void badSqlRunnerUpdate() { // $ hasCsrfUnprotectedRequestType
+ try {
+ String item = "item";
+ String price = "price";
+ String sql = "UPDATE PRODUCT SET PRICE='" + price + "' WHERE ITEM='" + item + "'";
+ SqlRunner sqlRunner = new SqlRunner(connection);
+ sqlRunner.update(sql);
+ } catch (SQLException e) { }
+ }
+
+ // BAD: allows request type not default-protected from CSRF when
+ // updating a database using `SqlRunner.delete`
+ @RequestMapping("/")
+ public void badSqlRunnerDelete() { // $ hasCsrfUnprotectedRequestType
+ try {
+ String item = "item";
+ String price = "price";
+ String sql = "DELETE PRODUCT SET PRICE='" + price + "' WHERE ITEM='" + item + "'";
+ SqlRunner sqlRunner = new SqlRunner(connection);
+ sqlRunner.delete(sql);
+ } catch (SQLException e) { }
+ }
+
+ // BAD: allows request type not default-protected from CSRF when
+ // updating a database using `JdbcTemplate.update`
+ @RequestMapping("/")
+ public void badJdbcTemplateUpdate() { // $ hasCsrfUnprotectedRequestType
+ String item = "item";
+ String price = "price";
+ String sql = "UPDATE PRODUCT SET PRICE='" + price + "' WHERE ITEM='" + item + "'";
+ JdbcTemplate jdbcTemplate = new JdbcTemplate();
+ jdbcTemplate.update(sql);
+ }
+
+ // BAD: allows request type not default-protected from CSRF when
+ // updating a database using `JdbcTemplate.batchUpdate`
+ @RequestMapping("/")
+ public void badJdbcTemplateBatchUpdate() { // $ hasCsrfUnprotectedRequestType
+ String item = "item";
+ String price = "price";
+ String sql = "UPDATE PRODUCT SET PRICE='" + price + "' WHERE ITEM='" + item + "'";
+ JdbcTemplate jdbcTemplate = new JdbcTemplate();
+ jdbcTemplate.batchUpdate(sql, null, null);
+ }
+
+ // BAD: allows request type not default-protected from CSRF when
+ // updating a database using `JdbcTemplate.execute`
+ @RequestMapping("/")
+ public void badJdbcTemplateExecute() { // $ hasCsrfUnprotectedRequestType
+ String item = "item";
+ String price = "price";
+ String sql = "UPDATE PRODUCT SET PRICE='" + price + "' WHERE ITEM='" + item + "'";
+ JdbcTemplate jdbcTemplate = new JdbcTemplate();
+ jdbcTemplate.execute(sql);
+ }
+
+ // GOOD: does not update a database, queries with SELECT
+ @RequestMapping("/")
+ public void goodJdbcTemplateExecute() {
+ String category = "category";
+ String query = "SELECT ITEM,PRICE FROM PRODUCT WHERE ITEM_CATEGORY='"
+ + category + "' ORDER BY PRICE";
+ JdbcTemplate jdbcTemplate = new JdbcTemplate();
+ jdbcTemplate.execute(query);
+ }
+
+ // BAD: allows request type not default-protected from CSRF when
+ // updating a database using `NamedParameterJdbcTemplate.update`
+ @RequestMapping("/")
+ public void badNamedParameterJdbcTemplateUpdate() { // $ hasCsrfUnprotectedRequestType
+ String item = "item";
+ String price = "price";
+ String sql = "UPDATE PRODUCT SET PRICE='" + price + "' WHERE ITEM='" + item + "'";
+ JdbcTemplate jdbcTemplate = new JdbcTemplate();
+ NamedParameterJdbcTemplate namedParamJdbcTemplate = new NamedParameterJdbcTemplate(jdbcTemplate);
+ namedParamJdbcTemplate.update(sql, null, null);
+ }
+
+ // BAD: allows request type not default-protected from CSRF when
+ // updating a database using `NamedParameterJdbcTemplate.batchUpdate`
+ @RequestMapping("/")
+ public void badNamedParameterJdbcTemplateBatchUpdate() { // $ hasCsrfUnprotectedRequestType
+ String item = "item";
+ String price = "price";
+ String sql = "UPDATE PRODUCT SET PRICE='" + price + "' WHERE ITEM='" + item + "'";
+ JdbcTemplate jdbcTemplate = new JdbcTemplate();
+ NamedParameterJdbcTemplate namedParamJdbcTemplate = new NamedParameterJdbcTemplate(jdbcTemplate);
+ namedParamJdbcTemplate.batchUpdate(sql, (Map[]) null);
+ }
+
+ // BAD: allows request type not default-protected from CSRF when
+ // updating a database using `NamedParameterJdbcTemplate.execute`
+ @RequestMapping("/")
+ public void badNamedParameterJdbcTemplateExecute() { // $ hasCsrfUnprotectedRequestType
+ String item = "item";
+ String price = "price";
+ String sql = "UPDATE PRODUCT SET PRICE='" + price + "' WHERE ITEM='" + item + "'";
+ JdbcTemplate jdbcTemplate = new JdbcTemplate();
+ NamedParameterJdbcTemplate namedParamJdbcTemplate = new NamedParameterJdbcTemplate(jdbcTemplate);
+ namedParamJdbcTemplate.execute(sql, null);
+ }
+
+ // GOOD: does not update a database, queries with SELECT
+ @RequestMapping("/")
+ public void goodNamedParameterJdbcTemplateExecute() {
+ String category = "category";
+ String query = "SELECT ITEM,PRICE FROM PRODUCT WHERE ITEM_CATEGORY='"
+ + category + "' ORDER BY PRICE";
+ JdbcTemplate jdbcTemplate = new JdbcTemplate();
+ NamedParameterJdbcTemplate namedParamJdbcTemplate = new NamedParameterJdbcTemplate(jdbcTemplate);
+ namedParamJdbcTemplate.execute(query, null);
+ }
+
@Autowired
private MyBatisService myBatisService;
diff --git a/java/ql/test/query-tests/security/CWE-352/options b/java/ql/test/query-tests/security/CWE-352/options
index 4a81432ec3e4..1fef01772fe9 100644
--- a/java/ql/test/query-tests/security/CWE-352/options
+++ b/java/ql/test/query-tests/security/CWE-352/options
@@ -1 +1 @@
-//semmle-extractor-options: --javac-args -cp ${testdir}/../../../stubs/springframework-5.3.8/:${testdir}/../../../stubs/org.mybatis-3.5.4/:${testdir}/../../../stubs/stapler-1.263/:${testdir}/../../../stubs/javax-servlet-2.5:${testdir}/../../../stubs/apache-commons-jelly-1.0.1:${testdir}/../../../stubs/apache-commons-fileupload-1.4:${testdir}/../../../stubs/saxon-xqj-9.x:${testdir}/../../../stubs/apache-commons-beanutils:${testdir}/../../../stubs/dom4j-2.1.1:${testdir}/../../../stubs/apache-commons-lang:${testdir}/../../../stubs/jaxen-1.2.0
+//semmle-extractor-options: --javac-args -cp ${testdir}/../../../stubs/springframework-5.3.8/:${testdir}/../../../stubs/org.mybatis-3.5.4/:${testdir}/../../../stubs/stapler-1.263/:${testdir}/../../../stubs/javax-servlet-2.5:${testdir}/../../../stubs/apache-commons-jelly-1.0.1:${testdir}/../../../stubs/apache-commons-fileupload-1.4:${testdir}/../../../stubs/saxon-xqj-9.x:${testdir}/../../../stubs/apache-commons-beanutils:${testdir}/../../../stubs/dom4j-2.1.1:${testdir}/../../../stubs/apache-commons-lang:${testdir}/../../../stubs/jaxen-1.2.0:${testdir}/../../../stubs/apache-commons-logging-1.2/
diff --git a/java/ql/test/stubs/org.mybatis-3.5.4/org/apache/ibatis/jdbc/SqlRunner.java b/java/ql/test/stubs/org.mybatis-3.5.4/org/apache/ibatis/jdbc/SqlRunner.java
new file mode 100644
index 000000000000..7738de81e0e9
--- /dev/null
+++ b/java/ql/test/stubs/org.mybatis-3.5.4/org/apache/ibatis/jdbc/SqlRunner.java
@@ -0,0 +1,37 @@
+package org.apache.ibatis.jdbc;
+
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.ResultSetMetaData;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+
+public class SqlRunner {
+
+ public static final int NO_GENERATED_KEY = Integer.MIN_VALUE + 1001;
+
+ private final Connection connection;
+ private boolean useGeneratedKeySupport;
+
+ public SqlRunner(Connection connection) {
+ this.connection = connection;
+ }
+
+ public void setUseGeneratedKeySupport(boolean useGeneratedKeySupport) { }
+ public Map selectOne(String sql, Object... args) throws SQLException { return null; }
+ public List> selectAll(String sql, Object... args) throws SQLException { return null; }
+ public int insert(String sql, Object... args) throws SQLException { return 0; }
+ public int update(String sql, Object... args) throws SQLException { return 0; }
+ public int delete(String sql, Object... args) throws SQLException { return 0; }
+ public void closeConnection() { }
+ private void setParameters(PreparedStatement ps, Object... args) throws SQLException { }
+ private List> getResults(ResultSet rs) throws SQLException { return null; }
+
+}
diff --git a/java/ql/test/stubs/springframework-5.3.8/org/springframework/jdbc/core/namedparam/NamedParameterJdbcTemplate.java b/java/ql/test/stubs/springframework-5.3.8/org/springframework/jdbc/core/namedparam/NamedParameterJdbcTemplate.java
new file mode 100644
index 000000000000..b5c37a6f86a2
--- /dev/null
+++ b/java/ql/test/stubs/springframework-5.3.8/org/springframework/jdbc/core/namedparam/NamedParameterJdbcTemplate.java
@@ -0,0 +1,169 @@
+package org.springframework.jdbc.core.namedparam;
+
+import java.sql.PreparedStatement;
+import java.sql.SQLException;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Consumer;
+import java.util.stream.Stream;
+
+import javax.sql.DataSource;
+
+import org.springframework.dao.DataAccessException;
+import org.springframework.jdbc.core.BatchPreparedStatementSetter;
+import org.springframework.jdbc.core.JdbcOperations;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.jdbc.core.PreparedStatementCallback;
+import org.springframework.jdbc.core.PreparedStatementCreator;
+import org.springframework.jdbc.core.ResultSetExtractor;
+import org.springframework.jdbc.core.RowCallbackHandler;
+import org.springframework.jdbc.core.RowMapper;
+import org.springframework.jdbc.core.SqlParameter;
+import org.springframework.jdbc.support.KeyHolder;
+import org.springframework.jdbc.support.rowset.SqlRowSet;
+
+public class NamedParameterJdbcTemplate implements NamedParameterJdbcOperations {
+
+ public static final int DEFAULT_CACHE_LIMIT = 256;
+ private final JdbcOperations classicJdbcTemplate;
+ public NamedParameterJdbcTemplate(DataSource dataSource) {
+ this.classicJdbcTemplate = new JdbcTemplate(dataSource);
+ }
+ public NamedParameterJdbcTemplate(JdbcOperations classicJdbcTemplate) {
+ this.classicJdbcTemplate = classicJdbcTemplate;
+ }
+ @Override
+ public JdbcOperations getJdbcOperations() { return null; }
+ public JdbcTemplate getJdbcTemplate() { return null; }
+ public void setCacheLimit(int cacheLimit) { }
+ public int getCacheLimit() { return 0; }
+
+ @Override
+ public T execute(String sql, SqlParameterSource paramSource, PreparedStatementCallback action)
+ throws DataAccessException { return null; }
+
+ @Override
+ public T execute(String sql, Map paramMap, PreparedStatementCallback action)
+ throws DataAccessException { return null; }
+
+ @Override
+ public T execute(String sql, PreparedStatementCallback action) throws DataAccessException { return null; }
+
+ @Override
+ public T query(String sql, SqlParameterSource paramSource, ResultSetExtractor rse)
+ throws DataAccessException { return null; }
+
+ @Override
+ public T query(String sql, Map paramMap, ResultSetExtractor rse)
+ throws DataAccessException { return null; }
+
+ @Override
+ public T query(String sql, ResultSetExtractor rse) throws DataAccessException { return null; }
+
+ @Override
+ public void query(String sql, SqlParameterSource paramSource, RowCallbackHandler rch)
+ throws DataAccessException { }
+
+ @Override
+ public void query(String sql, Map paramMap, RowCallbackHandler rch)
+ throws DataAccessException { }
+
+ @Override
+ public void query(String sql, RowCallbackHandler rch) throws DataAccessException { }
+
+ @Override
+ public List query(String sql, SqlParameterSource paramSource, RowMapper rowMapper)
+ throws DataAccessException { return null; }
+
+ @Override
+ public List query(String sql, Map paramMap, RowMapper rowMapper)
+ throws DataAccessException { return null; }
+
+ @Override
+ public List query(String sql, RowMapper rowMapper) throws DataAccessException { return null; }
+
+ @Override
+ public Stream queryForStream(String sql, SqlParameterSource paramSource, RowMapper rowMapper)
+ throws DataAccessException { return null; }
+
+ @Override
+ public Stream queryForStream(String sql, Map paramMap, RowMapper rowMapper)
+ throws DataAccessException { return null; }
+
+ @Override
+ public T queryForObject(String sql, SqlParameterSource paramSource, RowMapper rowMapper)
+ throws DataAccessException { return null; }
+
+ @Override
+ public T queryForObject(String sql, Map paramMap, RowMapperrowMapper)
+ throws DataAccessException { return null; }
+
+ @Override
+ public T queryForObject(String sql, SqlParameterSource paramSource, Class requiredType)
+ throws DataAccessException { return null; }
+
+ @Override
+ public T queryForObject(String sql, Map paramMap, Class requiredType)
+ throws DataAccessException { return null; }
+
+ @Override
+ public Map queryForMap(String sql, SqlParameterSource paramSource) throws DataAccessException { return null; }
+
+ @Override
+ public Map queryForMap(String sql, Map paramMap) throws DataAccessException { return null; }
+
+ @Override
+ public List queryForList(String sql, SqlParameterSource paramSource, Class elementType)
+ throws DataAccessException { return null; }
+
+ @Override
+ public List queryForList(String sql, Map paramMap, Class elementType)
+ throws DataAccessException { return null; }
+
+ @Override
+ public List> queryForList(String sql, SqlParameterSource paramSource)
+ throws DataAccessException { return null; }
+
+ @Override
+ public List> queryForList(String sql, Map paramMap)
+ throws DataAccessException { return null; }
+
+ @Override
+ public SqlRowSet queryForRowSet(String sql, SqlParameterSource paramSource) throws DataAccessException { return null; }
+
+ @Override
+ public SqlRowSet queryForRowSet(String sql, Map paramMap) throws DataAccessException { return null; }
+
+ @Override
+ public int update(String sql, SqlParameterSource paramSource) throws DataAccessException { return 0; }
+
+ @Override
+ public int update(String sql, Map paramMap) throws DataAccessException { return 0; }
+
+ @Override
+ public int update(String sql, SqlParameterSource paramSource, KeyHolder generatedKeyHolder)
+ throws DataAccessException { return 0; }
+
+ @Override
+ public int update(
+ String sql, SqlParameterSource paramSource, KeyHolder generatedKeyHolder, String[] keyColumnNames)
+ throws DataAccessException { return 0; }
+
+ @Override
+ public int[] batchUpdate(String sql, SqlParameterSource[] batchArgs) { return new int[0]; }
+
+ @Override
+ public int[] batchUpdate(String sql, Map[] batchValues) { return new int[0]; }
+
+ public int[] batchUpdate(String sql, SqlParameterSource[] batchArgs, KeyHolder generatedKeyHolder) { return new int[0]; }
+
+ public int[] batchUpdate(String sql, SqlParameterSource[] batchArgs, KeyHolder generatedKeyHolder,
+ String[] keyColumnNames) { return new int[0]; }
+
+ protected PreparedStatementCreator getPreparedStatementCreator(String sql, SqlParameterSource paramSource) {
+ return null;
+ }
+
+ protected ParsedSql getParsedSql(String sql) { return null; }
+
+}
From d4114f66c216d87d2816059d3e8d2af18098018e Mon Sep 17 00:00:00 2001
From: Jami Cogswell
Date: Thu, 19 Dec 2024 18:59:25 -0500
Subject: [PATCH 23/38] Java: more name-based heuristic tests to test regex
---
.../CsrfUnprotectedRequestTypeTest.java | 32 +++++++++++++++----
1 file changed, 26 insertions(+), 6 deletions(-)
diff --git a/java/ql/test/query-tests/security/CWE-352/CsrfUnprotectedRequestTypeTest.java b/java/ql/test/query-tests/security/CWE-352/CsrfUnprotectedRequestTypeTest.java
index 79404e6b1052..764ce4ecbea6 100644
--- a/java/ql/test/query-tests/security/CWE-352/CsrfUnprotectedRequestTypeTest.java
+++ b/java/ql/test/query-tests/security/CWE-352/CsrfUnprotectedRequestTypeTest.java
@@ -371,13 +371,33 @@ public void bad10(@RequestParam String user) { // $ hasCsrfUnprotectedRequestTyp
myBatisService.bad10(user);
}
- // Test name-based heuristic
+ // Test name-based heuristic for method names that imply a state-change
+ @GetMapping(value = "transfer")
+ public String transfer(@RequestParam String user) { return "transfer"; } // $ hasCsrfUnprotectedRequestType
- // BAD: method name implies a state-change
- @GetMapping(value = "delete")
- public String delete(@RequestParam String user) { // $ hasCsrfUnprotectedRequestType
- return "delete";
- }
+ @GetMapping(value = "transfer")
+ public String transferData(@RequestParam String user) { return "transfer"; } // $ hasCsrfUnprotectedRequestType
+
+ @GetMapping(value = "transfer")
+ public String doTransfer(@RequestParam String user) { return "transfer"; } // $ hasCsrfUnprotectedRequestType
+
+ @GetMapping(value = "transfer")
+ public String doTransferAllData(@RequestParam String user) { return "transfer"; } // $ hasCsrfUnprotectedRequestType
+
+ @GetMapping(value = "transfer")
+ public String doDataTransfer(@RequestParam String user) { return "transfer"; } // $ hasCsrfUnprotectedRequestType
+
+ @GetMapping(value = "transfer")
+ public String transfered(@RequestParam String user) { return "transfer"; } // OK: we look for 'transfer' only
+
+ @GetMapping(value = "transfer")
+ public String dotransfer(@RequestParam String user) { return "transfer"; } // OK: we look for 'transfer' within camelCase only
+
+ @GetMapping(value = "transfer")
+ public String doTransferdata(@RequestParam String user) { return "transfer"; } // OK: we look for 'transfer' within camelCase only
+
+ @GetMapping(value = "transfer")
+ public String getTransfer(@RequestParam String user) { return "transfer"; } // OK: starts with 'get'
// Test Stapler web methods with name-based heuristic
From 20e8eb4323e767105c6554eb6b75be1eaa92eda4 Mon Sep 17 00:00:00 2001
From: Jami Cogswell
Date: Thu, 19 Dec 2024 20:20:09 -0500
Subject: [PATCH 24/38] Java: some clean-up and refactoring
---
.../frameworks/spring/SpringController.qll | 5 ++
.../CsrfUnprotectedRequestTypeQuery.qll | 89 +++++++++++++------
2 files changed, 66 insertions(+), 28 deletions(-)
diff --git a/java/ql/lib/semmle/code/java/frameworks/spring/SpringController.qll b/java/ql/lib/semmle/code/java/frameworks/spring/SpringController.qll
index 9195a3878342..204c4b692c76 100644
--- a/java/ql/lib/semmle/code/java/frameworks/spring/SpringController.qll
+++ b/java/ql/lib/semmle/code/java/frameworks/spring/SpringController.qll
@@ -156,6 +156,11 @@ class SpringRequestMappingMethod extends SpringControllerMethod {
/** Gets the "value" @RequestMapping annotation value, if present. */
string getValue() { result = requestMappingAnnotation.getStringValue("value") }
+ /** Gets the "method" @RequestMapping annotation value, if present. */
+ string getMethod() {
+ result = requestMappingAnnotation.getAnEnumConstantArrayValue("method").getName()
+ }
+
/** Holds if this is considered an `@ResponseBody` method. */
predicate isResponseBody() {
this.getAnAnnotation().getType() instanceof SpringResponseBodyAnnotationType or
diff --git a/java/ql/lib/semmle/code/java/security/CsrfUnprotectedRequestTypeQuery.qll b/java/ql/lib/semmle/code/java/security/CsrfUnprotectedRequestTypeQuery.qll
index c89e6c442bff..d20b5404ae13 100644
--- a/java/ql/lib/semmle/code/java/security/CsrfUnprotectedRequestTypeQuery.qll
+++ b/java/ql/lib/semmle/code/java/security/CsrfUnprotectedRequestTypeQuery.qll
@@ -8,6 +8,7 @@ private import semmle.code.java.frameworks.Jdbc
private import semmle.code.java.dataflow.ExternalFlow
private import semmle.code.java.dispatch.VirtualDispatch
private import semmle.code.java.dataflow.TaintTracking
+import CallGraph
/** A method that is not protected from CSRF by default. */
abstract class CsrfUnprotectedMethod extends Method { }
@@ -24,12 +25,11 @@ private class SpringCsrfUnprotectedMethod extends CsrfUnprotectedMethod instance
or
this.hasAnnotation("org.springframework.web.bind.annotation", "RequestMapping") and
(
- this.getAnAnnotation().getAnEnumConstantArrayValue("method").getName() =
- ["GET", "HEAD", "OPTIONS", "TRACE"]
+ this.getMethod() = ["GET", "HEAD", "OPTIONS", "TRACE"]
or
// If no request type is specified with `@RequestMapping`, then all request types
// are possible, so we treat this as unsafe; example: @RequestMapping(value = "test").
- not exists(this.getAnAnnotation().getAnArrayValue("method"))
+ not exists(this.getMethod())
)
}
}
@@ -49,12 +49,42 @@ private class StaplerCsrfUnprotectedMethod extends CsrfUnprotectedMethod instanc
}
}
+/** Gets a word that is interesting because it may indicate a state change. */
+private string getAnInterestingWord() {
+ result =
+ [
+ "post", "put", "patch", "delete", "remove", "create", "add", "update", "edit", "publish",
+ "unpublish", "fill", "move", "transfer", "logout", "login", "access", "connect", "connection",
+ "register", "submit"
+ ]
+}
+
+/**
+ * Gets the regular expression used for matching strings that look like they
+ * contain an interesting word.
+ */
+private string getInterestingWordRegex() {
+ result = "(^|\\w+(?=[A-Z]))((?i)" + concat(getAnInterestingWord(), "|") + ")($|(?![a-z])\\w+)"
+}
+
+/** Gets a word that is uninteresting because it likely does not indicate a state change. */
+private string getAnUninterestingWord() {
+ result = ["get", "show", "view", "list", "query", "find"]
+}
+
+/**
+ * Gets the regular expression used for matching strings that look like they
+ * contain an uninteresting word.
+ */
+private string getUninterestingWordRegex() {
+ result = "^(" + concat(getAnUninterestingWord(), "|") + ")(?![a-z])\\w*"
+}
+
/** A method that appears to change application state based on its name. */
-private class NameStateChangeMethod extends Method {
- NameStateChangeMethod() {
- this.getName()
- .regexpMatch("(^|\\w+(?=[A-Z]))((?i)post|put|patch|delete|remove|create|add|update|edit|(un|)publish|fill|move|transfer|log(out|in)|access|connect(|ion)|register|submit)($|(?![a-z])\\w+)") and
- not this.getName().regexpMatch("^(get|show|view|list|query|find)(?![a-z])\\w*")
+private class NameBasedStateChangeMethod extends Method {
+ NameBasedStateChangeMethod() {
+ this.getName().regexpMatch(getInterestingWordRegex()) and
+ not this.getName().regexpMatch(getUninterestingWordRegex())
}
}
@@ -91,9 +121,9 @@ private class PreparedStatementDatabaseUpdateMethod extends DatabaseUpdateMethod
}
}
-/** A method found via the sql-injection models which may update a SQL database. */
-private class SqlInjectionMethod extends DatabaseUpdateMethod {
- SqlInjectionMethod() {
+/** A method found via the sql-injection sink models which may update a database. */
+private class SqlInjectionDatabaseUpdateMethod extends DatabaseUpdateMethod {
+ SqlInjectionDatabaseUpdateMethod() {
exists(DataFlow::Node n | this = n.asExpr().(Argument).getCall().getCallee() |
sinkNode(n, "sql-injection") and
// do not include `executeQuery` since it is typically used with a select statement
@@ -106,9 +136,10 @@ private class SqlInjectionMethod extends DatabaseUpdateMethod {
}
/**
- * A taint-tracking configuration for reasoning about SQL queries that update a database.
+ * A taint-tracking configuration for reasoning about SQL statements that update
+ * a database via a call to an `execute` method.
*/
-module SqlExecuteConfig implements DataFlow::ConfigSig {
+private module SqlExecuteConfig implements DataFlow::ConfigSig {
predicate isSource(DataFlow::Node source) {
exists(StringLiteral sl | source.asExpr() = sl |
sl.getValue().regexpMatch("^(?i)(insert|update|delete).*")
@@ -117,16 +148,19 @@ module SqlExecuteConfig implements DataFlow::ConfigSig {
predicate isSink(DataFlow::Node sink) {
exists(Method m | m = sink.asExpr().(Argument).getCall().getCallee() |
- m instanceof SqlInjectionMethod and
+ m instanceof SqlInjectionDatabaseUpdateMethod and
m.hasName("execute")
)
}
}
-/** Tracks flow from SQL queries that update a database to the argument of an execute method call. */
-module SqlExecuteFlow = TaintTracking::Global;
+/**
+ * Tracks flow from SQL statements that update a database to the argument of
+ * an `execute` method call.
+ */
+private module SqlExecuteFlow = TaintTracking::Global;
-/** Provides classes and predicates representing call paths. */
+/** Provides classes and predicates representing call graph paths. */
module CallGraph {
private newtype TCallPathNode =
TMethod(Method m) or
@@ -178,20 +212,19 @@ module CallGraph {
predicate edges(CallPathNode pred, CallPathNode succ) { pred.getASuccessor() = succ }
}
-import CallGraph
-
/**
- * Holds if `src` is an unprotected request handler that reaches a
- * `sink` that updates a database.
+ * Holds if `sourceMethod` is an unprotected request handler that reaches a
+ * `sinkMethodCall` that updates a database.
*/
-predicate unprotectedDatabaseUpdate(CallPathNode sourceMethod, CallPathNode sinkMethodCall) {
+private predicate unprotectedDatabaseUpdate(CallPathNode sourceMethod, CallPathNode sinkMethodCall) {
sourceMethod.asMethod() instanceof CsrfUnprotectedMethod and
exists(CallPathNode sinkMethod |
sinkMethod.asMethod() instanceof DatabaseUpdateMethod and
sinkMethodCall.getASuccessor() = pragma[only_bind_into](sinkMethod) and
sourceMethod.getASuccessor+() = pragma[only_bind_into](sinkMethodCall) and
+ // exclude SQL `execute` calls that do not update database
if
- sinkMethod.asMethod() instanceof SqlInjectionMethod and
+ sinkMethod.asMethod() instanceof SqlInjectionDatabaseUpdateMethod and
sinkMethod.asMethod().hasName("execute")
then
exists(SqlExecuteFlow::PathNode executeSink | SqlExecuteFlow::flowPath(_, executeSink) |
@@ -202,22 +235,22 @@ predicate unprotectedDatabaseUpdate(CallPathNode sourceMethod, CallPathNode sink
}
/**
- * Holds if `src` is an unprotected request handler that appears to
+ * Holds if `sourceMethod` is an unprotected request handler that appears to
* change application state based on its name.
*/
-private predicate unprotectedHeuristicStateChange(CallPathNode sourceMethod, CallPathNode sinkMethod) {
+private predicate unprotectedNameBasedStateChange(CallPathNode sourceMethod, CallPathNode sinkMethod) {
sourceMethod.asMethod() instanceof CsrfUnprotectedMethod and
- sinkMethod.asMethod() instanceof NameStateChangeMethod and
+ sinkMethod.asMethod() instanceof NameBasedStateChangeMethod and
sinkMethod = sourceMethod and
// exclude any alerts that update a database
not unprotectedDatabaseUpdate(sourceMethod, _)
}
/**
- * Holds if `src` is an unprotected request handler that may
+ * Holds if `source` is an unprotected request handler that may
* change an application's state.
*/
predicate unprotectedStateChange(CallPathNode source, CallPathNode sink) {
unprotectedDatabaseUpdate(source, sink) or
- unprotectedHeuristicStateChange(source, sink)
+ unprotectedNameBasedStateChange(source, sink)
}
From 04624251911e3b5856340a3fe38723a859df1638 Mon Sep 17 00:00:00 2001
From: Jami Cogswell
Date: Wed, 8 Jan 2025 18:22:19 -0500
Subject: [PATCH 25/38] Java: rename getMethod to getMethodValue
---
.../semmle/code/java/frameworks/spring/SpringController.qll | 2 +-
.../code/java/security/CsrfUnprotectedRequestTypeQuery.qll | 4 ++--
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/java/ql/lib/semmle/code/java/frameworks/spring/SpringController.qll b/java/ql/lib/semmle/code/java/frameworks/spring/SpringController.qll
index 204c4b692c76..a222be20c20a 100644
--- a/java/ql/lib/semmle/code/java/frameworks/spring/SpringController.qll
+++ b/java/ql/lib/semmle/code/java/frameworks/spring/SpringController.qll
@@ -157,7 +157,7 @@ class SpringRequestMappingMethod extends SpringControllerMethod {
string getValue() { result = requestMappingAnnotation.getStringValue("value") }
/** Gets the "method" @RequestMapping annotation value, if present. */
- string getMethod() {
+ string getMethodValue() {
result = requestMappingAnnotation.getAnEnumConstantArrayValue("method").getName()
}
diff --git a/java/ql/lib/semmle/code/java/security/CsrfUnprotectedRequestTypeQuery.qll b/java/ql/lib/semmle/code/java/security/CsrfUnprotectedRequestTypeQuery.qll
index d20b5404ae13..17c10cf09753 100644
--- a/java/ql/lib/semmle/code/java/security/CsrfUnprotectedRequestTypeQuery.qll
+++ b/java/ql/lib/semmle/code/java/security/CsrfUnprotectedRequestTypeQuery.qll
@@ -25,11 +25,11 @@ private class SpringCsrfUnprotectedMethod extends CsrfUnprotectedMethod instance
or
this.hasAnnotation("org.springframework.web.bind.annotation", "RequestMapping") and
(
- this.getMethod() = ["GET", "HEAD", "OPTIONS", "TRACE"]
+ this.getMethodValue() = ["GET", "HEAD", "OPTIONS", "TRACE"]
or
// If no request type is specified with `@RequestMapping`, then all request types
// are possible, so we treat this as unsafe; example: @RequestMapping(value = "test").
- not exists(this.getMethod())
+ not exists(this.getMethodValue())
)
}
}
From 8173fd01b807892d72c3b2de6fd0676613d0c4fa Mon Sep 17 00:00:00 2001
From: Jami Cogswell
Date: Wed, 8 Jan 2025 18:24:38 -0500
Subject: [PATCH 26/38] Java: use two negations
---
.../code/java/security/CsrfUnprotectedRequestTypeQuery.qll | 6 ++----
1 file changed, 2 insertions(+), 4 deletions(-)
diff --git a/java/ql/lib/semmle/code/java/security/CsrfUnprotectedRequestTypeQuery.qll b/java/ql/lib/semmle/code/java/security/CsrfUnprotectedRequestTypeQuery.qll
index 17c10cf09753..de76debd2c21 100644
--- a/java/ql/lib/semmle/code/java/security/CsrfUnprotectedRequestTypeQuery.qll
+++ b/java/ql/lib/semmle/code/java/security/CsrfUnprotectedRequestTypeQuery.qll
@@ -42,10 +42,8 @@ private class SpringCsrfUnprotectedMethod extends CsrfUnprotectedMethod instance
private class StaplerCsrfUnprotectedMethod extends CsrfUnprotectedMethod instanceof StaplerWebMethod
{
StaplerCsrfUnprotectedMethod() {
- not (
- this.hasAnnotation("org.kohsuke.stapler.interceptor", "RequirePOST") or
- this.hasAnnotation("org.kohsuke.stapler.verb", "POST")
- )
+ not this.hasAnnotation("org.kohsuke.stapler.interceptor", "RequirePOST") and
+ not this.hasAnnotation("org.kohsuke.stapler.verb", "POST")
}
}
From 530a77e5a08deb9e256c124102b59f18ac1bad0b Mon Sep 17 00:00:00 2001
From: Jami Cogswell
Date: Wed, 8 Jan 2025 18:37:57 -0500
Subject: [PATCH 27/38] Java: refactor into canTargetDatabaseUpdateMethod
---
.../security/CsrfUnprotectedRequestTypeQuery.qll | 13 +++++++++----
1 file changed, 9 insertions(+), 4 deletions(-)
diff --git a/java/ql/lib/semmle/code/java/security/CsrfUnprotectedRequestTypeQuery.qll b/java/ql/lib/semmle/code/java/security/CsrfUnprotectedRequestTypeQuery.qll
index de76debd2c21..0b5a77118609 100644
--- a/java/ql/lib/semmle/code/java/security/CsrfUnprotectedRequestTypeQuery.qll
+++ b/java/ql/lib/semmle/code/java/security/CsrfUnprotectedRequestTypeQuery.qll
@@ -183,16 +183,21 @@ module CallGraph {
[viableCallable(this.asCall()), this.asCall().getCallee()] = result.asMethod()
}
+ pragma[nomagic]
+ private predicate canTargetDatabaseUpdateMethod() {
+ exists(CallPathNode p |
+ p = this.getACallee() and
+ p.asMethod() instanceof DatabaseUpdateMethod
+ )
+ }
+
/** Gets a successor node of this `CallPathNode`, if any. */
CallPathNode getASuccessor() {
this.asMethod() = result.asCall().getEnclosingCallable()
or
result = this.getACallee() and
(
- exists(CallPathNode p |
- p = this.getACallee() and
- p.asMethod() instanceof DatabaseUpdateMethod
- )
+ this.canTargetDatabaseUpdateMethod()
implies
result.asMethod() instanceof DatabaseUpdateMethod
)
From f3721ebccf2846aff9696f69879d702529301e55 Mon Sep 17 00:00:00 2001
From: Jami Cogswell
Date: Wed, 8 Jan 2025 18:56:50 -0500
Subject: [PATCH 28/38] Java: refactor unprotectedDatabaseUpdate
---
.../CsrfUnprotectedRequestTypeQuery.qll | 28 +++++++++++--------
1 file changed, 16 insertions(+), 12 deletions(-)
diff --git a/java/ql/lib/semmle/code/java/security/CsrfUnprotectedRequestTypeQuery.qll b/java/ql/lib/semmle/code/java/security/CsrfUnprotectedRequestTypeQuery.qll
index 0b5a77118609..7c8382fb5b49 100644
--- a/java/ql/lib/semmle/code/java/security/CsrfUnprotectedRequestTypeQuery.qll
+++ b/java/ql/lib/semmle/code/java/security/CsrfUnprotectedRequestTypeQuery.qll
@@ -215,28 +215,32 @@ module CallGraph {
predicate edges(CallPathNode pred, CallPathNode succ) { pred.getASuccessor() = succ }
}
-/**
- * Holds if `sourceMethod` is an unprotected request handler that reaches a
- * `sinkMethodCall` that updates a database.
- */
-private predicate unprotectedDatabaseUpdate(CallPathNode sourceMethod, CallPathNode sinkMethodCall) {
- sourceMethod.asMethod() instanceof CsrfUnprotectedMethod and
+/** Holds if `sourceMethod` is an unprotected request handler. */
+private predicate source(CallPathNode sourceMethod) {
+ sourceMethod.asMethod() instanceof CsrfUnprotectedMethod
+}
+
+/** Holds if `sinkMethodCall` updates a database. */
+private predicate sink(CallPathNode sinkMethodCall) {
exists(CallPathNode sinkMethod |
sinkMethod.asMethod() instanceof DatabaseUpdateMethod and
- sinkMethodCall.getASuccessor() = pragma[only_bind_into](sinkMethod) and
- sourceMethod.getASuccessor+() = pragma[only_bind_into](sinkMethodCall) and
+ sinkMethodCall.getASuccessor() = sinkMethod and
// exclude SQL `execute` calls that do not update database
if
sinkMethod.asMethod() instanceof SqlInjectionDatabaseUpdateMethod and
sinkMethod.asMethod().hasName("execute")
- then
- exists(SqlExecuteFlow::PathNode executeSink | SqlExecuteFlow::flowPath(_, executeSink) |
- sinkMethodCall.asCall() = executeSink.getNode().asExpr().(Argument).getCall()
- )
+ then SqlExecuteFlow::flowToExpr(sinkMethodCall.asCall().getAnArgument())
else any()
)
}
+/**
+ * Holds if `sourceMethod` is an unprotected request handler that reaches a
+ * `sinkMethodCall` that updates a database.
+ */
+private predicate unprotectedDatabaseUpdate(CallPathNode sourceMethod, CallPathNode sinkMethodCall) =
+ doublyBoundedFastTC(CallGraph::edges/2, source/1, sink/1)(sourceMethod, sinkMethodCall)
+
/**
* Holds if `sourceMethod` is an unprotected request handler that appears to
* change application state based on its name.
From 096f6f88b243293faa3159f146709aa9183d9117 Mon Sep 17 00:00:00 2001
From: Jami Cogswell
Date: Tue, 21 Jan 2025 14:43:35 -0500
Subject: [PATCH 29/38] Java: precision to medium
---
java/ql/src/Security/CWE/CWE-352/CsrfUnprotectedRequestType.ql | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/java/ql/src/Security/CWE/CWE-352/CsrfUnprotectedRequestType.ql b/java/ql/src/Security/CWE/CWE-352/CsrfUnprotectedRequestType.ql
index 9caf29030077..e338cb84c005 100644
--- a/java/ql/src/Security/CWE/CWE-352/CsrfUnprotectedRequestType.ql
+++ b/java/ql/src/Security/CWE/CWE-352/CsrfUnprotectedRequestType.ql
@@ -6,7 +6,7 @@
* @kind path-problem
* @problem.severity error
* @security-severity 8.8
- * @precision low
+ * @precision medium
* @id java/csrf-unprotected-request-type
* @tags security
* external/cwe/cwe-352
From ead224c7b2904194c35be78a8db7df18309af283 Mon Sep 17 00:00:00 2001
From: Jami Cogswell
Date: Fri, 24 Jan 2025 12:17:46 -0500
Subject: [PATCH 30/38] Java: expand qhelp, include Stapler examples
---
.../CWE-352/CsrfUnprotectedRequestType.qhelp | 45 +++++++++++++------
.../CsrfUnprotectedRequestTypeBad.java | 5 ---
.../CsrfUnprotectedRequestTypeBadSpring.java | 14 ++++++
.../CsrfUnprotectedRequestTypeBadStapler.java | 12 +++++
.../CsrfUnprotectedRequestTypeGood.java | 5 ---
.../CsrfUnprotectedRequestTypeGoodSpring.java | 15 +++++++
...CsrfUnprotectedRequestTypeGoodStapler.java | 13 ++++++
7 files changed, 86 insertions(+), 23 deletions(-)
delete mode 100644 java/ql/src/Security/CWE/CWE-352/CsrfUnprotectedRequestTypeBad.java
create mode 100644 java/ql/src/Security/CWE/CWE-352/CsrfUnprotectedRequestTypeBadSpring.java
create mode 100644 java/ql/src/Security/CWE/CWE-352/CsrfUnprotectedRequestTypeBadStapler.java
delete mode 100644 java/ql/src/Security/CWE/CWE-352/CsrfUnprotectedRequestTypeGood.java
create mode 100644 java/ql/src/Security/CWE/CWE-352/CsrfUnprotectedRequestTypeGoodSpring.java
create mode 100644 java/ql/src/Security/CWE/CWE-352/CsrfUnprotectedRequestTypeGoodStapler.java
diff --git a/java/ql/src/Security/CWE/CWE-352/CsrfUnprotectedRequestType.qhelp b/java/ql/src/Security/CWE/CWE-352/CsrfUnprotectedRequestType.qhelp
index 7517fd5c7346..d568f236636f 100644
--- a/java/ql/src/Security/CWE/CWE-352/CsrfUnprotectedRequestType.qhelp
+++ b/java/ql/src/Security/CWE/CWE-352/CsrfUnprotectedRequestType.qhelp
@@ -10,22 +10,32 @@ result in exposure of data or unintended code execution.
-When handling requests, make sure any requests that change application state are protected from
-Cross Site Request Forgery (CSRF). Some application frameworks, such as Spring, provide default CSRF
-protection for HTTP request types that may change application state, such as POST. Other HTTP request
-types, such as GET, should not be used for actions that change the state of the application, since these
-request types are not default-protected from CSRF by the framework.
+Make sure any requests that change application state are protected from Cross Site Request Forgery (CSRF).
+Some application frameworks provide default CSRF protection for unsafe HTTP request methods (POST
,
+PUT
, DELETE
, PATCH
, CONNECT
) which may change the state of
+the application. Safe HTTP request methods (GET
, HEAD
, OPTIONS
,
+TRACE
) should be read-only and should not be used for actions that change application state.
+
+This query currently supports the Spring and Stapler web frameworks. Spring provides default CSRF protection
+for all unsafe HTTP methods. Stapler provides default CSRF protection for the POST
method.
-The following example shows a Spring request handler using a GET request for a state-changing action.
-Since a GET request does not have default CSRF protection in Spring, this type of request should
-not be used when modifying application state. Instead use one of Spring's default-protected request
-types, such as POST.
+ The following examples show Spring request handlers allowing safe HTTP request methods for state-changing actions.
+Since safe HTTP request methods do not have default CSRF protection in Spring, they should not be used when modifying
+application state. Instead use one of the unsafe HTTP methods which Spring default-protects from CSRF.
+
+
+
+
+
+ The following examples show Stapler web methods allowing safe HTTP request methods for state-changing actions.
+Since safe HTTP request methods do not have default CSRF protection in Stapler, they should not be used when modifying
+application state. Instead use the POST
method which Stapler default-protects from CSRF.
-
+
-
+
@@ -36,8 +46,17 @@ OWASP:
Spring Security Reference:
- Cross Site Request Forgery (CSRF)
- .
+ Cross Site Request Forgery (CSRF).
+
+
+Jenkins Developer Documentation:
+
+ Protecting from CSRF .
+
+
+MDN web docs:
+
+ HTTP request methods .
diff --git a/java/ql/src/Security/CWE/CWE-352/CsrfUnprotectedRequestTypeBad.java b/java/ql/src/Security/CWE/CWE-352/CsrfUnprotectedRequestTypeBad.java
deleted file mode 100644
index c79d15cd828c..000000000000
--- a/java/ql/src/Security/CWE/CWE-352/CsrfUnprotectedRequestTypeBad.java
+++ /dev/null
@@ -1,5 +0,0 @@
-// BAD - a GET request should not be used for a state-changing action like transfer
-@RequestMapping(value="transfer", method=RequestMethod.GET)
-public boolean transfer(HttpServletRequest request, HttpServletResponse response){
- return doTransfer(request, response);
-}
diff --git a/java/ql/src/Security/CWE/CWE-352/CsrfUnprotectedRequestTypeBadSpring.java b/java/ql/src/Security/CWE/CWE-352/CsrfUnprotectedRequestTypeBadSpring.java
new file mode 100644
index 000000000000..f3c8bb25906a
--- /dev/null
+++ b/java/ql/src/Security/CWE/CWE-352/CsrfUnprotectedRequestTypeBadSpring.java
@@ -0,0 +1,14 @@
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+
+// BAD - a safe HTTP request like GET should not be used for a state-changing action
+@RequestMapping(value="/transfer", method=RequestMethod.GET)
+public boolean doTransfer(HttpServletRequest request, HttpServletResponse response){
+ return transfer(request, response);
+}
+
+// BAD - no HTTP request type is specified, so safe HTTP requests are allowed
+@RequestMapping(value="/delete")
+public boolean doDelete(HttpServletRequest request, HttpServletResponse response){
+ return delete(request, response);
+}
diff --git a/java/ql/src/Security/CWE/CWE-352/CsrfUnprotectedRequestTypeBadStapler.java b/java/ql/src/Security/CWE/CWE-352/CsrfUnprotectedRequestTypeBadStapler.java
new file mode 100644
index 000000000000..2e924651d671
--- /dev/null
+++ b/java/ql/src/Security/CWE/CWE-352/CsrfUnprotectedRequestTypeBadStapler.java
@@ -0,0 +1,12 @@
+import org.kohsuke.stapler.verb.GET;
+
+// BAD - a safe HTTP request like GET should not be used for a state-changing action
+@GET
+public HttpRedirect doTransfer() {
+ return transfer();
+}
+
+// BAD - no HTTP request type is specified, so safe HTTP requests are allowed
+public HttpRedirect doDelete() {
+ return delete();
+}
diff --git a/java/ql/src/Security/CWE/CWE-352/CsrfUnprotectedRequestTypeGood.java b/java/ql/src/Security/CWE/CWE-352/CsrfUnprotectedRequestTypeGood.java
deleted file mode 100644
index 40b54a800e12..000000000000
--- a/java/ql/src/Security/CWE/CWE-352/CsrfUnprotectedRequestTypeGood.java
+++ /dev/null
@@ -1,5 +0,0 @@
-// GOOD - use a POST request for a state-changing action
-@RequestMapping(value="transfer", method=RequestMethod.POST)
-public boolean transfer(HttpServletRequest request, HttpServletResponse response){
- return doTransfer(request, response);
-}
diff --git a/java/ql/src/Security/CWE/CWE-352/CsrfUnprotectedRequestTypeGoodSpring.java b/java/ql/src/Security/CWE/CWE-352/CsrfUnprotectedRequestTypeGoodSpring.java
new file mode 100644
index 000000000000..df02ccc70c73
--- /dev/null
+++ b/java/ql/src/Security/CWE/CWE-352/CsrfUnprotectedRequestTypeGoodSpring.java
@@ -0,0 +1,15 @@
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.bind.annotation.DeleteMapping;
+
+// GOOD - use an unsafe HTTP request like POST
+@RequestMapping(value="/transfer", method=RequestMethod.POST)
+public boolean doTransfer(HttpServletRequest request, HttpServletResponse response){
+ return transfer(request, response);
+}
+
+// GOOD - use an unsafe HTTP request like DELETE
+@DeleteMapping(value="/delete")
+public boolean doDelete(HttpServletRequest request, HttpServletResponse response){
+ return delete(request, response);
+}
diff --git a/java/ql/src/Security/CWE/CWE-352/CsrfUnprotectedRequestTypeGoodStapler.java b/java/ql/src/Security/CWE/CWE-352/CsrfUnprotectedRequestTypeGoodStapler.java
new file mode 100644
index 000000000000..5a837d2af30f
--- /dev/null
+++ b/java/ql/src/Security/CWE/CWE-352/CsrfUnprotectedRequestTypeGoodStapler.java
@@ -0,0 +1,13 @@
+import org.kohsuke.stapler.verb.POST;
+
+// GOOD - use POST
+@POST
+public HttpRedirect doTransfer() {
+ return transfer();
+}
+
+// GOOD - use POST
+@POST
+public HttpRedirect doDelete() {
+ return delete();
+}
From 530103e2d93c606b58ea5fa03f0ab7cd53a90266 Mon Sep 17 00:00:00 2001
From: Jami Cogswell
Date: Wed, 29 Jan 2025 21:30:53 -0500
Subject: [PATCH 31/38] Java: narrow query
remove PUT and DELETE from StaplerCsrfUnprotectedMethod
remove OPTIONS and TRACE from SpringCsrfUnprotectedMethod
---
.../CsrfUnprotectedRequestTypeQuery.qll | 7 +++++--
.../CsrfUnprotectedRequestTypeTest.java | 18 ++++++++++++++----
2 files changed, 19 insertions(+), 6 deletions(-)
diff --git a/java/ql/lib/semmle/code/java/security/CsrfUnprotectedRequestTypeQuery.qll b/java/ql/lib/semmle/code/java/security/CsrfUnprotectedRequestTypeQuery.qll
index 7c8382fb5b49..42d6db246c0e 100644
--- a/java/ql/lib/semmle/code/java/security/CsrfUnprotectedRequestTypeQuery.qll
+++ b/java/ql/lib/semmle/code/java/security/CsrfUnprotectedRequestTypeQuery.qll
@@ -25,7 +25,7 @@ private class SpringCsrfUnprotectedMethod extends CsrfUnprotectedMethod instance
or
this.hasAnnotation("org.springframework.web.bind.annotation", "RequestMapping") and
(
- this.getMethodValue() = ["GET", "HEAD", "OPTIONS", "TRACE"]
+ this.getMethodValue() = ["GET", "HEAD"]
or
// If no request type is specified with `@RequestMapping`, then all request types
// are possible, so we treat this as unsafe; example: @RequestMapping(value = "test").
@@ -43,7 +43,10 @@ private class StaplerCsrfUnprotectedMethod extends CsrfUnprotectedMethod instanc
{
StaplerCsrfUnprotectedMethod() {
not this.hasAnnotation("org.kohsuke.stapler.interceptor", "RequirePOST") and
- not this.hasAnnotation("org.kohsuke.stapler.verb", "POST")
+ // Jenkins only explicitly protects against CSRF for POST requests, but we
+ // also exclude PUT and DELETE since these request types are only exploitable
+ // if there is a CORS issue.
+ not this.hasAnnotation("org.kohsuke.stapler.verb", ["POST", "PUT", "DELETE"])
}
}
diff --git a/java/ql/test/query-tests/security/CWE-352/CsrfUnprotectedRequestTypeTest.java b/java/ql/test/query-tests/security/CWE-352/CsrfUnprotectedRequestTypeTest.java
index 764ce4ecbea6..00b8ea24f719 100644
--- a/java/ql/test/query-tests/security/CWE-352/CsrfUnprotectedRequestTypeTest.java
+++ b/java/ql/test/query-tests/security/CWE-352/CsrfUnprotectedRequestTypeTest.java
@@ -88,6 +88,17 @@ public void bad5() { // $ hasCsrfUnprotectedRequestType
} catch (SQLException e) { }
}
+ // GOOD: uses OPTIONS or TRACE, which are unlikely to be exploitable via CSRF
+ @RequestMapping(value = "", method = { OPTIONS, TRACE })
+ public void good0() {
+ try {
+ String sql = "DELETE";
+ Connection conn = DriverManager.getConnection("url");
+ PreparedStatement ps = conn.prepareStatement(sql);
+ ps.executeUpdate(); // database update method call
+ } catch (SQLException e) { }
+ }
+
// GOOD: uses POST request when updating a database
@RequestMapping(value = "", method = RequestMethod.POST)
public void good1() {
@@ -430,11 +441,10 @@ public String doPost3(String user) { // $ hasCsrfUnprotectedRequestType
return "post";
}
- // BAD: Stapler web method annotated with `@PUT` and method name that implies a state-change
- // We treat this case as bad for Stapler since the Jenkins docs only say that @POST/@RequirePOST
- // provide default protection against CSRF.
+ // GOOD: Stapler web method annotated with `@PUT` and method name that implies a state-change
+ // We treat this case as good since PUT is only exploitable if there is a CORS issue.
@PUT
- public String doPut(String user) { // $ hasCsrfUnprotectedRequestType
+ public String doPut(String user) {
return "put";
}
From 577152e20faf7798287fa4a942c3134f03553e4b Mon Sep 17 00:00:00 2001
From: Jami Cogswell
Date: Wed, 29 Jan 2025 22:21:48 -0500
Subject: [PATCH 32/38] Java: minor qhelp update
---
.../Security/CWE/CWE-352/CsrfUnprotectedRequestType.qhelp | 7 +++----
.../CWE/CWE-352/CsrfUnprotectedRequestTypeBadStapler.java | 4 ++--
.../CWE/CWE-352/CsrfUnprotectedRequestTypeGoodStapler.java | 4 ++--
3 files changed, 7 insertions(+), 8 deletions(-)
diff --git a/java/ql/src/Security/CWE/CWE-352/CsrfUnprotectedRequestType.qhelp b/java/ql/src/Security/CWE/CWE-352/CsrfUnprotectedRequestType.qhelp
index d568f236636f..5d71f3f68977 100644
--- a/java/ql/src/Security/CWE/CWE-352/CsrfUnprotectedRequestType.qhelp
+++ b/java/ql/src/Security/CWE/CWE-352/CsrfUnprotectedRequestType.qhelp
@@ -11,10 +11,9 @@ result in exposure of data or unintended code execution.
Make sure any requests that change application state are protected from Cross Site Request Forgery (CSRF).
-Some application frameworks provide default CSRF protection for unsafe HTTP request methods (POST
,
-PUT
, DELETE
, PATCH
, CONNECT
) which may change the state of
-the application. Safe HTTP request methods (GET
, HEAD
, OPTIONS
,
-TRACE
) should be read-only and should not be used for actions that change application state.
+Some application frameworks provide default CSRF protection for unsafe HTTP request methods (such as
+POST
) which may change the state of the application. Safe HTTP request methods (such as
+GET
) should be read-only and should not be used for actions that change application state.
This query currently supports the Spring and Stapler web frameworks. Spring provides default CSRF protection
for all unsafe HTTP methods. Stapler provides default CSRF protection for the POST
method.
diff --git a/java/ql/src/Security/CWE/CWE-352/CsrfUnprotectedRequestTypeBadStapler.java b/java/ql/src/Security/CWE/CWE-352/CsrfUnprotectedRequestTypeBadStapler.java
index 2e924651d671..d76e85cfd37b 100644
--- a/java/ql/src/Security/CWE/CWE-352/CsrfUnprotectedRequestTypeBadStapler.java
+++ b/java/ql/src/Security/CWE/CWE-352/CsrfUnprotectedRequestTypeBadStapler.java
@@ -7,6 +7,6 @@ public HttpRedirect doTransfer() {
}
// BAD - no HTTP request type is specified, so safe HTTP requests are allowed
-public HttpRedirect doDelete() {
- return delete();
+public HttpRedirect doPost() {
+ return post();
}
diff --git a/java/ql/src/Security/CWE/CWE-352/CsrfUnprotectedRequestTypeGoodStapler.java b/java/ql/src/Security/CWE/CWE-352/CsrfUnprotectedRequestTypeGoodStapler.java
index 5a837d2af30f..f8c81633c19a 100644
--- a/java/ql/src/Security/CWE/CWE-352/CsrfUnprotectedRequestTypeGoodStapler.java
+++ b/java/ql/src/Security/CWE/CWE-352/CsrfUnprotectedRequestTypeGoodStapler.java
@@ -8,6 +8,6 @@ public HttpRedirect doTransfer() {
// GOOD - use POST
@POST
-public HttpRedirect doDelete() {
- return delete();
+public HttpRedirect doPost() {
+ return post();
}
From 0071e1acc21159bc11fb6fde2846de483aab630b Mon Sep 17 00:00:00 2001
From: Jami Cogswell
Date: Thu, 30 Jan 2025 10:19:21 -0500
Subject: [PATCH 33/38] Java: resolve merge conflict
remove import no longer needed since contents of MyBatisMapperXML.qll have been moved to MyBatis.qll
---
.../Security/CWE/CWE-089/MyBatisMapperXmlSqlInjection.ql | 1 -
1 file changed, 1 deletion(-)
diff --git a/java/ql/src/experimental/Security/CWE/CWE-089/MyBatisMapperXmlSqlInjection.ql b/java/ql/src/experimental/Security/CWE/CWE-089/MyBatisMapperXmlSqlInjection.ql
index a73bf21c5dd0..01b33138d72a 100644
--- a/java/ql/src/experimental/Security/CWE/CWE-089/MyBatisMapperXmlSqlInjection.ql
+++ b/java/ql/src/experimental/Security/CWE/CWE-089/MyBatisMapperXmlSqlInjection.ql
@@ -15,7 +15,6 @@
import java
deprecated import MyBatisCommonLib
deprecated import MyBatisMapperXmlSqlInjectionLib
-deprecated import semmle.code.xml.MyBatisMapperXML
import semmle.code.java.dataflow.FlowSources
private import semmle.code.java.security.Sanitizers
deprecated import MyBatisMapperXmlSqlInjectionFlow::PathGraph
From 516df3b4be4f75a68438e0a965eca6b915a8e0e9 Mon Sep 17 00:00:00 2001
From: Jami Cogswell
Date: Mon, 3 Feb 2025 14:52:57 -0500
Subject: [PATCH 34/38] Java: qhelp wording updates
---
.../CWE-352/CsrfUnprotectedRequestType.qhelp | 17 +++++++++--------
1 file changed, 9 insertions(+), 8 deletions(-)
diff --git a/java/ql/src/Security/CWE/CWE-352/CsrfUnprotectedRequestType.qhelp b/java/ql/src/Security/CWE/CWE-352/CsrfUnprotectedRequestType.qhelp
index 5d71f3f68977..c7cfb14bd29f 100644
--- a/java/ql/src/Security/CWE/CWE-352/CsrfUnprotectedRequestType.qhelp
+++ b/java/ql/src/Security/CWE/CWE-352/CsrfUnprotectedRequestType.qhelp
@@ -3,17 +3,18 @@
When you set up a web server to receive a request from a client without any mechanism
-for verifying that it was intentionally sent, then it is vulnerable to attack. An attacker can
-trick a client into making an unintended request to the web server that will be treated as
-an authentic request. This can be done via a URL, image load, XMLHttpRequest, etc. and can
-result in exposure of data or unintended code execution.
+for verifying that it was intentionally sent, then it is vulnerable to a Cross Site Request
+Forgery (CSRF) attack. An attacker can trick a client into making an unintended request
+to the web server that will be treated as an authentic request. This can be done via a URL,
+image load, XMLHttpRequest, etc. and can result in exposure of data or unintended code execution.
-Make sure any requests that change application state are protected from Cross Site Request Forgery (CSRF).
-Some application frameworks provide default CSRF protection for unsafe HTTP request methods (such as
-POST
) which may change the state of the application. Safe HTTP request methods (such as
-GET
) should be read-only and should not be used for actions that change application state.
+Make sure any requests that change application state are protected from CSRF. Some application
+frameworks provide default CSRF protection for unsafe HTTP request methods (such as POST
)
+which may change the state of the application. Safe HTTP request methods (such as GET
)
+should only perform read-only operations and should not be used for actions that change application
+state.
This query currently supports the Spring and Stapler web frameworks. Spring provides default CSRF protection
for all unsafe HTTP methods. Stapler provides default CSRF protection for the POST
method.
From 283c3b1e444691ad4d7a882fc4117550804eca5c Mon Sep 17 00:00:00 2001
From: Jami Cogswell
Date: Tue, 4 Feb 2025 12:47:19 -0500
Subject: [PATCH 35/38] Java: minor qhelp updates
---
.../Security/CWE/CWE-352/CsrfUnprotectedRequestType.qhelp | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/java/ql/src/Security/CWE/CWE-352/CsrfUnprotectedRequestType.qhelp b/java/ql/src/Security/CWE/CWE-352/CsrfUnprotectedRequestType.qhelp
index c7cfb14bd29f..08df43e220d1 100644
--- a/java/ql/src/Security/CWE/CWE-352/CsrfUnprotectedRequestType.qhelp
+++ b/java/ql/src/Security/CWE/CWE-352/CsrfUnprotectedRequestType.qhelp
@@ -3,7 +3,7 @@
When you set up a web server to receive a request from a client without any mechanism
-for verifying that it was intentionally sent, then it is vulnerable to a Cross Site Request
+for verifying that it was intentionally sent, then it is vulnerable to a Cross-Site Request
Forgery (CSRF) attack. An attacker can trick a client into making an unintended request
to the web server that will be treated as an authentic request. This can be done via a URL,
image load, XMLHttpRequest, etc. and can result in exposure of data or unintended code execution.
@@ -17,13 +17,13 @@ should only perform read-only operations and should not be used for actions that
state.
This query currently supports the Spring and Stapler web frameworks. Spring provides default CSRF protection
-for all unsafe HTTP methods. Stapler provides default CSRF protection for the POST
method.
+for all unsafe HTTP methods whereas Stapler provides default CSRF protection for the POST
method.
The following examples show Spring request handlers allowing safe HTTP request methods for state-changing actions.
Since safe HTTP request methods do not have default CSRF protection in Spring, they should not be used when modifying
-application state. Instead use one of the unsafe HTTP methods which Spring default-protects from CSRF.
+application state. Instead, use one of the unsafe HTTP methods which Spring default-protects from CSRF.
@@ -31,7 +31,7 @@ application state. Instead use one of the unsafe HTTP methods which Spring defau
The following examples show Stapler web methods allowing safe HTTP request methods for state-changing actions.
Since safe HTTP request methods do not have default CSRF protection in Stapler, they should not be used when modifying
-application state. Instead use the POST
method which Stapler default-protects from CSRF.
+application state. Instead, use the POST
method which Stapler default-protects from CSRF.
From f4382826740e41c152d5790c48e8b1241c428a51 Mon Sep 17 00:00:00 2001
From: Jami Cogswell
Date: Tue, 4 Feb 2025 13:21:43 -0500
Subject: [PATCH 36/38] Java: rewrite qhelp overview section; aligns with
overview section used by Python and Ruby
---
.../CWE-352/CsrfUnprotectedRequestType.qhelp | 28 +++++++++++++++----
1 file changed, 23 insertions(+), 5 deletions(-)
diff --git a/java/ql/src/Security/CWE/CWE-352/CsrfUnprotectedRequestType.qhelp b/java/ql/src/Security/CWE/CWE-352/CsrfUnprotectedRequestType.qhelp
index 08df43e220d1..d2c29bf2a3d9 100644
--- a/java/ql/src/Security/CWE/CWE-352/CsrfUnprotectedRequestType.qhelp
+++ b/java/ql/src/Security/CWE/CWE-352/CsrfUnprotectedRequestType.qhelp
@@ -2,11 +2,29 @@
-When you set up a web server to receive a request from a client without any mechanism
-for verifying that it was intentionally sent, then it is vulnerable to a Cross-Site Request
-Forgery (CSRF) attack. An attacker can trick a client into making an unintended request
-to the web server that will be treated as an authentic request. This can be done via a URL,
-image load, XMLHttpRequest, etc. and can result in exposure of data or unintended code execution.
+
+ Cross-site request forgery (CSRF) is a type of vulnerability in which an
+ attacker is able to force a user to carry out an action that the user did
+ not intend.
+
+
+
+ The attacker tricks an authenticated user into submitting a request to the
+ web application. Typically this request will result in a state change on
+ the server, such as changing the user's password. The request can be
+ initiated when the user visits a site controlled by the attacker. If the
+ web application relies only on cookies for authentication, or on other
+ credentials that are automatically included in the request, then this
+ request will appear as legitimate to the server.
+
+
+
+ A common countermeasure for CSRF is to generate a unique token to be
+ included in the HTML sent from the server to a user. This token can be
+ used as a hidden field to be sent back with requests to the server, where
+ the server can then check that the token is valid and associated with the
+ relevant user session.
+
From 03678463338df097c7a6af0339e9b9635e9b1e30 Mon Sep 17 00:00:00 2001
From: Jami Cogswell
Date: Tue, 4 Feb 2025 13:36:15 -0500
Subject: [PATCH 37/38] Java: remove token section from qhelp overview
discussing tokens is not directly relevant to this query's recommendation and examples
---
.../Security/CWE/CWE-352/CsrfUnprotectedRequestType.qhelp | 8 --------
1 file changed, 8 deletions(-)
diff --git a/java/ql/src/Security/CWE/CWE-352/CsrfUnprotectedRequestType.qhelp b/java/ql/src/Security/CWE/CWE-352/CsrfUnprotectedRequestType.qhelp
index d2c29bf2a3d9..8555a37f9408 100644
--- a/java/ql/src/Security/CWE/CWE-352/CsrfUnprotectedRequestType.qhelp
+++ b/java/ql/src/Security/CWE/CWE-352/CsrfUnprotectedRequestType.qhelp
@@ -17,14 +17,6 @@
credentials that are automatically included in the request, then this
request will appear as legitimate to the server.
-
-
- A common countermeasure for CSRF is to generate a unique token to be
- included in the HTML sent from the server to a user. This token can be
- used as a hidden field to be sent back with requests to the server, where
- the server can then check that the token is valid and associated with the
- relevant user session.
-
From c6a71cd3fd475f7bd9f7128eb0fe059d43299787 Mon Sep 17 00:00:00 2001
From: Jami Cogswell
Date: Wed, 5 Feb 2025 10:20:57 -0500
Subject: [PATCH 38/38] Java: minor qhelp updates
---
.../src/Security/CWE/CWE-352/CsrfUnprotectedRequestType.qhelp | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/java/ql/src/Security/CWE/CWE-352/CsrfUnprotectedRequestType.qhelp b/java/ql/src/Security/CWE/CWE-352/CsrfUnprotectedRequestType.qhelp
index 8555a37f9408..137243c3f5ba 100644
--- a/java/ql/src/Security/CWE/CWE-352/CsrfUnprotectedRequestType.qhelp
+++ b/java/ql/src/Security/CWE/CWE-352/CsrfUnprotectedRequestType.qhelp
@@ -10,7 +10,7 @@
The attacker tricks an authenticated user into submitting a request to the
- web application. Typically this request will result in a state change on
+ web application. Typically, this request will result in a state change on
the server, such as changing the user's password. The request can be
initiated when the user visits a site controlled by the attacker. If the
web application relies only on cookies for authentication, or on other
@@ -51,7 +51,7 @@ application state. Instead, use the POST
method which Stapler defau
OWASP:
-Cross-Site Request Forgery (CSRF) .
+Cross Site Request Forgery (CSRF) .
Spring Security Reference: