mirror of
https://github.com/github/codeql.git
synced 2026-04-25 00:35:20 +02:00
Merge pull request #18288 from jcogs33/jcogs33/csrf-unprotected-request-type
Java: add CSRF query
This commit is contained in:
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 `<include>` 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 `<insert>` element in a `MyBatisMapperSqlOperation`.
|
||||
*/
|
||||
class MyBatisMapperInsert extends MyBatisMapperSqlOperation {
|
||||
MyBatisMapperInsert() { this.getName() = "insert" }
|
||||
}
|
||||
|
||||
/**
|
||||
* A `<update>` element in a `MyBatisMapperSqlOperation`.
|
||||
*/
|
||||
class MyBatisMapperUpdate extends MyBatisMapperSqlOperation {
|
||||
MyBatisMapperUpdate() { this.getName() = "update" }
|
||||
}
|
||||
|
||||
/**
|
||||
* A `<delete>` element in a `MyBatisMapperSqlOperation`.
|
||||
*/
|
||||
class MyBatisMapperDelete extends MyBatisMapperSqlOperation {
|
||||
MyBatisMapperDelete() { this.getName() = "delete" }
|
||||
}
|
||||
|
||||
/**
|
||||
* A `<select>` element in a `MyBatisMapperSqlOperation`.
|
||||
*/
|
||||
class MyBatisMapperSelect extends MyBatisMapperSqlOperation {
|
||||
MyBatisMapperSelect() { this.getName() = "select" }
|
||||
}
|
||||
|
||||
/**
|
||||
* A `<sql>` element in a `MyBatisMapperXMLElement`.
|
||||
*/
|
||||
class MyBatisMapperSql extends MyBatisMapperXmlElement {
|
||||
MyBatisMapperSql() { this.getName() = "sql" }
|
||||
|
||||
/**
|
||||
* Gets the value of the `id` attribute of this `<sql>`.
|
||||
*/
|
||||
string getId() { result = this.getAttribute("id").getValue() }
|
||||
}
|
||||
|
||||
/**
|
||||
* A `<include>` element in a `MyBatisMapperXMLElement`.
|
||||
*/
|
||||
class MyBatisMapperInclude extends MyBatisMapperXmlElement {
|
||||
MyBatisMapperInclude() { this.getName() = "include" }
|
||||
|
||||
/**
|
||||
* Gets the value of the `refid` attribute of this `<include>`.
|
||||
*/
|
||||
string getRefid() { result = this.getAttribute("refid").getValue() }
|
||||
}
|
||||
|
||||
/**
|
||||
* A `<foreach>` element in a `MyBatisMapperXMLElement`.
|
||||
*/
|
||||
class MyBatisMapperForeach extends MyBatisMapperXmlElement {
|
||||
MyBatisMapperForeach() { this.getName() = "foreach" }
|
||||
}
|
||||
|
||||
@@ -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 getMethodValue() {
|
||||
result = requestMappingAnnotation.getAnEnumConstantArrayValue("method").getName()
|
||||
}
|
||||
|
||||
/** Holds if this is considered an `@ResponseBody` method. */
|
||||
predicate isResponseBody() {
|
||||
this.getAnAnnotation().getType() instanceof SpringResponseBodyAnnotationType or
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,266 @@
|
||||
/** 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
|
||||
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
|
||||
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 { }
|
||||
|
||||
/**
|
||||
* 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.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").
|
||||
not exists(this.getMethodValue())
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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") and
|
||||
// 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"])
|
||||
}
|
||||
}
|
||||
|
||||
/** 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 NameBasedStateChangeMethod extends Method {
|
||||
NameBasedStateChangeMethod() {
|
||||
this.getName().regexpMatch(getInterestingWordRegex()) and
|
||||
not this.getName().regexpMatch(getUninterestingWordRegex())
|
||||
}
|
||||
}
|
||||
|
||||
/** A method that updates a database. */
|
||||
abstract class DatabaseUpdateMethod extends Method { }
|
||||
|
||||
/** A MyBatis method that updates a database. */
|
||||
private class MyBatisDatabaseUpdateMethod extends DatabaseUpdateMethod {
|
||||
MyBatisDatabaseUpdateMethod() {
|
||||
exists(MyBatisMapperSqlOperation mapperXml |
|
||||
(
|
||||
mapperXml instanceof MyBatisMapperInsert or
|
||||
mapperXml instanceof MyBatisMapperUpdate or
|
||||
mapperXml instanceof MyBatisMapperDelete
|
||||
) 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")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/** A method declared in `java.sql.PreparedStatement` that updates a database. */
|
||||
private class PreparedStatementDatabaseUpdateMethod extends DatabaseUpdateMethod {
|
||||
PreparedStatementDatabaseUpdateMethod() {
|
||||
this instanceof PreparedStatementExecuteUpdateMethod or
|
||||
this instanceof PreparedStatementExecuteLargeUpdateMethod
|
||||
}
|
||||
}
|
||||
|
||||
/** 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
|
||||
this.hasName([
|
||||
"delete", "insert", "update", "batchUpdate", "executeUpdate", "executeLargeUpdate",
|
||||
"execute"
|
||||
])
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A taint-tracking configuration for reasoning about SQL statements that update
|
||||
* a database via a call to an `execute` method.
|
||||
*/
|
||||
private 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 SqlInjectionDatabaseUpdateMethod and
|
||||
m.hasName("execute")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tracks flow from SQL statements that update a database to the argument of
|
||||
* an `execute` method call.
|
||||
*/
|
||||
private module SqlExecuteFlow = TaintTracking::Global<SqlExecuteConfig>;
|
||||
|
||||
/** Provides classes and predicates representing call graph paths. */
|
||||
module CallGraph {
|
||||
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
|
||||
result = this.asCall().toString()
|
||||
}
|
||||
|
||||
private CallPathNode getACallee() {
|
||||
[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
|
||||
(
|
||||
this.canTargetDatabaseUpdateMethod()
|
||||
implies
|
||||
result.asMethod() instanceof DatabaseUpdateMethod
|
||||
)
|
||||
}
|
||||
|
||||
/** Gets the location of this `CallPathNode`. */
|
||||
Location getLocation() {
|
||||
result = this.asMethod().getLocation()
|
||||
or
|
||||
result = this.asCall().getLocation()
|
||||
}
|
||||
}
|
||||
|
||||
/** Holds if `pred` has a successor node `succ`. */
|
||||
predicate edges(CallPathNode pred, CallPathNode succ) { pred.getASuccessor() = succ }
|
||||
}
|
||||
|
||||
/** 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() = sinkMethod and
|
||||
// exclude SQL `execute` calls that do not update database
|
||||
if
|
||||
sinkMethod.asMethod() instanceof SqlInjectionDatabaseUpdateMethod and
|
||||
sinkMethod.asMethod().hasName("execute")
|
||||
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.
|
||||
*/
|
||||
private predicate unprotectedNameBasedStateChange(CallPathNode sourceMethod, CallPathNode sinkMethod) {
|
||||
sourceMethod.asMethod() instanceof CsrfUnprotectedMethod and
|
||||
sinkMethod.asMethod() instanceof NameBasedStateChangeMethod and
|
||||
sinkMethod = sourceMethod and
|
||||
// exclude any alerts that update a database
|
||||
not unprotectedDatabaseUpdate(sourceMethod, _)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
unprotectedNameBasedStateChange(source, sink)
|
||||
}
|
||||
Reference in New Issue
Block a user