Merge remote-tracking branch 'origin/main' into smowton/admin/merge-rc317-into-main

This commit is contained in:
Chris Smowton
2025-03-19 16:01:29 +00:00
2476 changed files with 45571 additions and 29067 deletions

View 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.

View File

@@ -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.

View 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.

View 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.

View 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.

View File

@@ -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.
*/

View File

@@ -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
}
}

View File

@@ -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))
}
}

View File

@@ -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

View File

@@ -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>;

View File

@@ -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
}
}

View File

@@ -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
}
}

View File

@@ -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
}
}

View File

@@ -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()
}
}

View File

@@ -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
)
)
}