Skip to content

Commit 959194e

Browse files
authored
Merge pull request #1336 from WebFuzzing/stack-trace-oracle
Stack Trace Oracle
2 parents 2532d83 + 42ddf09 commit 959194e

12 files changed

Lines changed: 2512 additions & 0 deletions

File tree

core/src/main/kotlin/org/evomaster/core/problem/enterprise/ExperimentalFaultCategory.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,9 @@ enum class ExperimentalFaultCategory(
5454
SECURITY_FORGOTTEN_AUTHENTICATION(980, "A Protected Resource Is Accessible Without Providing Any Authentication",
5555
"forgottenAuthentication",
5656
"TODO"),
57+
SECURITY_STACK_TRACE(981, "Stack Trace",
58+
"stackTrace",
59+
"TODO"),
5760
;
5861

5962
override fun getCode(): Int {

core/src/main/kotlin/org/evomaster/core/problem/rest/service/SecurityRest.kt

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import org.evomaster.core.EMConfig
66
import javax.annotation.PostConstruct
77

88
import org.evomaster.core.logging.LoggingUtil
9+
import org.evomaster.core.problem.enterprise.DetectedFault
910
import org.evomaster.core.problem.enterprise.ExperimentalFaultCategory
1011
import org.evomaster.core.problem.enterprise.SampleType
1112
import org.evomaster.core.problem.enterprise.auth.AuthSettings
@@ -22,10 +23,12 @@ import org.evomaster.core.problem.rest.resource.RestResourceCalls
2223
import org.evomaster.core.problem.rest.service.sampler.AbstractRestSampler
2324

2425
import org.evomaster.core.search.*
26+
import org.evomaster.core.search.action.ActionResult
2527
import org.evomaster.core.search.service.Archive
2628
import org.evomaster.core.search.service.FitnessFunction
2729
import org.evomaster.core.search.service.IdMapper
2830
import org.evomaster.core.search.service.Randomness
31+
import org.evomaster.core.utils.StackTraceUtils
2932
import org.slf4j.Logger
3033
import org.slf4j.LoggerFactory
3134

@@ -282,10 +285,77 @@ class SecurityRest {
282285
handleForgottenAuthentication()
283286
}
284287

288+
if (!config.isEnabledFaultCategory(ExperimentalFaultCategory.SECURITY_STACK_TRACE)) {
289+
LoggingUtil.uniqueUserInfo("Skipping experimental security test for stack traces as disabled in configuration")
290+
} else {
291+
handleStackTraceCheck()
292+
}
293+
285294
//TODO other rules. See FaultCategory
286295
//etc.
287296
}
288297

298+
/**
299+
* Checks whether any response body contains a stack trace, which would constitute a security issue.
300+
* Stack traces expose internal implementation details that can aid attackers in exploiting vulnerabilities.
301+
*
302+
* Note: This is a best-effort oracle that may produce false positives, as some applications might
303+
* legitimately return stack traces as part of their business logic.
304+
*
305+
* This check is performed only at the end of the search, not during each fitness evaluation,
306+
* to avoid performance overhead during the main search phase.
307+
*/
308+
private fun handleStackTraceCheck(){
309+
310+
mainloop@ for(action in actionDefinitions){
311+
312+
val suspicious = RestIndividualSelectorUtils.findIndividuals(
313+
individualsInSolution,
314+
action.verb,
315+
action.path,
316+
status = 500,
317+
)
318+
319+
if(suspicious.isEmpty()){
320+
continue
321+
}
322+
323+
for(target in suspicious) {
324+
var isFaultFound = false
325+
326+
val copyTarget = target.copy()
327+
// we need BOTH the check here and in the fitness function.
328+
// here: because we need to make sure 500 from archive are re-executed and added back with SECURITY tag if fault found
329+
// in ff: to avoid losing info if test is re-evaluated
330+
331+
isFaultFound = copyTarget.evaluatedMainActions()
332+
.asSequence()
333+
.filter { (it.action as RestCallAction).verb == action.verb && it.action.path == action.path }
334+
.mapNotNull { it.result as? RestCallResult }
335+
.any { r ->
336+
val body = r.getBody()
337+
r.getStatusCode() == 500 &&
338+
body != null &&
339+
StackTraceUtils.looksLikeStackTrace(body)
340+
}
341+
342+
if(isFaultFound){
343+
copyTarget.individual.modifySampleType(SampleType.SECURITY)
344+
copyTarget.individual.ensureFlattenedStructure()
345+
val evaluatedIndividual = fitness.computeWholeAchievedCoverageForPostProcessing(copyTarget.individual)
346+
347+
if (evaluatedIndividual == null) {
348+
log.warn("Failed to evaluate constructed individual in handleStackTraceCheck")
349+
continue@mainloop
350+
}
351+
352+
val added = archive.addIfNeeded(evaluatedIndividual)
353+
assert(added)
354+
continue@mainloop
355+
}
356+
}
357+
}
358+
}
289359

290360
/**
291361
* Authenticated user A accesses endpoint X, but get 401 (instead of 403).

core/src/main/kotlin/org/evomaster/core/problem/rest/service/fitness/AbstractRestFitness.kt

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ import org.evomaster.core.search.gene.string.StringGene
5959
import org.evomaster.core.search.gene.utils.GeneUtils
6060
import org.evomaster.core.search.service.DataPool
6161
import org.evomaster.core.taint.TaintAnalysis
62+
import org.evomaster.core.utils.StackTraceUtils
6263
import org.slf4j.Logger
6364
import org.slf4j.LoggerFactory
6465
import java.net.URL
@@ -1212,13 +1213,18 @@ abstract class AbstractRestFitness : HttpWsFitness<RestIndividual>() {
12121213
handleExistenceLeakage(individual,actionResults,fv)
12131214
handleNotRecognizedAuthenticated(individual, actionResults, fv)
12141215
handleForgottenAuthentication(individual, actionResults, fv)
1216+
handleStackTraceCheck(individual, actionResults, fv)
12151217
}
12161218

12171219
private fun handleSsrfFaults(
12181220
individual: RestIndividual,
12191221
actionResults: List<ActionResult>,
12201222
fv: FitnessValue
12211223
) {
1224+
if (!config.isEnabledFaultCategory(DefinedFaultCategory.SSRF)) {
1225+
return
1226+
}
1227+
12221228
individual.seeMainExecutableActions().forEach {
12231229
val ar = (actionResults.find { r -> r.sourceLocalId == it.getLocalId() } as RestCallResult?)
12241230
if (ar != null) {
@@ -1240,6 +1246,9 @@ abstract class AbstractRestFitness : HttpWsFitness<RestIndividual>() {
12401246
actionResults: List<ActionResult>,
12411247
fv: FitnessValue
12421248
) {
1249+
if (!config.isEnabledFaultCategory(DefinedFaultCategory.SECURITY_NOT_RECOGNIZED_AUTHENTICATED)) {
1250+
return
1251+
}
12431252

12441253
val notRecognized = individual.seeMainExecutableActions()
12451254
.filter {
@@ -1277,6 +1286,10 @@ abstract class AbstractRestFitness : HttpWsFitness<RestIndividual>() {
12771286
actionResults: List<ActionResult>,
12781287
fv: FitnessValue
12791288
) {
1289+
if (!config.isEnabledFaultCategory(DefinedFaultCategory.SECURITY_EXISTENCE_LEAKAGE)) {
1290+
return
1291+
}
1292+
12801293
val getPaths = individual.seeMainExecutableActions()
12811294
.filter { it.verb == HttpVerb.GET }
12821295
.map { it.path }
@@ -1301,11 +1314,40 @@ abstract class AbstractRestFitness : HttpWsFitness<RestIndividual>() {
13011314
}
13021315
}
13031316

1317+
1318+
private fun handleStackTraceCheck(
1319+
individual: RestIndividual,
1320+
actionResults: List<ActionResult>,
1321+
fv: FitnessValue
1322+
) {
1323+
if (!config.isEnabledFaultCategory(ExperimentalFaultCategory.SECURITY_STACK_TRACE)) {
1324+
return
1325+
}
1326+
1327+
for(index in individual.seeMainExecutableActions().indices){
1328+
val a = individual.seeMainExecutableActions()[index]
1329+
val r = actionResults.find { it.sourceLocalId == a.getLocalId() } as RestCallResult
1330+
1331+
if(r.getStatusCode() == 500 && r.getBody() != null && StackTraceUtils.looksLikeStackTrace(r.getBody()!!)){
1332+
val scenarioId = idMapper.handleLocalTarget(
1333+
idMapper.getFaultDescriptiveId(ExperimentalFaultCategory.SECURITY_STACK_TRACE, a.getName())
1334+
)
1335+
fv.updateTarget(scenarioId, 1.0, index)
1336+
r.addFault(DetectedFault(ExperimentalFaultCategory.SECURITY_STACK_TRACE, a.getName(), null))
1337+
}
1338+
}
1339+
}
1340+
13041341
private fun handleForgottenAuthentication(
13051342
individual: RestIndividual,
13061343
actionResults: List<ActionResult>,
13071344
fv: FitnessValue
13081345
) {
1346+
1347+
if (!config.isEnabledFaultCategory(ExperimentalFaultCategory.SECURITY_FORGOTTEN_AUTHENTICATION)) {
1348+
return
1349+
}
1350+
13091351
val endpoints = individual.seeMainExecutableActions()
13101352
.map { it.getName() }
13111353
.toSet()
@@ -1337,6 +1379,11 @@ abstract class AbstractRestFitness : HttpWsFitness<RestIndividual>() {
13371379
actionResults: List<ActionResult>,
13381380
fv: FitnessValue
13391381
) {
1382+
1383+
if (!config.isEnabledFaultCategory(DefinedFaultCategory.SECURITY_WRONG_AUTHORIZATION)) {
1384+
return
1385+
}
1386+
13401387
if (RestSecurityOracle.hasForbiddenOperation(verb, individual, actionResults)) {
13411388
val actionIndex = individual.size() - 1
13421389
val action = individual.seeMainExecutableActions()[actionIndex]

0 commit comments

Comments
 (0)