-
Notifications
You must be signed in to change notification settings - Fork 97
Stack Trace Oracle #1336
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Stack Trace Oracle #1336
Changes from all commits
79a81d2
57e76dd
b51f710
e1792d7
9d77e53
973bc9c
b9d124d
5b44059
011e6aa
6176898
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 | ||
|
@@ -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 | ||
|
||
|
@@ -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 | ||
|
@@ -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){ | ||
|
||
val suspicious = RestIndividualSelectorUtils.findIndividuals( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
|
||
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). | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 | ||
|
@@ -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) { | ||
|
@@ -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 { | ||
|
@@ -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 } | ||
|
@@ -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){ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. shouldn't be a check for There was a problem hiding this comment. Choose a reason for hiding this commentThe 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() | ||
|
@@ -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] | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
check for
config.getDisabledOracleCodesList()
?There was a problem hiding this comment.
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.