Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ enum class ExperimentalFaultCategory(
SECURITY_FORGOTTEN_AUTHENTICATION(980, "A Protected Resource Is Accessible Without Providing Any Authentication",
"forgottenAuthentication",
"TODO"),
SECURITY_STACK_TRACE(981, "Stack Trace",
"stackTrace",
"TODO"),
;

override fun getCode(): Int {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import org.evomaster.core.EMConfig
import javax.annotation.PostConstruct

import org.evomaster.core.logging.LoggingUtil
import org.evomaster.core.problem.enterprise.DetectedFault
import org.evomaster.core.problem.enterprise.ExperimentalFaultCategory
import org.evomaster.core.problem.enterprise.SampleType
import org.evomaster.core.problem.enterprise.auth.AuthSettings
Expand All @@ -22,10 +23,12 @@ import org.evomaster.core.problem.rest.resource.RestResourceCalls
import org.evomaster.core.problem.rest.service.sampler.AbstractRestSampler

import org.evomaster.core.search.*
import org.evomaster.core.search.action.ActionResult
import org.evomaster.core.search.service.Archive
import org.evomaster.core.search.service.FitnessFunction
import org.evomaster.core.search.service.IdMapper
import org.evomaster.core.search.service.Randomness
import org.evomaster.core.utils.StackTraceUtils
import org.slf4j.Logger
import org.slf4j.LoggerFactory

Expand Down Expand Up @@ -253,7 +256,7 @@ class SecurityRest {

private fun accessControlBasedOnRESTGuidelines() {

if(config.getDisabledOracleCodesList().contains(DefinedFaultCategory.SECURITY_WRONG_AUTHORIZATION)){
if (config.getDisabledOracleCodesList().contains(DefinedFaultCategory.SECURITY_WRONG_AUTHORIZATION)) {
LoggingUtil.uniqueUserInfo("Skipping security test for forbidden but ok others as disabled in configuration")
} else {
// quite a few rules here that can be defined
Expand All @@ -262,30 +265,97 @@ class SecurityRest {
handleForbiddenOperationButOKOthers(HttpVerb.PATCH)
}

if(config.getDisabledOracleCodesList().contains(DefinedFaultCategory.SECURITY_EXISTENCE_LEAKAGE)){
if (config.getDisabledOracleCodesList().contains(DefinedFaultCategory.SECURITY_EXISTENCE_LEAKAGE)) {
LoggingUtil.uniqueUserInfo("Skipping security test for existence leakage as disabled in configuration")
} else {
// getting 404 instead of 403
handleExistenceLeakage()
}

if(config.getDisabledOracleCodesList().contains(DefinedFaultCategory.SECURITY_NOT_RECOGNIZED_AUTHENTICATED)){
if (config.getDisabledOracleCodesList().contains(DefinedFaultCategory.SECURITY_NOT_RECOGNIZED_AUTHENTICATED)) {
LoggingUtil.uniqueUserInfo("Skipping security test for not recognized authenticated as disabled in configuration")
} else {
//authenticated, but wrongly getting 401 (eg instead of 403)
handleNotRecognizedAuthenticated()
}

if(config.getDisabledOracleCodesList().contains(ExperimentalFaultCategory.SECURITY_FORGOTTEN_AUTHENTICATION)) {
if (config.getDisabledOracleCodesList().contains(ExperimentalFaultCategory.SECURITY_FORGOTTEN_AUTHENTICATION)) {
LoggingUtil.uniqueUserInfo("Skipping experimental security test for forgotten authentication as disabled in configuration")
} else {
handleForgottenAuthentication()
}

if (config.getDisabledOracleCodesList().contains(ExperimentalFaultCategory.SECURITY_STACK_TRACE)) {
LoggingUtil.uniqueUserInfo("Skipping experimental security test for stack traces as disabled in configuration")
} else {
handleStackTraceCheck()
}

//TODO other rules. See FaultCategory
//etc.
}

/**
* Checks whether any response body contains a stack trace, which would constitute a security issue.
* Stack traces expose internal implementation details that can aid attackers in exploiting vulnerabilities.
*
* Note: This is a best-effort oracle that may produce false positives, as some applications might
* legitimately return stack traces as part of their business logic.
*
* This check is performed only at the end of the search, not during each fitness evaluation,
* to avoid performance overhead during the main search phase.
*/
private fun handleStackTraceCheck(){

mainloop@ for(action in actionDefinitions){
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

check for config.getDisabledOracleCodesList() ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am checking it in accessControlBasedOnRESTGuidelines.


val suspicious = RestIndividualSelectorUtils.findIndividuals(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we need a bit of code comments to explain why we need BOTH the check here and in the fitness function.

  1. here: because we need to make sure 500 from archive are re-executed and added back with SECURITY tag if fault found
  2. in ff: to avoid losing info if test is re-evaluated

individualsInSolution,
action.verb,
action.path,
status = 500,
)

if(suspicious.isEmpty()){
continue
}

for(target in suspicious) {
var isFaultFound = false

val copyTarget = target.copy()
// we need BOTH the check here and in the fitness function.
// here: because we need to make sure 500 from archive are re-executed and added back with SECURITY tag if fault found
// in ff: to avoid losing info if test is re-evaluated

isFaultFound = copyTarget.evaluatedMainActions()
.asSequence()
.filter { (it.action as RestCallAction).verb == action.verb && it.action.path == action.path }
.mapNotNull { it.result as? RestCallResult }
.any { r ->
val body = r.getBody()
r.getStatusCode() == 500 &&
body != null &&
StackTraceUtils.looksLikeStackTrace(body)
}

if(isFaultFound){
copyTarget.individual.modifySampleType(SampleType.SECURITY)
copyTarget.individual.ensureFlattenedStructure()
val evaluatedIndividual = fitness.computeWholeAchievedCoverageForPostProcessing(copyTarget.individual)

if (evaluatedIndividual == null) {
log.warn("Failed to evaluate constructed individual in handleStackTraceCheck")
continue@mainloop
}

val added = archive.addIfNeeded(evaluatedIndividual)
assert(added)
continue@mainloop
}
}
}
}

/**
* Authenticated user A accesses endpoint X, but get 401 (instead of 403).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ import org.evomaster.core.search.gene.string.StringGene
import org.evomaster.core.search.gene.utils.GeneUtils
import org.evomaster.core.search.service.DataPool
import org.evomaster.core.taint.TaintAnalysis
import org.evomaster.core.utils.StackTraceUtils
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import java.net.URL
Expand Down Expand Up @@ -1202,13 +1203,18 @@ abstract class AbstractRestFitness : HttpWsFitness<RestIndividual>() {
handleExistenceLeakage(individual,actionResults,fv)
handleNotRecognizedAuthenticated(individual, actionResults, fv)
handleForgottenAuthentication(individual, actionResults, fv)
handleStackTraceCheck(individual, actionResults, fv)
}

private fun handleSsrfFaults(
individual: RestIndividual,
actionResults: List<ActionResult>,
fv: FitnessValue
) {
if (config.getDisabledOracleCodesList().contains(DefinedFaultCategory.SSRF)) {
return
}

individual.seeMainExecutableActions().forEach {
val ar = (actionResults.find { r -> r.sourceLocalId == it.getLocalId() } as RestCallResult?)
if (ar != null) {
Expand All @@ -1230,6 +1236,9 @@ abstract class AbstractRestFitness : HttpWsFitness<RestIndividual>() {
actionResults: List<ActionResult>,
fv: FitnessValue
) {
if (config.getDisabledOracleCodesList().contains(DefinedFaultCategory.SECURITY_NOT_RECOGNIZED_AUTHENTICATED)) {
return
}

val notRecognized = individual.seeMainExecutableActions()
.filter {
Expand Down Expand Up @@ -1267,6 +1276,10 @@ abstract class AbstractRestFitness : HttpWsFitness<RestIndividual>() {
actionResults: List<ActionResult>,
fv: FitnessValue
) {
if (config.getDisabledOracleCodesList().contains(DefinedFaultCategory.SECURITY_EXISTENCE_LEAKAGE)) {
return
}

val getPaths = individual.seeMainExecutableActions()
.filter { it.verb == HttpVerb.GET }
.map { it.path }
Expand All @@ -1291,11 +1304,40 @@ abstract class AbstractRestFitness : HttpWsFitness<RestIndividual>() {
}
}


private fun handleStackTraceCheck(
individual: RestIndividual,
actionResults: List<ActionResult>,
fv: FitnessValue
) {
if (config.getDisabledOracleCodesList().contains(ExperimentalFaultCategory.SECURITY_STACK_TRACE)) {
return
}

for(index in individual.seeMainExecutableActions().indices){
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldn't be a check for config.getDisabledOracleCodesList()?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added this check for all oracle types in the AbstractRestFitness.

val a = individual.seeMainExecutableActions()[index]
val r = actionResults.find { it.sourceLocalId == a.getLocalId() } as RestCallResult

if(r.getStatusCode() == 500 && r.getBody() != null && StackTraceUtils.looksLikeStackTrace(r.getBody()!!)){
val scenarioId = idMapper.handleLocalTarget(
idMapper.getFaultDescriptiveId(ExperimentalFaultCategory.SECURITY_STACK_TRACE, a.getName())
)
fv.updateTarget(scenarioId, 1.0, index)
r.addFault(DetectedFault(ExperimentalFaultCategory.SECURITY_STACK_TRACE, a.getName(), null))
}
}
}

private fun handleForgottenAuthentication(
individual: RestIndividual,
actionResults: List<ActionResult>,
fv: FitnessValue
) {

if (config.getDisabledOracleCodesList().contains(ExperimentalFaultCategory.SECURITY_FORGOTTEN_AUTHENTICATION)) {
return
}

val endpoints = individual.seeMainExecutableActions()
.map { it.getName() }
.toSet()
Expand Down Expand Up @@ -1327,6 +1369,11 @@ abstract class AbstractRestFitness : HttpWsFitness<RestIndividual>() {
actionResults: List<ActionResult>,
fv: FitnessValue
) {

if (config.getDisabledOracleCodesList().contains(DefinedFaultCategory.SECURITY_WRONG_AUTHORIZATION)) {
return
}

if (RestSecurityOracle.hasForbiddenOperation(verb, individual, actionResults)) {
val actionIndex = individual.size() - 1
val action = individual.seeMainExecutableActions()[actionIndex]
Expand Down
Loading