mirror of
https://github.com/github/codeql.git
synced 2026-05-01 11:45:14 +02:00
Merge remote-tracking branch 'origin/main' into smowton/admin/merge-rc317-into-main
This commit is contained in:
5
java/ql/lib/change-notes/2025-03-03-maven-fixes.md
Normal file
5
java/ql/lib/change-notes/2025-03-03-maven-fixes.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
category: fix
|
||||
---
|
||||
* Java build-mode `none` no longer fails when a required version of Maven cannot be downloaded, such as due to a firewall. It will now attempt to use the system version of Maven if present, or otherwise proceed without detailed dependency information.
|
||||
* Java build-mode `none` now correctly uses Maven dependency information on Windows platforms.
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
category: minorAnalysis
|
||||
---
|
||||
* Added a path injection sanitizer for calls to `java.lang.String.matches`, `java.lang.String.replace`, and `java.lang.String.replaceAll` that make sure '/', '\', '..' are not in the path.
|
||||
4
java/ql/lib/change-notes/2025-03-18-cyclic-types.md
Normal file
4
java/ql/lib/change-notes/2025-03-18-cyclic-types.md
Normal file
@@ -0,0 +1,4 @@
|
||||
---
|
||||
category: fix
|
||||
---
|
||||
* Java extraction no longer freezes for a long time or times out when using libraries that feature expanding cyclic generic types. For example, this was known to occur when using some classes from the Blazebit Persistence library.
|
||||
4
java/ql/lib/change-notes/2025-03-18-gradle-fixes.md
Normal file
4
java/ql/lib/change-notes/2025-03-18-gradle-fixes.md
Normal file
@@ -0,0 +1,4 @@
|
||||
---
|
||||
category: fix
|
||||
---
|
||||
* Java build-mode `none` no longer fails when a required version of Gradle cannot be downloaded using the `gradle wrapper` command, such as due to a firewall. It will now attempt to use the system version of Gradle if present, or otherwise proceed without detailed dependency information.
|
||||
4
java/ql/lib/change-notes/2025-03-18-maven-enforcer.md
Normal file
4
java/ql/lib/change-notes/2025-03-18-maven-enforcer.md
Normal file
@@ -0,0 +1,4 @@
|
||||
---
|
||||
category: minorAnalysis
|
||||
---
|
||||
* Java extraction is now able to download Maven 3.9.x if a Maven Enforcer Plugin configuration indicates it is necessary. Maven 3.8.x is still preferred if the enforcer-plugin configuration (if any) permits it.
|
||||
@@ -45,6 +45,36 @@ class StringContainsMethod extends Method {
|
||||
}
|
||||
}
|
||||
|
||||
/** A call to the `java.lang.String.matches` method. */
|
||||
class StringMatchesCall extends MethodCall {
|
||||
StringMatchesCall() {
|
||||
exists(Method m | m = this.getMethod() |
|
||||
m.getDeclaringType() instanceof TypeString and
|
||||
m.hasName("matches")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/** A call to the `java.lang.String.replaceAll` method. */
|
||||
class StringReplaceAllCall extends MethodCall {
|
||||
StringReplaceAllCall() {
|
||||
exists(Method m | m = this.getMethod() |
|
||||
m.getDeclaringType() instanceof TypeString and
|
||||
m.hasName("replaceAll")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/** A call to the `java.lang.String.replace` method. */
|
||||
class StringReplaceCall extends MethodCall {
|
||||
StringReplaceCall() {
|
||||
exists(Method m | m = this.getMethod() |
|
||||
m.getDeclaringType() instanceof TypeString and
|
||||
m.hasName("replace")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The methods on the class `java.lang.String` that are used to perform partial matches with a specified substring or char.
|
||||
*/
|
||||
|
||||
@@ -168,12 +168,15 @@ private module SsaInput implements SsaImplCommon::InputSig<Location> {
|
||||
* Holds if the `i`th of basic block `bb` reads source variable `v`.
|
||||
*/
|
||||
predicate variableRead(BasicBlock bb, int i, SourceVariable v, boolean certain) {
|
||||
exists(VarRead use |
|
||||
v.getAnAccess() = use and bb.getNode(i) = use.getControlFlowNode() and certain = true
|
||||
hasDominanceInformation(bb) and
|
||||
(
|
||||
exists(VarRead use |
|
||||
v.getAnAccess() = use and bb.getNode(i) = use.getControlFlowNode() and certain = true
|
||||
)
|
||||
or
|
||||
variableCapture(v, _, bb, i) and
|
||||
certain = false
|
||||
)
|
||||
or
|
||||
variableCapture(v, _, bb, i) and
|
||||
certain = false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -36,14 +36,12 @@ module SsaFlow {
|
||||
TExplicitParameterNode(result.(Impl::ParameterNode).getParameter()) = n
|
||||
}
|
||||
|
||||
predicate localFlowStep(
|
||||
SsaImpl::Impl::DefinitionExt def, Node nodeFrom, Node nodeTo, boolean isUseStep
|
||||
) {
|
||||
Impl::localFlowStep(def, asNode(nodeFrom), asNode(nodeTo), isUseStep)
|
||||
predicate localFlowStep(SsaSourceVariable v, Node nodeFrom, Node nodeTo, boolean isUseStep) {
|
||||
Impl::localFlowStep(v, asNode(nodeFrom), asNode(nodeTo), isUseStep)
|
||||
}
|
||||
|
||||
predicate localMustFlowStep(SsaImpl::Impl::DefinitionExt def, Node nodeFrom, Node nodeTo) {
|
||||
Impl::localMustFlowStep(def, asNode(nodeFrom), asNode(nodeTo))
|
||||
predicate localMustFlowStep(Node nodeFrom, Node nodeTo) {
|
||||
Impl::localMustFlowStep(_, asNode(nodeFrom), asNode(nodeTo))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -168,7 +168,7 @@ predicate localMustFlowStep(Node node1, Node node2) {
|
||||
node2.(ImplicitInstanceAccess).getInstanceAccess().(OwnInstanceAccess).getEnclosingCallable()
|
||||
)
|
||||
or
|
||||
SsaFlow::localMustFlowStep(_, node1, node2)
|
||||
SsaFlow::localMustFlowStep(node1, node2)
|
||||
or
|
||||
node2.asExpr().(CastingExpr).getExpr() = node1.asExpr()
|
||||
or
|
||||
|
||||
@@ -204,12 +204,15 @@ private module SsaInput implements SsaImplCommon::InputSig<Location> {
|
||||
* This includes implicit reads via calls.
|
||||
*/
|
||||
predicate variableRead(BasicBlock bb, int i, SourceVariable v, boolean certain) {
|
||||
exists(VarRead use |
|
||||
v.getAnAccess() = use and bb.getNode(i) = use.getControlFlowNode() and certain = true
|
||||
hasDominanceInformation(bb) and
|
||||
(
|
||||
exists(VarRead use |
|
||||
v.getAnAccess() = use and bb.getNode(i) = use.getControlFlowNode() and certain = true
|
||||
)
|
||||
or
|
||||
variableCapture(v, _, bb, i) and
|
||||
certain = false
|
||||
)
|
||||
or
|
||||
variableCapture(v, _, bb, i) and
|
||||
certain = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -544,15 +547,13 @@ private module Cached {
|
||||
import DataFlowIntegrationImpl
|
||||
|
||||
cached
|
||||
predicate localFlowStep(Impl::DefinitionExt def, Node nodeFrom, Node nodeTo, boolean isUseStep) {
|
||||
not def instanceof UntrackedDef and
|
||||
DataFlowIntegrationImpl::localFlowStep(def, nodeFrom, nodeTo, isUseStep)
|
||||
predicate localFlowStep(TrackedVar v, Node nodeFrom, Node nodeTo, boolean isUseStep) {
|
||||
DataFlowIntegrationImpl::localFlowStep(v, nodeFrom, nodeTo, isUseStep)
|
||||
}
|
||||
|
||||
cached
|
||||
predicate localMustFlowStep(Impl::DefinitionExt def, Node nodeFrom, Node nodeTo) {
|
||||
not def instanceof UntrackedDef and
|
||||
DataFlowIntegrationImpl::localMustFlowStep(def, nodeFrom, nodeTo)
|
||||
predicate localMustFlowStep(TrackedVar v, Node nodeFrom, Node nodeTo) {
|
||||
DataFlowIntegrationImpl::localMustFlowStep(v, nodeFrom, nodeTo)
|
||||
}
|
||||
|
||||
signature predicate guardChecksSig(Guards::Guard g, Expr e, boolean branch);
|
||||
@@ -669,10 +670,13 @@ private module DataFlowIntegrationInput implements Impl::DataFlowIntegrationInpu
|
||||
}
|
||||
|
||||
class Guard extends Guards::Guard {
|
||||
predicate hasCfgNode(BasicBlock bb, int i) {
|
||||
this = bb.getNode(i).asExpr()
|
||||
or
|
||||
this = bb.getNode(i).asStmt()
|
||||
/**
|
||||
* Holds if the control flow branching from `bb1` is dependent on this guard,
|
||||
* and that the edge from `bb1` to `bb2` corresponds to the evaluation of this
|
||||
* guard to `branch`.
|
||||
*/
|
||||
predicate controlsBranchEdge(BasicBlock bb1, BasicBlock bb2, boolean branch) {
|
||||
super.hasBranchEdge(bb1, bb2, branch)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -680,11 +684,6 @@ private module DataFlowIntegrationInput implements Impl::DataFlowIntegrationInpu
|
||||
predicate guardControlsBlock(Guard guard, BasicBlock bb, boolean branch) {
|
||||
guard.controls(bb, branch)
|
||||
}
|
||||
|
||||
/** Gets an immediate conditional successor of basic block `bb`, if any. */
|
||||
BasicBlock getAConditionalBasicBlockSuccessor(BasicBlock bb, boolean branch) {
|
||||
result = bb.(Guards::ConditionBlock).getTestSuccessor(branch)
|
||||
}
|
||||
}
|
||||
|
||||
private module DataFlowIntegrationImpl = Impl::DataFlowIntegration<DataFlowIntegrationInput>;
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* Provides classes for working with Spring classes and interfaces from
|
||||
* `org.springframework.boot.*`.
|
||||
*/
|
||||
|
||||
import java
|
||||
|
||||
/**
|
||||
* The class `org.springframework.boot.actuate.autoconfigure.security.servlet.EndpointRequest`.
|
||||
*/
|
||||
class SpringEndpointRequest extends Class {
|
||||
SpringEndpointRequest() {
|
||||
this.hasQualifiedName("org.springframework.boot.actuate.autoconfigure.security.servlet",
|
||||
"EndpointRequest")
|
||||
}
|
||||
}
|
||||
|
||||
/** A call to `EndpointRequest.toAnyEndpoint` method. */
|
||||
class SpringToAnyEndpointCall extends MethodCall {
|
||||
SpringToAnyEndpointCall() {
|
||||
this.getMethod().hasName("toAnyEndpoint") and
|
||||
this.getMethod().getDeclaringType() instanceof SpringEndpointRequest
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
/**
|
||||
* Provides classes for working with Spring classes and interfaces from
|
||||
* `org.springframework.security.*`.
|
||||
*/
|
||||
|
||||
import java
|
||||
|
||||
/** The class `org.springframework.security.config.annotation.web.builders.HttpSecurity`. */
|
||||
class SpringHttpSecurity extends Class {
|
||||
SpringHttpSecurity() {
|
||||
this.hasQualifiedName("org.springframework.security.config.annotation.web.builders",
|
||||
"HttpSecurity")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The class
|
||||
* `org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer$AuthorizedUrl`
|
||||
* or the class
|
||||
* `org.springframework.security.config.annotation.web.configurers.AuthorizeHttpRequestsConfigurer$AuthorizedUrl`.
|
||||
*/
|
||||
class SpringAuthorizedUrl extends Class {
|
||||
SpringAuthorizedUrl() {
|
||||
this.hasQualifiedName("org.springframework.security.config.annotation.web.configurers",
|
||||
[
|
||||
"ExpressionUrlAuthorizationConfigurer<HttpSecurity>$AuthorizedUrl<>",
|
||||
"AuthorizeHttpRequestsConfigurer<HttpSecurity>$AuthorizedUrl<>"
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The class `org.springframework.security.config.annotation.web.AbstractRequestMatcherRegistry`.
|
||||
*/
|
||||
class SpringAbstractRequestMatcherRegistry extends Class {
|
||||
SpringAbstractRequestMatcherRegistry() {
|
||||
this.hasQualifiedName("org.springframework.security.config.annotation.web",
|
||||
"AbstractRequestMatcherRegistry<AuthorizedUrl<>>")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A call to the `HttpSecurity.authorizeRequests` method.
|
||||
*
|
||||
* Note: this method is deprecated and scheduled for removal
|
||||
* in Spring Security 7.0.
|
||||
*/
|
||||
class SpringAuthorizeRequestsCall extends MethodCall {
|
||||
SpringAuthorizeRequestsCall() {
|
||||
this.getMethod().hasName("authorizeRequests") and
|
||||
this.getMethod().getDeclaringType() instanceof SpringHttpSecurity
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A call to the `HttpSecurity.authorizeHttpRequests` method.
|
||||
*
|
||||
* Note: the no-argument version of this method is deprecated
|
||||
* and scheduled for removal in Spring Security 7.0.
|
||||
*/
|
||||
class SpringAuthorizeHttpRequestsCall extends MethodCall {
|
||||
SpringAuthorizeHttpRequestsCall() {
|
||||
this.getMethod().hasName("authorizeHttpRequests") and
|
||||
this.getMethod().getDeclaringType() instanceof SpringHttpSecurity
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A call to the `HttpSecurity.requestMatcher` method.
|
||||
*
|
||||
* Note: this method was removed in Spring Security 6.0.
|
||||
* It was replaced by `securityMatcher`.
|
||||
*/
|
||||
class SpringRequestMatcherCall extends MethodCall {
|
||||
SpringRequestMatcherCall() {
|
||||
this.getMethod().hasName("requestMatcher") and
|
||||
this.getMethod().getDeclaringType() instanceof SpringHttpSecurity
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A call to the `HttpSecurity.requestMatchers` method.
|
||||
*
|
||||
* Note: this method was removed in Spring Security 6.0.
|
||||
* It was replaced by `securityMatchers`.
|
||||
*/
|
||||
class SpringRequestMatchersCall extends MethodCall {
|
||||
SpringRequestMatchersCall() {
|
||||
this.getMethod().hasName("requestMatchers") and
|
||||
this.getMethod().getDeclaringType() instanceof SpringHttpSecurity
|
||||
}
|
||||
}
|
||||
|
||||
/** A call to the `HttpSecurity.securityMatcher` method. */
|
||||
class SpringSecurityMatcherCall extends MethodCall {
|
||||
SpringSecurityMatcherCall() {
|
||||
this.getMethod().hasName("securityMatcher") and
|
||||
this.getMethod().getDeclaringType() instanceof SpringHttpSecurity
|
||||
}
|
||||
}
|
||||
|
||||
/** A call to the `HttpSecurity.securityMatchers` method. */
|
||||
class SpringSecurityMatchersCall extends MethodCall {
|
||||
SpringSecurityMatchersCall() {
|
||||
this.getMethod().hasName("securityMatchers") and
|
||||
this.getMethod().getDeclaringType() instanceof SpringHttpSecurity
|
||||
}
|
||||
}
|
||||
|
||||
/** A call to the `AuthorizedUrl.permitAll` method. */
|
||||
class SpringPermitAllCall extends MethodCall {
|
||||
SpringPermitAllCall() {
|
||||
this.getMethod().hasName("permitAll") and
|
||||
this.getMethod().getDeclaringType() instanceof SpringAuthorizedUrl
|
||||
}
|
||||
}
|
||||
|
||||
/** A call to the `AbstractRequestMatcherRegistry.anyRequest` method. */
|
||||
class SpringAnyRequestCall extends MethodCall {
|
||||
SpringAnyRequestCall() {
|
||||
this.getMethod().hasName("anyRequest") and
|
||||
this.getMethod().getDeclaringType() instanceof SpringAbstractRequestMatcherRegistry
|
||||
}
|
||||
}
|
||||
@@ -51,7 +51,9 @@ private module VerifiedIntentFlow = DataFlow::Global<VerifiedIntentConfig>;
|
||||
/** An `onReceive` method that doesn't verify the action of the intent it receives. */
|
||||
private class UnverifiedOnReceiveMethod extends OnReceiveMethod {
|
||||
UnverifiedOnReceiveMethod() {
|
||||
not VerifiedIntentFlow::flow(DataFlow::parameterNode(this.getIntentParameter()), _)
|
||||
not VerifiedIntentFlow::flow(DataFlow::parameterNode(this.getIntentParameter()), _) and
|
||||
// Empty methods do not need to be verified since they do not perform any actions.
|
||||
this.getBody().getNumStmt() > 0
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -383,3 +383,178 @@ private class FileConstructorChildArgumentStep extends AdditionalTaintStep {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/** A call to `java.lang.String.replace` or `java.lang.String.replaceAll`. */
|
||||
private class StringReplaceOrReplaceAllCall extends MethodCall {
|
||||
StringReplaceOrReplaceAllCall() {
|
||||
this instanceof StringReplaceCall or
|
||||
this instanceof StringReplaceAllCall
|
||||
}
|
||||
}
|
||||
|
||||
/** Gets a character used for replacement. */
|
||||
private string getAReplacementChar() { result = ["", "_", "-"] }
|
||||
|
||||
/** Gets a directory character represented as regex. */
|
||||
private string getADirRegexChar() { result = ["\\.", "/", "\\\\"] }
|
||||
|
||||
/** Gets a directory character represented as a char. */
|
||||
private string getADirChar() { result = [".", "/", "\\"] }
|
||||
|
||||
/** Holds if `target` is the first argument of `replaceAllCall`. */
|
||||
private predicate isReplaceAllTarget(
|
||||
StringReplaceAllCall replaceAllCall, CompileTimeConstantExpr target
|
||||
) {
|
||||
target = replaceAllCall.getArgument(0)
|
||||
}
|
||||
|
||||
/** Holds if `target` is the first argument of `replaceCall`. */
|
||||
private predicate isReplaceTarget(StringReplaceCall replaceCall, CompileTimeConstantExpr target) {
|
||||
target = replaceCall.getArgument(0)
|
||||
}
|
||||
|
||||
/** Holds if a single `replaceAllCall` replaces all directory characters. */
|
||||
private predicate replacesDirectoryCharactersWithSingleReplaceAll(
|
||||
StringReplaceAllCall replaceAllCall
|
||||
) {
|
||||
exists(CompileTimeConstantExpr target, string targetValue |
|
||||
isReplaceAllTarget(replaceAllCall, target) and
|
||||
target.getStringValue() = targetValue and
|
||||
replaceAllCall.getArgument(1).(CompileTimeConstantExpr).getStringValue() = getAReplacementChar()
|
||||
|
|
||||
not targetValue.matches("%[^%]%") and
|
||||
targetValue.matches("[%.%]") and
|
||||
targetValue.matches("[%/%]") and
|
||||
// Search for "\\\\" (needs extra backslashes to avoid escaping the '%')
|
||||
targetValue.matches("[%\\\\\\\\%]")
|
||||
or
|
||||
targetValue.matches("%|%") and
|
||||
targetValue.matches("%" + ["[.]", "\\."] + "%") and
|
||||
targetValue.matches("%/%") and
|
||||
targetValue.matches("%\\\\\\\\%")
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if there are two chained replacement calls, `rc1` and `rc2`, that replace
|
||||
* '.' and one of '/' or '\'.
|
||||
*/
|
||||
private predicate replacesDirectoryCharactersWithDoubleReplaceOrReplaceAll(
|
||||
StringReplaceOrReplaceAllCall rc1
|
||||
) {
|
||||
exists(
|
||||
CompileTimeConstantExpr target1, string targetValue1, StringReplaceOrReplaceAllCall rc2,
|
||||
CompileTimeConstantExpr target2, string targetValue2
|
||||
|
|
||||
rc1 instanceof StringReplaceAllCall and
|
||||
isReplaceAllTarget(rc1, target1) and
|
||||
isReplaceAllTarget(rc2, target2) and
|
||||
targetValue1 = getADirRegexChar() and
|
||||
targetValue2 = getADirRegexChar()
|
||||
or
|
||||
rc1 instanceof StringReplaceCall and
|
||||
isReplaceTarget(rc1, target1) and
|
||||
isReplaceTarget(rc2, target2) and
|
||||
targetValue1 = getADirChar() and
|
||||
targetValue2 = getADirChar()
|
||||
|
|
||||
rc2.getQualifier() = rc1 and
|
||||
target1.getStringValue() = targetValue1 and
|
||||
target2.getStringValue() = targetValue2 and
|
||||
rc1.getArgument(1).(CompileTimeConstantExpr).getStringValue() = getAReplacementChar() and
|
||||
rc2.getArgument(1).(CompileTimeConstantExpr).getStringValue() = getAReplacementChar() and
|
||||
// make sure the calls replace different characters
|
||||
targetValue2 != targetValue1 and
|
||||
// make sure one of the calls replaces '.'
|
||||
// then the other call must replace one of '/' or '\' if they are not equal
|
||||
(targetValue2.matches("%.%") or targetValue1.matches("%.%"))
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* A sanitizer that protects against path injection vulnerabilities by replacing
|
||||
* directory characters ('..', '/', and '\') with safe characters.
|
||||
*/
|
||||
private class ReplaceDirectoryCharactersSanitizer extends StringReplaceOrReplaceAllCall {
|
||||
ReplaceDirectoryCharactersSanitizer() {
|
||||
replacesDirectoryCharactersWithSingleReplaceAll(this) or
|
||||
replacesDirectoryCharactersWithDoubleReplaceOrReplaceAll(this)
|
||||
}
|
||||
}
|
||||
|
||||
/** Holds if `target` is the first argument of `matchesCall`. */
|
||||
private predicate isMatchesTarget(StringMatchesCall matchesCall, CompileTimeConstantExpr target) {
|
||||
target = matchesCall.getArgument(0)
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if `matchesCall` confirms that `checkedExpr` does not contain any directory characters
|
||||
* on the given `branch`.
|
||||
*/
|
||||
private predicate isMatchesCall(StringMatchesCall matchesCall, Expr checkedExpr, boolean branch) {
|
||||
exists(CompileTimeConstantExpr target, string targetValue |
|
||||
isMatchesTarget(matchesCall, target) and
|
||||
target.getStringValue() = targetValue and
|
||||
checkedExpr = matchesCall.getQualifier()
|
||||
|
|
||||
(
|
||||
// Allow anything except `.`, '/', '\'
|
||||
targetValue.matches(["[%]*", "[%]+", "[%]{%}"]) and
|
||||
(
|
||||
// Note: we do not account for when '.', '/', '\' are inside a character range
|
||||
not targetValue.matches("[%" + [".", "/", "\\\\\\\\"] + "%]%") and
|
||||
not targetValue.matches("%[^%]%")
|
||||
or
|
||||
targetValue.matches("[^%.%]%") and
|
||||
targetValue.matches("[^%/%]%") and
|
||||
targetValue.matches("[^%\\\\\\\\%]%")
|
||||
) and
|
||||
branch = true
|
||||
or
|
||||
// Disallow `.`, '/', '\'
|
||||
targetValue.matches([".*[%].*", ".+[%].+"]) and
|
||||
targetValue.matches("%[%.%]%") and
|
||||
targetValue.matches("%[%/%]%") and
|
||||
targetValue.matches("%[%\\\\\\\\%]%") and
|
||||
not targetValue.matches("%[^%]%") and
|
||||
branch = false
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* A guard that protects against path traversal by looking for patterns
|
||||
* that exclude directory characters: `..`, '/', and '\'.
|
||||
*/
|
||||
private class DirectoryCharactersGuard extends PathGuard {
|
||||
Expr checkedExpr;
|
||||
boolean branch;
|
||||
|
||||
DirectoryCharactersGuard() { isMatchesCall(this, checkedExpr, branch) }
|
||||
|
||||
override Expr getCheckedExpr() { result = checkedExpr }
|
||||
|
||||
boolean getBranch() { result = branch }
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if `g` is a guard that considers a path safe because it is checked to make
|
||||
* sure it does not contain any directory characters: '..', '/', and '\'.
|
||||
*/
|
||||
private predicate directoryCharactersGuard(Guard g, Expr e, boolean branch) {
|
||||
branch = g.(DirectoryCharactersGuard).getBranch() and
|
||||
localTaintFlowToPathGuard(e, g)
|
||||
}
|
||||
|
||||
/**
|
||||
* A sanitizer that protects against path injection vulnerabilities
|
||||
* by ensuring that the path does not contain any directory characters:
|
||||
* '..', '/', and '\'.
|
||||
*/
|
||||
private class DirectoryCharactersSanitizer extends PathInjectionSanitizer {
|
||||
DirectoryCharactersSanitizer() {
|
||||
this.asExpr() instanceof ReplaceDirectoryCharactersSanitizer or
|
||||
this = DataFlow::BarrierGuard<directoryCharactersGuard/3>::getABarrierNode() or
|
||||
this = ValidationMethod<directoryCharactersGuard/3>::getAValidatedNode()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
/** Provides classes and predicates to reason about exposed actuators in Spring Boot. */
|
||||
|
||||
import java
|
||||
private import semmle.code.java.frameworks.spring.SpringSecurity
|
||||
private import semmle.code.java.frameworks.spring.SpringBoot
|
||||
|
||||
/**
|
||||
* A call to an `HttpSecurity` matcher method with argument
|
||||
* `EndpointRequest.toAnyEndpoint()`.
|
||||
*/
|
||||
private class HttpSecurityMatcherCall extends MethodCall {
|
||||
HttpSecurityMatcherCall() {
|
||||
(
|
||||
this instanceof SpringRequestMatcherCall or
|
||||
this instanceof SpringSecurityMatcherCall
|
||||
) and
|
||||
this.getArgument(0) instanceof SpringToAnyEndpointCall
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A call to an `HttpSecurity` matchers method with lambda
|
||||
* argument `EndpointRequest.toAnyEndpoint()`.
|
||||
*/
|
||||
private class HttpSecurityMatchersCall extends MethodCall {
|
||||
HttpSecurityMatchersCall() {
|
||||
(
|
||||
this instanceof SpringRequestMatchersCall or
|
||||
this instanceof SpringSecurityMatchersCall
|
||||
) and
|
||||
this.getArgument(0).(LambdaExpr).getExprBody() instanceof SpringToAnyEndpointCall
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A call to an `AbstractRequestMatcherRegistry.requestMatchers` method with
|
||||
* argument `EndpointRequest.toAnyEndpoint()`.
|
||||
*/
|
||||
private class RegistryRequestMatchersCall extends MethodCall {
|
||||
RegistryRequestMatchersCall() {
|
||||
this.getMethod().hasName("requestMatchers") and
|
||||
this.getMethod().getDeclaringType() instanceof SpringAbstractRequestMatcherRegistry and
|
||||
this.getAnArgument() instanceof SpringToAnyEndpointCall
|
||||
}
|
||||
}
|
||||
|
||||
/** A call to an `HttpSecurity` method that authorizes requests. */
|
||||
private class AuthorizeCall extends MethodCall {
|
||||
AuthorizeCall() {
|
||||
this instanceof SpringAuthorizeRequestsCall or
|
||||
this instanceof SpringAuthorizeHttpRequestsCall
|
||||
}
|
||||
}
|
||||
|
||||
/** Holds if `permitAllCall` is called on request(s) mapped to actuator endpoint(s). */
|
||||
predicate permitsSpringBootActuators(SpringPermitAllCall permitAllCall) {
|
||||
exists(AuthorizeCall authorizeCall |
|
||||
// .requestMatcher(EndpointRequest).authorizeRequests([...]).[...]
|
||||
authorizeCall.getQualifier() instanceof HttpSecurityMatcherCall
|
||||
or
|
||||
// .requestMatchers(matcher -> EndpointRequest).authorizeRequests([...]).[...]
|
||||
authorizeCall.getQualifier() instanceof HttpSecurityMatchersCall
|
||||
|
|
||||
// [...].authorizeRequests(r -> r.anyRequest().permitAll()) or
|
||||
// [...].authorizeRequests(r -> r.requestMatchers(EndpointRequest).permitAll())
|
||||
authorizeCall.getArgument(0).(LambdaExpr).getExprBody() = permitAllCall and
|
||||
(
|
||||
permitAllCall.getQualifier() instanceof SpringAnyRequestCall or
|
||||
permitAllCall.getQualifier() instanceof RegistryRequestMatchersCall
|
||||
)
|
||||
or
|
||||
// [...].authorizeRequests().requestMatchers(EndpointRequest).permitAll() or
|
||||
// [...].authorizeRequests().anyRequest().permitAll()
|
||||
authorizeCall.getNumArgument() = 0 and
|
||||
exists(RegistryRequestMatchersCall registryRequestMatchersCall |
|
||||
registryRequestMatchersCall.getQualifier() = authorizeCall and
|
||||
permitAllCall.getQualifier() = registryRequestMatchersCall
|
||||
)
|
||||
or
|
||||
exists(SpringAnyRequestCall anyRequestCall |
|
||||
anyRequestCall.getQualifier() = authorizeCall and
|
||||
permitAllCall.getQualifier() = anyRequestCall
|
||||
)
|
||||
)
|
||||
or
|
||||
exists(AuthorizeCall authorizeCall |
|
||||
// http.authorizeRequests([...]).[...]
|
||||
authorizeCall.getQualifier() instanceof VarAccess
|
||||
|
|
||||
// [...].authorizeRequests(r -> r.requestMatchers(EndpointRequest).permitAll())
|
||||
authorizeCall.getArgument(0).(LambdaExpr).getExprBody() = permitAllCall and
|
||||
permitAllCall.getQualifier() instanceof RegistryRequestMatchersCall
|
||||
or
|
||||
// [...].authorizeRequests().requestMatchers(EndpointRequest).permitAll() or
|
||||
authorizeCall.getNumArgument() = 0 and
|
||||
exists(RegistryRequestMatchersCall registryRequestMatchersCall |
|
||||
registryRequestMatchersCall.getQualifier() = authorizeCall and
|
||||
permitAllCall.getQualifier() = registryRequestMatchersCall
|
||||
)
|
||||
or
|
||||
exists(Variable v, HttpSecurityMatcherCall matcherCall |
|
||||
// http.securityMatcher(EndpointRequest.toAnyEndpoint());
|
||||
// http.authorizeRequests([...].permitAll())
|
||||
v.getAnAccess() = authorizeCall.getQualifier() and
|
||||
v.getAnAccess() = matcherCall.getQualifier() and
|
||||
authorizeCall.getArgument(0).(LambdaExpr).getExprBody() = permitAllCall and
|
||||
permitAllCall.getQualifier() instanceof SpringAnyRequestCall
|
||||
)
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user