From 6eb9c5d7898e293a11e81ed3ed07c4d1330f1057 Mon Sep 17 00:00:00 2001 From: Omur Sahin Date: Sun, 11 Jan 2026 20:25:33 +0300 Subject: [PATCH 1/8] test apps --- .../{ => base}/ExistenceLeakageApplication.kt | 10 +- .../ExistenceLeakageParentApplication.kt | 119 +++++++++++++++ ...enceLeakageParentNoExistenceApplication.kt | 136 ++++++++++++++++++ .../ExistenceLeakageController.kt | 1 + .../ExistenceLeakageParentController.kt | 21 +++ ...tenceLeakageParentNoExistenceController.kt | 22 +++ ...curityExistenceLeakageNoExistenceEMTest.kt | 50 +++++++ .../SecurityExistenceLeakageParentEMTest.kt | 50 +++++++ 8 files changed, 406 insertions(+), 3 deletions(-) rename core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/main/kotlin/com/foo/rest/examples/spring/openapi/v3/security/existenceleakage/{ => base}/ExistenceLeakageApplication.kt (84%) create mode 100644 core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/main/kotlin/com/foo/rest/examples/spring/openapi/v3/security/existenceleakage/parentauth/ExistenceLeakageParentApplication.kt create mode 100644 core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/main/kotlin/com/foo/rest/examples/spring/openapi/v3/security/existenceleakage/parentnoexistence/ExistenceLeakageParentNoExistenceApplication.kt create mode 100644 core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/com/foo/rest/examples/spring/openapi/v3/security/existenceleakage/ExistenceLeakageParentController.kt create mode 100644 core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/com/foo/rest/examples/spring/openapi/v3/security/existenceleakage/ExistenceLeakageParentNoExistenceController.kt create mode 100644 core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/org/evomaster/e2etests/spring/openapi/v3/security/existenceleakage/SecurityExistenceLeakageNoExistenceEMTest.kt create mode 100644 core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/org/evomaster/e2etests/spring/openapi/v3/security/existenceleakage/SecurityExistenceLeakageParentEMTest.kt diff --git a/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/main/kotlin/com/foo/rest/examples/spring/openapi/v3/security/existenceleakage/ExistenceLeakageApplication.kt b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/main/kotlin/com/foo/rest/examples/spring/openapi/v3/security/existenceleakage/base/ExistenceLeakageApplication.kt similarity index 84% rename from core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/main/kotlin/com/foo/rest/examples/spring/openapi/v3/security/existenceleakage/ExistenceLeakageApplication.kt rename to core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/main/kotlin/com/foo/rest/examples/spring/openapi/v3/security/existenceleakage/base/ExistenceLeakageApplication.kt index 21ab8a11e6..b5627e31c6 100644 --- a/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/main/kotlin/com/foo/rest/examples/spring/openapi/v3/security/existenceleakage/ExistenceLeakageApplication.kt +++ b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/main/kotlin/com/foo/rest/examples/spring/openapi/v3/security/existenceleakage/base/ExistenceLeakageApplication.kt @@ -1,11 +1,15 @@ -package com.foo.rest.examples.spring.openapi.v3.security.existenceleakage +package com.foo.rest.examples.spring.openapi.v3.security.existenceleakage.base import org.springframework.boot.SpringApplication import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration import org.springframework.http.ResponseEntity -import org.springframework.web.bind.annotation.* - +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.PutMapping +import org.springframework.web.bind.annotation.RequestHeader +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController @SpringBootApplication(exclude = [SecurityAutoConfiguration::class]) @RequestMapping(path = ["/api/resources"]) diff --git a/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/main/kotlin/com/foo/rest/examples/spring/openapi/v3/security/existenceleakage/parentauth/ExistenceLeakageParentApplication.kt b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/main/kotlin/com/foo/rest/examples/spring/openapi/v3/security/existenceleakage/parentauth/ExistenceLeakageParentApplication.kt new file mode 100644 index 0000000000..aee2e54b54 --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/main/kotlin/com/foo/rest/examples/spring/openapi/v3/security/existenceleakage/parentauth/ExistenceLeakageParentApplication.kt @@ -0,0 +1,119 @@ +package com.foo.rest.examples.spring.openapi.v3.security.existenceleakage.parentauth + +import org.springframework.boot.SpringApplication +import org.springframework.boot.autoconfigure.SpringBootApplication +import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.* + +@SpringBootApplication(exclude = [SecurityAutoConfiguration::class]) +@RestController +@RequestMapping("/api/parents") +open class ExistenceLeakageParentApplication { + + companion object { + @JvmStatic + fun main(args: Array) { + SpringApplication.run(ExistenceLeakageParentApplication::class.java, *args) + } + + private val parents = mutableMapOf() + private val children = mutableMapOf, String>() + + fun reset() { + parents.clear() + children.clear() + } + } + + private fun checkAuth(auth: String?) = + auth != null && (auth == "FOO" || auth == "BAR") + + + @PutMapping("/{pid}") + open fun createParent( + @RequestHeader("Authorization") auth: String?, + @PathVariable pid: Int + ): ResponseEntity { + + if (!checkAuth(auth)) return ResponseEntity.status(401).build() + + if (!parents.containsKey(pid)) { + parents[pid] = auth!! + return ResponseEntity.status(201).build() + } + + return if (parents[pid] != auth) + ResponseEntity.status(403).build() + else + ResponseEntity.status(204).build() + } + + @GetMapping("/{pid}") + open fun getParent( + @RequestHeader("Authorization") auth: String?, + @PathVariable pid: Int + ): ResponseEntity { + + if (!checkAuth(auth)) return ResponseEntity.status(401).build() + + if (!parents.containsKey(pid)) { + return ResponseEntity.status(404).build() + } + + if (parents[pid] != auth) { + return ResponseEntity.status(403).build() + } + + return ResponseEntity.ok(parents[pid]) + } + + @PutMapping("/{pid}/children/{cid}") + open fun createChild( + @RequestHeader("Authorization") auth: String?, + @PathVariable pid: Int, + @PathVariable cid: Int + ): ResponseEntity { + + if (!checkAuth(auth)) return ResponseEntity.status(401).build() + + if (!parents.containsKey(pid)) { + return ResponseEntity.status(404).build() + } + + if (parents[pid] != auth) { + return ResponseEntity.status(403).build() + } + + val key = Pair(pid, cid) + if (!children.containsKey(key)) { + children[key] = auth!! + return ResponseEntity.status(201).build() + } + + return ResponseEntity.status(204).build() + } + + @GetMapping("/{pid}/children/{cid}") + open fun getChild( + @RequestHeader("Authorization") auth: String?, + @PathVariable pid: Int, + @PathVariable cid: Int + ): ResponseEntity { + + if (!checkAuth(auth)) return ResponseEntity.status(401).build() + + val key = Pair(pid, cid) + + if (!children.containsKey(key)) { + // wrong, leaking non-existence. should return 403 + return ResponseEntity.status(404).build() + } + + if (children[key] != auth) { + return ResponseEntity.status(403).build() + } + + return ResponseEntity.ok(children[key]) + } +} diff --git a/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/main/kotlin/com/foo/rest/examples/spring/openapi/v3/security/existenceleakage/parentnoexistence/ExistenceLeakageParentNoExistenceApplication.kt b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/main/kotlin/com/foo/rest/examples/spring/openapi/v3/security/existenceleakage/parentnoexistence/ExistenceLeakageParentNoExistenceApplication.kt new file mode 100644 index 0000000000..73794cae45 --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/main/kotlin/com/foo/rest/examples/spring/openapi/v3/security/existenceleakage/parentnoexistence/ExistenceLeakageParentNoExistenceApplication.kt @@ -0,0 +1,136 @@ +package com.foo.rest.examples.spring.openapi.v3.security.existenceleakage.parentnoexistence + +import org.springframework.boot.SpringApplication +import org.springframework.boot.autoconfigure.SpringBootApplication +import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.* + +@SpringBootApplication(exclude = [SecurityAutoConfiguration::class]) +@RestController +@RequestMapping("/api/parents") +open class ExistenceLeakageParentNoExistenceApplication { + + companion object { + @JvmStatic + fun main(args: Array) { + SpringApplication.run(ExistenceLeakageParentNoExistenceApplication::class.java, *args) + } + + private val parents = mutableMapOf() + private val children = mutableMapOf, String>() + + fun reset() { + parents.clear() + children.clear() + } + } + + private fun checkAuth(auth: String?) = + auth != null && (auth == "FOO" || auth == "BAR") + + + @PutMapping("/{pid}") + open fun createParent( + @RequestHeader("Authorization") auth: String?, + @PathVariable pid: Int + ): ResponseEntity { + + if (!checkAuth(auth)) return ResponseEntity.status(401).build() + + if (!parents.containsKey(pid)) { + parents[pid] = auth!! + return ResponseEntity.status(201).build() + } + + return if (parents[pid] != auth) + ResponseEntity.status(403).build() + else + ResponseEntity.status(204).build() + } + + @GetMapping("/{pid}") + open fun getParent( + @RequestHeader("Authorization") auth: String?, + @PathVariable pid: Int + ): ResponseEntity { + + if (!checkAuth(auth)) return ResponseEntity.status(401).build() + + if (!parents.containsKey(pid)) { + return ResponseEntity.status(404).build() + } + + if (parents[pid] != auth) { + return ResponseEntity.status(403).build() + } + + return ResponseEntity.ok(parents[pid]) + } + + @PutMapping("/{pid}/children/{cid}") + open fun createChild( + @RequestHeader("Authorization") auth: String?, + @PathVariable pid: Int, + @PathVariable cid: Int + ): ResponseEntity { + + if (!checkAuth(auth)) return ResponseEntity.status(401).build() + + if (!parents.containsKey(pid)) { + return ResponseEntity.status(404).build() + } + + if (parents[pid] != auth) { + return ResponseEntity.status(403).build() + } + + val key = Pair(pid, cid) + if (!children.containsKey(key)) { + children[key] = auth!! + return ResponseEntity.status(201).build() + } + + return ResponseEntity.status(204).build() + } + + @GetMapping("/{pid}/children/{cid}") + open fun getChild( + @RequestHeader("Authorization") auth: String?, + @PathVariable pid: Int, + @PathVariable cid: Int + ): ResponseEntity { + + // Authentication is required + if (!checkAuth(auth)) { + return ResponseEntity.status(401).build() + } + + // If the parent resource does not exist, returning 403 is correct for everyone + if (!parents.containsKey(pid)) { + return ResponseEntity.status(403).build() + } + + // If the parent exists but is not owned by the caller, + // we must not leak whether the child exists or not + if (parents[pid] != auth) { + return ResponseEntity.status(403).build() + } + + val key = Pair(pid, cid) + + // At this point, the caller owns the parent. + // If the child does not exist, returning 404 is legitimate + if (!children.containsKey(key)) { + return ResponseEntity.status(404).build() + } + + // Optional safety check: child ownership + if (children[key] != auth) { + return ResponseEntity.status(403).build() + } + + return ResponseEntity.ok(children[key]) + } + +} diff --git a/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/com/foo/rest/examples/spring/openapi/v3/security/existenceleakage/ExistenceLeakageController.kt b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/com/foo/rest/examples/spring/openapi/v3/security/existenceleakage/ExistenceLeakageController.kt index 213e74f2fd..c0e24291c9 100644 --- a/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/com/foo/rest/examples/spring/openapi/v3/security/existenceleakage/ExistenceLeakageController.kt +++ b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/com/foo/rest/examples/spring/openapi/v3/security/existenceleakage/ExistenceLeakageController.kt @@ -1,6 +1,7 @@ package com.foo.rest.examples.spring.openapi.v3.security.existenceleakage import com.foo.rest.examples.spring.openapi.v3.SpringController +import com.foo.rest.examples.spring.openapi.v3.security.existenceleakage.base.ExistenceLeakageApplication import org.evomaster.client.java.controller.AuthUtils import org.evomaster.client.java.controller.api.dto.auth.AuthenticationDto diff --git a/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/com/foo/rest/examples/spring/openapi/v3/security/existenceleakage/ExistenceLeakageParentController.kt b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/com/foo/rest/examples/spring/openapi/v3/security/existenceleakage/ExistenceLeakageParentController.kt new file mode 100644 index 0000000000..73606bb620 --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/com/foo/rest/examples/spring/openapi/v3/security/existenceleakage/ExistenceLeakageParentController.kt @@ -0,0 +1,21 @@ +package com.foo.rest.examples.spring.openapi.v3.security.existenceleakage + +import com.foo.rest.examples.spring.openapi.v3.SpringController +import com.foo.rest.examples.spring.openapi.v3.security.existenceleakage.parentauth.ExistenceLeakageParentApplication +import org.evomaster.client.java.controller.AuthUtils +import org.evomaster.client.java.controller.api.dto.auth.AuthenticationDto + +class ExistenceLeakageParentController : SpringController(ExistenceLeakageParentApplication::class.java) { + + override fun getInfoForAuthentication(): List { + return listOf( + AuthUtils.getForAuthorizationHeader("FOO","FOO"), + AuthUtils.getForAuthorizationHeader("BAR","BAR"), + ) + } + + override fun resetStateOfSUT() { + ExistenceLeakageParentApplication.reset() + } + +} \ No newline at end of file diff --git a/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/com/foo/rest/examples/spring/openapi/v3/security/existenceleakage/ExistenceLeakageParentNoExistenceController.kt b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/com/foo/rest/examples/spring/openapi/v3/security/existenceleakage/ExistenceLeakageParentNoExistenceController.kt new file mode 100644 index 0000000000..c31a4e880a --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/com/foo/rest/examples/spring/openapi/v3/security/existenceleakage/ExistenceLeakageParentNoExistenceController.kt @@ -0,0 +1,22 @@ +package com.foo.rest.examples.spring.openapi.v3.security.existenceleakage + +import com.foo.rest.examples.spring.openapi.v3.SpringController +import com.foo.rest.examples.spring.openapi.v3.security.existenceleakage.parentauth.ExistenceLeakageParentApplication +import com.foo.rest.examples.spring.openapi.v3.security.existenceleakage.parentnoexistence.ExistenceLeakageParentNoExistenceApplication +import org.evomaster.client.java.controller.AuthUtils +import org.evomaster.client.java.controller.api.dto.auth.AuthenticationDto + +class ExistenceLeakageParentNoExistenceController : SpringController(ExistenceLeakageParentNoExistenceApplication::class.java) { + + override fun getInfoForAuthentication(): List { + return listOf( + AuthUtils.getForAuthorizationHeader("FOO","FOO"), + AuthUtils.getForAuthorizationHeader("BAR","BAR"), + ) + } + + override fun resetStateOfSUT() { + ExistenceLeakageParentNoExistenceApplication.reset() + } + +} \ No newline at end of file diff --git a/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/org/evomaster/e2etests/spring/openapi/v3/security/existenceleakage/SecurityExistenceLeakageNoExistenceEMTest.kt b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/org/evomaster/e2etests/spring/openapi/v3/security/existenceleakage/SecurityExistenceLeakageNoExistenceEMTest.kt new file mode 100644 index 0000000000..4d063d9c29 --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/org/evomaster/e2etests/spring/openapi/v3/security/existenceleakage/SecurityExistenceLeakageNoExistenceEMTest.kt @@ -0,0 +1,50 @@ +package org.evomaster.e2etests.spring.openapi.v3.security.existenceleakage + +import com.foo.rest.examples.spring.openapi.v3.security.existenceleakage.ExistenceLeakageParentNoExistenceController +import com.webfuzzing.commons.faults.DefinedFaultCategory +import org.evomaster.core.problem.enterprise.DetectedFaultUtils +import org.evomaster.core.problem.rest.data.HttpVerb +import org.evomaster.e2etests.spring.openapi.v3.SpringTestBase +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.Test + +class SecurityExistenceLeakageNoExistenceEMTest : SpringTestBase(){ + + companion object { + @BeforeAll + @JvmStatic + fun init() { + initClass(ExistenceLeakageParentNoExistenceController()) + } + } + + + @Test + fun testRunEM() { + + runTestHandlingFlakyAndCompilation( + "SecurityExistenceLeakageNoExistenceEM", + 100 + ) { args: MutableList -> + + setOption(args, "security", "true") + setOption(args, "schemaOracles", "false") + + val solution = initAndRun(args) + + assertTrue(solution.individuals.size >= 1) + + assertHasAtLeastOne(solution, HttpVerb.PUT, 201, "/api/resources/{id}", null) + assertHasAtLeastOne(solution, HttpVerb.GET, 404, "/api/resources/{id}", null) + assertHasAtLeastOne(solution, HttpVerb.GET, 200, "/api/resources/{id}", null) + assertHasAtLeastOne(solution, HttpVerb.GET, 403, "/api/resources/{id}", null) + + + val faults = DetectedFaultUtils.getDetectedFaultCategories(solution) + assertEquals(1, faults.size) + assertEquals(DefinedFaultCategory.SECURITY_EXISTENCE_LEAKAGE, faults.first()) + } + } +} \ No newline at end of file diff --git a/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/org/evomaster/e2etests/spring/openapi/v3/security/existenceleakage/SecurityExistenceLeakageParentEMTest.kt b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/org/evomaster/e2etests/spring/openapi/v3/security/existenceleakage/SecurityExistenceLeakageParentEMTest.kt new file mode 100644 index 0000000000..eb1f897323 --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/org/evomaster/e2etests/spring/openapi/v3/security/existenceleakage/SecurityExistenceLeakageParentEMTest.kt @@ -0,0 +1,50 @@ +package org.evomaster.e2etests.spring.openapi.v3.security.existenceleakage + +import com.foo.rest.examples.spring.openapi.v3.security.existenceleakage.ExistenceLeakageParentController +import com.webfuzzing.commons.faults.DefinedFaultCategory +import org.evomaster.core.problem.enterprise.DetectedFaultUtils +import org.evomaster.core.problem.rest.data.HttpVerb +import org.evomaster.e2etests.spring.openapi.v3.SpringTestBase +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.Test + +class SecurityExistenceLeakageParentEMTest : SpringTestBase(){ + + companion object { + @BeforeAll + @JvmStatic + fun init() { + initClass(ExistenceLeakageParentController()) + } + } + + + @Test + fun testRunEM() { + + runTestHandlingFlakyAndCompilation( + "SecurityExistenceLeakageParentEM", + 100 + ) { args: MutableList -> + + setOption(args, "security", "true") + setOption(args, "schemaOracles", "false") + + val solution = initAndRun(args) + + assertTrue(solution.individuals.size >= 1) + + assertHasAtLeastOne(solution, HttpVerb.PUT, 201, "/api/resources/{id}", null) + assertHasAtLeastOne(solution, HttpVerb.GET, 404, "/api/resources/{id}", null) + assertHasAtLeastOne(solution, HttpVerb.GET, 200, "/api/resources/{id}", null) + assertHasAtLeastOne(solution, HttpVerb.GET, 403, "/api/resources/{id}", null) + + + val faults = DetectedFaultUtils.getDetectedFaultCategories(solution) + assertEquals(1, faults.size) + assertEquals(DefinedFaultCategory.SECURITY_EXISTENCE_LEAKAGE, faults.first()) + } + } +} \ No newline at end of file From c34460b2d593f281d98b9ea144f71f1e8a9fe6ca Mon Sep 17 00:00:00 2001 From: arcuri82 Date: Mon, 12 Jan 2026 12:27:40 +0100 Subject: [PATCH 2/8] candidate fix for broken links in ind manipulation --- .../org/evomaster/core/problem/rest/service/SecurityRest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/kotlin/org/evomaster/core/problem/rest/service/SecurityRest.kt b/core/src/main/kotlin/org/evomaster/core/problem/rest/service/SecurityRest.kt index 78572ab1eb..380c9a32d5 100644 --- a/core/src/main/kotlin/org/evomaster/core/problem/rest/service/SecurityRest.kt +++ b/core/src/main/kotlin/org/evomaster/core/problem/rest/service/SecurityRest.kt @@ -227,8 +227,8 @@ class SecurityRest { */ val repeat = lastCall.copy() as RestCallAction repeat.forceNewTaints() + repeat.resetLocalIdRecursively() copy.addMainActionInEmptyEnterpriseGroup(action = repeat) - copy.resetLocalIdRecursively() //TODO what about links? copy.doInitializeLocalId() } copy.seeMainExecutableActions().last().auth = otherAuth From 2eb203983e147fe2cb586370632b3636df0b04ad Mon Sep 17 00:00:00 2001 From: Omur Date: Tue, 13 Jan 2026 13:30:29 +0300 Subject: [PATCH 3/8] existence update --- .../ExistenceLeakageParentApplication.kt | 2 +- ...enceLeakageParentNoExistenceApplication.kt | 2 +- ...curityExistenceLeakageNoExistenceEMTest.kt | 17 ++--- .../SecurityExistenceLeakageParentEMTest.kt | 4 +- .../core/problem/rest/data/RestPath.kt | 14 ++++ .../problem/rest/oracle/RestSecurityOracle.kt | 5 ++ .../core/problem/rest/service/SecurityRest.kt | 64 ++++++++++++++++--- 7 files changed, 84 insertions(+), 24 deletions(-) diff --git a/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/main/kotlin/com/foo/rest/examples/spring/openapi/v3/security/existenceleakage/parentauth/ExistenceLeakageParentApplication.kt b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/main/kotlin/com/foo/rest/examples/spring/openapi/v3/security/existenceleakage/parentauth/ExistenceLeakageParentApplication.kt index aee2e54b54..45d22149f3 100644 --- a/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/main/kotlin/com/foo/rest/examples/spring/openapi/v3/security/existenceleakage/parentauth/ExistenceLeakageParentApplication.kt +++ b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/main/kotlin/com/foo/rest/examples/spring/openapi/v3/security/existenceleakage/parentauth/ExistenceLeakageParentApplication.kt @@ -58,7 +58,7 @@ open class ExistenceLeakageParentApplication { if (!checkAuth(auth)) return ResponseEntity.status(401).build() if (!parents.containsKey(pid)) { - return ResponseEntity.status(404).build() + return ResponseEntity.status(403).build() } if (parents[pid] != auth) { diff --git a/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/main/kotlin/com/foo/rest/examples/spring/openapi/v3/security/existenceleakage/parentnoexistence/ExistenceLeakageParentNoExistenceApplication.kt b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/main/kotlin/com/foo/rest/examples/spring/openapi/v3/security/existenceleakage/parentnoexistence/ExistenceLeakageParentNoExistenceApplication.kt index 73794cae45..e179dac50a 100644 --- a/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/main/kotlin/com/foo/rest/examples/spring/openapi/v3/security/existenceleakage/parentnoexistence/ExistenceLeakageParentNoExistenceApplication.kt +++ b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/main/kotlin/com/foo/rest/examples/spring/openapi/v3/security/existenceleakage/parentnoexistence/ExistenceLeakageParentNoExistenceApplication.kt @@ -58,7 +58,7 @@ open class ExistenceLeakageParentNoExistenceApplication { if (!checkAuth(auth)) return ResponseEntity.status(401).build() if (!parents.containsKey(pid)) { - return ResponseEntity.status(404).build() + return ResponseEntity.status(403).build() } if (parents[pid] != auth) { diff --git a/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/org/evomaster/e2etests/spring/openapi/v3/security/existenceleakage/SecurityExistenceLeakageNoExistenceEMTest.kt b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/org/evomaster/e2etests/spring/openapi/v3/security/existenceleakage/SecurityExistenceLeakageNoExistenceEMTest.kt index 4d063d9c29..a659667a5c 100644 --- a/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/org/evomaster/e2etests/spring/openapi/v3/security/existenceleakage/SecurityExistenceLeakageNoExistenceEMTest.kt +++ b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/org/evomaster/e2etests/spring/openapi/v3/security/existenceleakage/SecurityExistenceLeakageNoExistenceEMTest.kt @@ -3,10 +3,9 @@ package org.evomaster.e2etests.spring.openapi.v3.security.existenceleakage import com.foo.rest.examples.spring.openapi.v3.security.existenceleakage.ExistenceLeakageParentNoExistenceController import com.webfuzzing.commons.faults.DefinedFaultCategory import org.evomaster.core.problem.enterprise.DetectedFaultUtils -import org.evomaster.core.problem.rest.data.HttpVerb import org.evomaster.e2etests.spring.openapi.v3.SpringTestBase -import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Assertions.assertFalse import org.junit.jupiter.api.BeforeAll import org.junit.jupiter.api.Test @@ -26,7 +25,7 @@ class SecurityExistenceLeakageNoExistenceEMTest : SpringTestBase(){ runTestHandlingFlakyAndCompilation( "SecurityExistenceLeakageNoExistenceEM", - 100 + 6000 ) { args: MutableList -> setOption(args, "security", "true") @@ -36,15 +35,9 @@ class SecurityExistenceLeakageNoExistenceEMTest : SpringTestBase(){ assertTrue(solution.individuals.size >= 1) - assertHasAtLeastOne(solution, HttpVerb.PUT, 201, "/api/resources/{id}", null) - assertHasAtLeastOne(solution, HttpVerb.GET, 404, "/api/resources/{id}", null) - assertHasAtLeastOne(solution, HttpVerb.GET, 200, "/api/resources/{id}", null) - assertHasAtLeastOne(solution, HttpVerb.GET, 403, "/api/resources/{id}", null) + val faultCategories = DetectedFaultUtils.getDetectedFaultCategories(solution) - - val faults = DetectedFaultUtils.getDetectedFaultCategories(solution) - assertEquals(1, faults.size) - assertEquals(DefinedFaultCategory.SECURITY_EXISTENCE_LEAKAGE, faults.first()) + assertFalse({ DefinedFaultCategory.SECURITY_EXISTENCE_LEAKAGE in faultCategories }) } } -} \ No newline at end of file +} diff --git a/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/org/evomaster/e2etests/spring/openapi/v3/security/existenceleakage/SecurityExistenceLeakageParentEMTest.kt b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/org/evomaster/e2etests/spring/openapi/v3/security/existenceleakage/SecurityExistenceLeakageParentEMTest.kt index eb1f897323..a953c1af82 100644 --- a/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/org/evomaster/e2etests/spring/openapi/v3/security/existenceleakage/SecurityExistenceLeakageParentEMTest.kt +++ b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/org/evomaster/e2etests/spring/openapi/v3/security/existenceleakage/SecurityExistenceLeakageParentEMTest.kt @@ -26,7 +26,7 @@ class SecurityExistenceLeakageParentEMTest : SpringTestBase(){ runTestHandlingFlakyAndCompilation( "SecurityExistenceLeakageParentEM", - 100 + 1000 ) { args: MutableList -> setOption(args, "security", "true") @@ -47,4 +47,4 @@ class SecurityExistenceLeakageParentEMTest : SpringTestBase(){ assertEquals(DefinedFaultCategory.SECURITY_EXISTENCE_LEAKAGE, faults.first()) } } -} \ No newline at end of file +} diff --git a/core/src/main/kotlin/org/evomaster/core/problem/rest/data/RestPath.kt b/core/src/main/kotlin/org/evomaster/core/problem/rest/data/RestPath.kt index 7d3901b8d6..47db7498da 100644 --- a/core/src/main/kotlin/org/evomaster/core/problem/rest/data/RestPath.kt +++ b/core/src/main/kotlin/org/evomaster/core/problem/rest/data/RestPath.kt @@ -591,6 +591,20 @@ class RestPath(path: String) { fun isRoot() = levels() == 0 + /** + * Checks if this path has an ancestor (parent path). + * Returns true if the path has more than one level AND ends with a parameter. + * + * Examples: + * - /{id}/child/{cid} returns true (has ancestor and ends with parameter) + * - /{id} returns false (single level, no ancestor) + * - /{id}/child returns false (does not end with parameter) + * - /{id}/child/ returns false (does not end with parameter) + * - /users/{id} returns true (has ancestor and ends with parameter) + * - /users returns false (single level, no ancestor) + */ + fun hasAncestorAndLastElementParameter(): Boolean = levels() > 1 && isLastElementAParameter() + fun parentPath() : RestPath { if(isRoot()){ throw IllegalStateException("Root has no parent") diff --git a/core/src/main/kotlin/org/evomaster/core/problem/rest/oracle/RestSecurityOracle.kt b/core/src/main/kotlin/org/evomaster/core/problem/rest/oracle/RestSecurityOracle.kt index 0bb446ab32..85686fcdfd 100644 --- a/core/src/main/kotlin/org/evomaster/core/problem/rest/oracle/RestSecurityOracle.kt +++ b/core/src/main/kotlin/org/evomaster/core/problem/rest/oracle/RestSecurityOracle.kt @@ -200,6 +200,11 @@ object RestSecurityOracle { (actionResults.find { r -> r.sourceLocalId == it.getLocalId() } as RestCallResult) .getStatusCode() == 404 } + val lastStatusCode = (actionResults.last() as RestCallResult).getStatusCode() + + if(StatusGroup.G_2xx.isInGroup(lastStatusCode)){ + return false + } return a403.isNotEmpty() && a404.isNotEmpty() } diff --git a/core/src/main/kotlin/org/evomaster/core/problem/rest/service/SecurityRest.kt b/core/src/main/kotlin/org/evomaster/core/problem/rest/service/SecurityRest.kt index 380c9a32d5..761c7d61be 100644 --- a/core/src/main/kotlin/org/evomaster/core/problem/rest/service/SecurityRest.kt +++ b/core/src/main/kotlin/org/evomaster/core/problem/rest/service/SecurityRest.kt @@ -388,7 +388,7 @@ class SecurityRest { var anySuccess = false genes.forEach { - gene -> + gene -> val leafGene = gene.getLeafGene() if(leafGene !is StringGene) return@forEach @@ -398,7 +398,7 @@ class SecurityRest { // we need to do this way because we need to append our payload var newPayload = leafGene.getPhenotype().getValueAsRawString() + String.format(payload, config.sqliInjectedSleepDurationMs/1000.0) - // append the SQLi payload value + // append the SQLi payload value leafGene.getPhenotype().setFromStringValue(newPayload).also { if(it) anySuccess = true } @@ -599,7 +599,7 @@ class SecurityRest { action.path, status = 401, authenticated = true - ) + ) }.distinctBy { it.seeMainExecutableActions().last().auth.name } if(suspicious.isEmpty()){ @@ -687,7 +687,7 @@ class SecurityRest { HttpVerb.GET, get.path, status = 403 - ) + ) if(inds403.isEmpty()){ return@forEach } @@ -704,7 +704,7 @@ class SecurityRest { //found the bug. val forbidden = inds403.minBy { it.individual.size() } - val notfound = inds404.maxBy { it.individual.size() } + val notfound = inds404.minBy { it.individual.size() } //needs slicing to minimize the newly generated test val index403 = RestIndividualSelectorUtils.getIndexOfAction( @@ -725,7 +725,55 @@ class SecurityRest { // slice the individual in a way that delete all calls after the chosen verb request val second = RestIndividualBuilder.sliceAllCallsInIndividualAfterAction(notfound.individual, index404) - val final = RestIndividualBuilder.merge(first, second) + var final = RestIndividualBuilder.merge(first, second) + + // Check if the 404 path is a child resource and if user has access to immediate parent resource + // If user has access to parent (returns 200), then 404 on child is legitimate, not an existence leakage + val path404 = get.path + val action404 = final.seeMainExecutableActions().last() as RestCallAction + val lastAuth = action404.auth + + // Find the immediate parent path from existing actions in the individual + // We need to find the parent action that appears BEFORE the 404 action and is closest to it + val allActions = final.seeMainExecutableActions() + val action404Index = allActions.indexOf(action404) + val immediateParentAction = allActions + .take(action404Index) // Only consider actions before the 404 action + .lastOrNull { it.path.isDirectOrPossibleAncestorOf(path404) && !it.path.isEquivalent(path404) } + + if (immediateParentAction != null) { + val parentPath = immediateParentAction.path + + // Try to find or create a GET action for parent path + val parentGetAction = actionDefinitions + .firstOrNull { it.verb == HttpVerb.GET && it.path.isEquivalent(parentPath) } + ?.copy() as RestCallAction? + + if (parentGetAction != null) { + parentGetAction.resetLocalIdRecursively() + parentGetAction.auth = lastAuth + + // Bind to the same path params from the existing parent action to ensure same IDs + parentGetAction.bindBasedOn( + immediateParentAction.path, + immediateParentAction.parameters.filterIsInstance(), + null + ) + + final.addResourceCall( + restCalls = RestResourceCalls( + actions = mutableListOf(parentGetAction), + sqlActions = listOf() + ) + ) + final.seeMainExecutableActions() + .filter { it.verb == HttpVerb.PUT || it.verb == HttpVerb.POST }.forEach { + it.saveCreatedResourceLocation = true + } + final.fixResourceForwardLinks() + final.ensureFlattenedStructure() + } + } final.modifySampleType(SampleType.SECURITY) final.ensureFlattenedStructure() @@ -1060,8 +1108,8 @@ class SecurityRest { ) finalIndividual.seeMainExecutableActions() .filter { it.verb == HttpVerb.PUT || it.verb == HttpVerb.POST }.forEach { - it.saveCreatedResourceLocation = true - } + it.saveCreatedResourceLocation = true + } finalIndividual.fixResourceForwardLinks() finalIndividual.modifySampleType(SampleType.SECURITY) From 09e782108318bd82bee6796277436bd01fb0e765 Mon Sep 17 00:00:00 2001 From: Omur Sahin Date: Tue, 13 Jan 2026 22:24:30 +0300 Subject: [PATCH 4/8] another try --- .../ExistenceLeakageParentApplication.kt | 20 +++- ...curityExistenceLeakageNoExistenceEMTest.kt | 3 + .../SecurityExistenceLeakageParentEMTest.kt | 11 +- .../core/problem/rest/service/SecurityRest.kt | 100 +++++++----------- 4 files changed, 64 insertions(+), 70 deletions(-) diff --git a/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/main/kotlin/com/foo/rest/examples/spring/openapi/v3/security/existenceleakage/parentauth/ExistenceLeakageParentApplication.kt b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/main/kotlin/com/foo/rest/examples/spring/openapi/v3/security/existenceleakage/parentauth/ExistenceLeakageParentApplication.kt index 45d22149f3..2627c926b3 100644 --- a/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/main/kotlin/com/foo/rest/examples/spring/openapi/v3/security/existenceleakage/parentauth/ExistenceLeakageParentApplication.kt +++ b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/main/kotlin/com/foo/rest/examples/spring/openapi/v3/security/existenceleakage/parentauth/ExistenceLeakageParentApplication.kt @@ -101,15 +101,31 @@ open class ExistenceLeakageParentApplication { @PathVariable cid: Int ): ResponseEntity { - if (!checkAuth(auth)) return ResponseEntity.status(401).build() + // Authentication is required + if (!checkAuth(auth)) { + return ResponseEntity.status(401).build() + } + + // If the parent resource does not exist, returning 403 is correct for everyone + if (!parents.containsKey(pid)) { + return ResponseEntity.status(404).build() + } + + // If the parent exists but is not owned by the caller, + // we must not leak whether the child exists or not + if (parents[pid] != auth) { + return ResponseEntity.status(403).build() + } val key = Pair(pid, cid) + // At this point, the caller owns the parent. + // If the child does not exist, returning 404 is legitimate if (!children.containsKey(key)) { - // wrong, leaking non-existence. should return 403 return ResponseEntity.status(404).build() } + // Optional safety check: child ownership if (children[key] != auth) { return ResponseEntity.status(403).build() } diff --git a/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/org/evomaster/e2etests/spring/openapi/v3/security/existenceleakage/SecurityExistenceLeakageNoExistenceEMTest.kt b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/org/evomaster/e2etests/spring/openapi/v3/security/existenceleakage/SecurityExistenceLeakageNoExistenceEMTest.kt index a659667a5c..4af66561fe 100644 --- a/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/org/evomaster/e2etests/spring/openapi/v3/security/existenceleakage/SecurityExistenceLeakageNoExistenceEMTest.kt +++ b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/org/evomaster/e2etests/spring/openapi/v3/security/existenceleakage/SecurityExistenceLeakageNoExistenceEMTest.kt @@ -3,6 +3,7 @@ package org.evomaster.e2etests.spring.openapi.v3.security.existenceleakage import com.foo.rest.examples.spring.openapi.v3.security.existenceleakage.ExistenceLeakageParentNoExistenceController import com.webfuzzing.commons.faults.DefinedFaultCategory import org.evomaster.core.problem.enterprise.DetectedFaultUtils +import org.evomaster.core.problem.rest.data.HttpVerb import org.evomaster.e2etests.spring.openapi.v3.SpringTestBase import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Assertions.assertFalse @@ -35,6 +36,8 @@ class SecurityExistenceLeakageNoExistenceEMTest : SpringTestBase(){ assertTrue(solution.individuals.size >= 1) + assertHasAtLeastOne(solution, HttpVerb.GET, 404, "/api/parents/{pid}/children/{cid}", null) + val faultCategories = DetectedFaultUtils.getDetectedFaultCategories(solution) assertFalse({ DefinedFaultCategory.SECURITY_EXISTENCE_LEAKAGE in faultCategories }) diff --git a/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/org/evomaster/e2etests/spring/openapi/v3/security/existenceleakage/SecurityExistenceLeakageParentEMTest.kt b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/org/evomaster/e2etests/spring/openapi/v3/security/existenceleakage/SecurityExistenceLeakageParentEMTest.kt index a953c1af82..677896b870 100644 --- a/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/org/evomaster/e2etests/spring/openapi/v3/security/existenceleakage/SecurityExistenceLeakageParentEMTest.kt +++ b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/org/evomaster/e2etests/spring/openapi/v3/security/existenceleakage/SecurityExistenceLeakageParentEMTest.kt @@ -26,7 +26,7 @@ class SecurityExistenceLeakageParentEMTest : SpringTestBase(){ runTestHandlingFlakyAndCompilation( "SecurityExistenceLeakageParentEM", - 1000 + 100 ) { args: MutableList -> setOption(args, "security", "true") @@ -36,10 +36,11 @@ class SecurityExistenceLeakageParentEMTest : SpringTestBase(){ assertTrue(solution.individuals.size >= 1) - assertHasAtLeastOne(solution, HttpVerb.PUT, 201, "/api/resources/{id}", null) - assertHasAtLeastOne(solution, HttpVerb.GET, 404, "/api/resources/{id}", null) - assertHasAtLeastOne(solution, HttpVerb.GET, 200, "/api/resources/{id}", null) - assertHasAtLeastOne(solution, HttpVerb.GET, 403, "/api/resources/{id}", null) + assertHasAtLeastOne(solution, HttpVerb.PUT, 201, "/api/parents/{pid}", null) + assertHasAtLeastOne(solution, HttpVerb.PUT, 201, "/api/parents/{pid}/children/{cid}", null) + assertHasAtLeastOne(solution, HttpVerb.GET, 403, "/api/parents/{pid}/children/{cid}", null) + assertHasAtLeastOne(solution, HttpVerb.GET, 404, "/api/parents/{pid}/children/{cid}", null) + assertHasAtLeastOne(solution, HttpVerb.GET, 403, "/api/parents/{pid}", null) val faults = DetectedFaultUtils.getDetectedFaultCategories(solution) diff --git a/core/src/main/kotlin/org/evomaster/core/problem/rest/service/SecurityRest.kt b/core/src/main/kotlin/org/evomaster/core/problem/rest/service/SecurityRest.kt index 761c7d61be..9fbdaaa376 100644 --- a/core/src/main/kotlin/org/evomaster/core/problem/rest/service/SecurityRest.kt +++ b/core/src/main/kotlin/org/evomaster/core/problem/rest/service/SecurityRest.kt @@ -725,54 +725,44 @@ class SecurityRest { // slice the individual in a way that delete all calls after the chosen verb request val second = RestIndividualBuilder.sliceAllCallsInIndividualAfterAction(notfound.individual, index404) - var final = RestIndividualBuilder.merge(first, second) + val final = RestIndividualBuilder.merge(first, second) - // Check if the 404 path is a child resource and if user has access to immediate parent resource - // If user has access to parent (returns 200), then 404 on child is legitimate, not an existence leakage + // Check if the 404 path is a child resource and add parent GET to check access val path404 = get.path val action404 = final.seeMainExecutableActions().last() as RestCallAction val lastAuth = action404.auth - // Find the immediate parent path from existing actions in the individual - // We need to find the parent action that appears BEFORE the 404 action and is closest to it - val allActions = final.seeMainExecutableActions() - val action404Index = allActions.indexOf(action404) - val immediateParentAction = allActions - .take(action404Index) // Only consider actions before the 404 action - .lastOrNull { it.path.isDirectOrPossibleAncestorOf(path404) && !it.path.isEquivalent(path404) } - - if (immediateParentAction != null) { - val parentPath = immediateParentAction.path - - // Try to find or create a GET action for parent path - val parentGetAction = actionDefinitions - .firstOrNull { it.verb == HttpVerb.GET && it.path.isEquivalent(parentPath) } - ?.copy() as RestCallAction? - - if (parentGetAction != null) { - parentGetAction.resetLocalIdRecursively() - parentGetAction.auth = lastAuth - - // Bind to the same path params from the existing parent action to ensure same IDs - parentGetAction.bindBasedOn( - immediateParentAction.path, - immediateParentAction.parameters.filterIsInstance(), - null - ) + // Find the parent path from getOperations (all GET operation definitions) + val parentGetOperation = getOperations + .filter { it.path.isDirectOrPossibleAncestorOf(path404) && !it.path.isEquivalent(path404) } + .maxByOrNull { it.path.toString().length } // Get the most specific parent (closest ancestor) + + if (parentGetOperation != null) { + val parentGetAction = parentGetOperation.copy() as RestCallAction + parentGetAction.resetLocalIdRecursively() + parentGetAction.forceNewTaints() + parentGetAction.auth = lastAuth + + // Bind to the same path params from the 404 action to ensure same IDs + parentGetAction.bindBasedOn( + action404.path, + action404.parameters.filterIsInstance(), + null + ) - final.addResourceCall( - restCalls = RestResourceCalls( - actions = mutableListOf(parentGetAction), - sqlActions = listOf() - ) + parentGetAction.doInitialize() + + final.addResourceCall( + restCalls = RestResourceCalls( + actions = mutableListOf(parentGetAction), + sqlActions = listOf() ) - final.seeMainExecutableActions() - .filter { it.verb == HttpVerb.PUT || it.verb == HttpVerb.POST }.forEach { - it.saveCreatedResourceLocation = true - } - final.fixResourceForwardLinks() - final.ensureFlattenedStructure() - } + ) + final.seeMainExecutableActions() + .filter { it.verb == HttpVerb.PUT || it.verb == HttpVerb.POST }.forEach { + it.saveCreatedResourceLocation = true + } + final.fixResourceForwardLinks() } final.modifySampleType(SampleType.SECURITY) @@ -784,31 +774,15 @@ class SecurityRest { return@forEach } - //verify if newly constructed individual still find the bug - val check403 = RestIndividualSelectorUtils.getIndexOfAction( - evaluatedIndividual, - HttpVerb.GET, - get.path, - 403 - ) - val check404 = RestIndividualSelectorUtils.getIndexOfAction( - evaluatedIndividual, - HttpVerb.GET, - get.path, - 404 - ) - //fitness function should have detected the fault - val faults = (evaluatedIndividual.evaluatedMainActions().last().result as RestCallResult).getFaults() + val faultsCategories = DetectedFaultUtils.getDetectedFaultCategories(evaluatedIndividual) - if(check403 < 0 || check404 < 0 || faults.none { it.category == DefinedFaultCategory.SECURITY_EXISTENCE_LEAKAGE }){ - //if this happens, it is a bug in the merge... or flakiness - log.warn("Failed to construct new test showing the 403 vs 404 security leakage issue") - return@forEach + if(DefinedFaultCategory.SECURITY_EXISTENCE_LEAKAGE in faultsCategories){ + // add the evaluated individual to the archive + val added = archive.addIfNeeded(evaluatedIndividual) + //if we arrive here, should always be added, because we are creating a new testing target + assert(added) } - val added = archive.addIfNeeded(evaluatedIndividual) - //if we arrive here, should always be added, because we are creating a new testing target - assert(added) } } From 2abcf1d3a93e4be5debe68b5e5c809474716a3c0 Mon Sep 17 00:00:00 2001 From: arcuri82 Date: Fri, 16 Jan 2026 15:31:52 +0100 Subject: [PATCH 5/8] working on Information Leakage --- .../SecurityExistenceLeakageTest.kt | 2 +- .../core/problem/rest/data/RestPath.kt | 8 +++ .../problem/rest/oracle/RestSecurityOracle.kt | 65 +++++++++++++++++-- .../core/problem/rest/service/SecurityRest.kt | 6 +- .../service/fitness/AbstractRestFitness.kt | 10 ++- 5 files changed, 83 insertions(+), 8 deletions(-) diff --git a/core-tests/integration-tests/core-it/src/test/kotlin/org/evomaster/core/problem/rest/securityrestoracle/SecurityExistenceLeakageTest.kt b/core-tests/integration-tests/core-it/src/test/kotlin/org/evomaster/core/problem/rest/securityrestoracle/SecurityExistenceLeakageTest.kt index 5fd6b70372..b8921dc364 100644 --- a/core-tests/integration-tests/core-it/src/test/kotlin/org/evomaster/core/problem/rest/securityrestoracle/SecurityExistenceLeakageTest.kt +++ b/core-tests/integration-tests/core-it/src/test/kotlin/org/evomaster/core/problem/rest/securityrestoracle/SecurityExistenceLeakageTest.kt @@ -82,7 +82,7 @@ class SecurityExistenceLeakageTest: IntegrationTestRestBase() { assertEquals(403, r1.getStatusCode()) assertEquals(404, r2.getStatusCode()) - val faultDetected = RestSecurityOracle.hasExistenceLeakage(RestPath("/api/resources/{id}"),ei.individual, ei.seeResults()) + val faultDetected = RestSecurityOracle.hasExistenceLeakage(RestPath("/api/resources/{id}"),ei.individual, ei.seeResults(), listOf()) assertTrue(faultDetected) //fault should be put on 404 diff --git a/core/src/main/kotlin/org/evomaster/core/problem/rest/data/RestPath.kt b/core/src/main/kotlin/org/evomaster/core/problem/rest/data/RestPath.kt index 47db7498da..8331f87ca2 100644 --- a/core/src/main/kotlin/org/evomaster/core/problem/rest/data/RestPath.kt +++ b/core/src/main/kotlin/org/evomaster/core/problem/rest/data/RestPath.kt @@ -243,6 +243,14 @@ class RestPath(path: String) { return other.isSameOrAncestorOf(this) } + fun isStrictlyAncestorOf(other: RestPath): Boolean { + if(this.elements.size == other.elements.size && (this.endsWithSlash || !other.endsWithSlash)){ + //if same size, then only possibility of being ancestor if other ends with slash, but not this + return false + } + return isSameOrAncestorOf(other) + } + /** * Prefix or same as "other" diff --git a/core/src/main/kotlin/org/evomaster/core/problem/rest/oracle/RestSecurityOracle.kt b/core/src/main/kotlin/org/evomaster/core/problem/rest/oracle/RestSecurityOracle.kt index 85686fcdfd..1c3e34270d 100644 --- a/core/src/main/kotlin/org/evomaster/core/problem/rest/oracle/RestSecurityOracle.kt +++ b/core/src/main/kotlin/org/evomaster/core/problem/rest/oracle/RestSecurityOracle.kt @@ -171,7 +171,8 @@ object RestSecurityOracle { fun hasExistenceLeakage( path: RestPath, individual: RestIndividual, - actionResults: List + actionResults: List, + actionDefinitions: List ): Boolean{ verifySampleType(individual) @@ -200,13 +201,69 @@ object RestSecurityOracle { (actionResults.find { r -> r.sourceLocalId == it.getLocalId() } as RestCallResult) .getStatusCode() == 404 } - val lastStatusCode = (actionResults.last() as RestCallResult).getStatusCode() - if(StatusGroup.G_2xx.isInGroup(lastStatusCode)){ + if(a403.isEmpty() || a404.isEmpty()){ + //no discrepancy of status on same path. so no leakage return false } - return a403.isNotEmpty() && a404.isNotEmpty() + /* + by itself, the fact that there is 404 does not imply a leakage, as the user + "might" own parent resources. + we need to check for that. + */ + + val topGet = findStrictTopGETResourceAncestor(path, actionDefinitions) + //if null, then for sure the user with 404 does not own a parent resource + ?: return true + + val verifiers = individual.seeMainExecutableActions() + .filter { it.verb == HttpVerb.GET && it.path == topGet.path } + .filter { actionResults.find { r -> r.sourceLocalId == it.getLocalId() } != null } + + if(verifiers.isEmpty()){ + //a top GET exists in schema, but was not called in the test. + //as such, we cannot be sure if bug is found, as, even if 403-404 on same path, + //it could well be that the 404 was legit + return false + } + + for(notfound in a404){ + + //FIXME i don't think it is correct, as ignoring dynamic info? + //TODO need tests for it + val matching = verifiers.filter { + notfound.usingSameResolvedPath(it) + && ! notfound.auth.isDifferentFrom(it.auth) + } + + if(matching.isEmpty()){ + continue + } + + val codes = matching.map { (actionResults.find { r -> r.sourceLocalId == it.getLocalId() } as RestCallResult).getStatusCode() } + + if(codes.any{ StatusGroup.G_2xx.isInGroup(it)}) { + //a 2xx can be done on parent resource + continue + } + + if(codes.any{ it == 403 || it == 404 }) { + //there is at least one call on ancestor resource with same auth, but none was positive 2xx + return true // and there is at least one discrepancy 403-404 on same endpoint + } + //other codes like 400 or 500 are ignored here, eg, due to input validation + } + + return false + } + + private fun findStrictTopGETResourceAncestor(path: RestPath, actions: List) : RestCallAction?{ + return actions + .filter { it.verb == HttpVerb.GET } + .filter { it.path.isStrictlyAncestorOf(path)} + .filter { it.path.isLastElementAParameter() } + .minByOrNull { it.path.levels() } } /** diff --git a/core/src/main/kotlin/org/evomaster/core/problem/rest/service/SecurityRest.kt b/core/src/main/kotlin/org/evomaster/core/problem/rest/service/SecurityRest.kt index 9fbdaaa376..3e0191276c 100644 --- a/core/src/main/kotlin/org/evomaster/core/problem/rest/service/SecurityRest.kt +++ b/core/src/main/kotlin/org/evomaster/core/problem/rest/service/SecurityRest.kt @@ -729,13 +729,13 @@ class SecurityRest { // Check if the 404 path is a child resource and add parent GET to check access val path404 = get.path - val action404 = final.seeMainExecutableActions().last() as RestCallAction + val action404 = final.seeMainExecutableActions().last() val lastAuth = action404.auth // Find the parent path from getOperations (all GET operation definitions) val parentGetOperation = getOperations .filter { it.path.isDirectOrPossibleAncestorOf(path404) && !it.path.isEquivalent(path404) } - .maxByOrNull { it.path.toString().length } // Get the most specific parent (closest ancestor) + .minByOrNull { it.path.toString().length } // Get the top parent if (parentGetOperation != null) { val parentGetAction = parentGetOperation.copy() as RestCallAction @@ -743,6 +743,7 @@ class SecurityRest { parentGetAction.forceNewTaints() parentGetAction.auth = lastAuth + //FIXME this would not work for dynamic parameters // Bind to the same path params from the 404 action to ensure same IDs parentGetAction.bindBasedOn( action404.path, @@ -760,6 +761,7 @@ class SecurityRest { ) final.seeMainExecutableActions() .filter { it.verb == HttpVerb.PUT || it.verb == HttpVerb.POST }.forEach { + //FIXME unclear. should be refactored with dealing of dynamic params it.saveCreatedResourceLocation = true } final.fixResourceForwardLinks() diff --git a/core/src/main/kotlin/org/evomaster/core/problem/rest/service/fitness/AbstractRestFitness.kt b/core/src/main/kotlin/org/evomaster/core/problem/rest/service/fitness/AbstractRestFitness.kt index d3bf0f01c7..99024afd52 100644 --- a/core/src/main/kotlin/org/evomaster/core/problem/rest/service/fitness/AbstractRestFitness.kt +++ b/core/src/main/kotlin/org/evomaster/core/problem/rest/service/fitness/AbstractRestFitness.kt @@ -108,9 +108,15 @@ abstract class AbstractRestFitness : HttpWsFitness() { private lateinit var schemaOracle: RestSchemaOracle + /** + * All actions that can be defined from the OpenAPI schema + */ + private lateinit var actionDefinitions: List + @PostConstruct fun initBean(){ schemaOracle = RestSchemaOracle((sampler as AbstractRestSampler).schemaHolder) + actionDefinitions = sampler.getActionDefinitions() as List } @@ -1361,7 +1367,9 @@ abstract class AbstractRestFitness : HttpWsFitness() { .map { it.path } .toSet() - val faultyPaths = getPaths.filter { RestSecurityOracle.hasExistenceLeakage(it, individual, actionResults) } + val faultyPaths = getPaths.filter { + RestSecurityOracle.hasExistenceLeakage(it, individual, actionResults, actionDefinitions) + } if(faultyPaths.isEmpty()){ return } From 05d05c542c144ae065f4a262099a04896c630718 Mon Sep 17 00:00:00 2001 From: arcuri82 Date: Fri, 16 Jan 2026 20:21:49 +0100 Subject: [PATCH 6/8] fixed information leakage check in fitness function --- .../core/problem/rest/data/RestCallAction.kt | 16 ++++++++++++++++ .../problem/rest/oracle/RestSecurityOracle.kt | 2 +- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/core/src/main/kotlin/org/evomaster/core/problem/rest/data/RestCallAction.kt b/core/src/main/kotlin/org/evomaster/core/problem/rest/data/RestCallAction.kt index c889d67ecf..b45d5d04b8 100644 --- a/core/src/main/kotlin/org/evomaster/core/problem/rest/data/RestCallAction.kt +++ b/core/src/main/kotlin/org/evomaster/core/problem/rest/data/RestCallAction.kt @@ -199,8 +199,24 @@ class RestCallAction( } fun usingSameResolvedPath(other: RestCallAction) = + //FIXME this does not consider dynamic fields? this.resolvedOnlyPath() == other.resolvedOnlyPath() + /** + * When the URL path of this endpoint is resolved, would it be a (strict) parent from the other action + */ + fun isResolvedParentPath(other: RestCallAction): Boolean { + + val parent = this.resolvedOnlyPath() // TODO deal with dynamic info + val child = other.resolvedOnlyPath() + + if(parent.length >= child.length) { + return false + } + return child.startsWith(parent) + } + + /** Note: in swagger the "consume" type might be missing. So, if for any reason there is a form param, then consider diff --git a/core/src/main/kotlin/org/evomaster/core/problem/rest/oracle/RestSecurityOracle.kt b/core/src/main/kotlin/org/evomaster/core/problem/rest/oracle/RestSecurityOracle.kt index 1c3e34270d..5cba1020a9 100644 --- a/core/src/main/kotlin/org/evomaster/core/problem/rest/oracle/RestSecurityOracle.kt +++ b/core/src/main/kotlin/org/evomaster/core/problem/rest/oracle/RestSecurityOracle.kt @@ -233,7 +233,7 @@ object RestSecurityOracle { //FIXME i don't think it is correct, as ignoring dynamic info? //TODO need tests for it val matching = verifiers.filter { - notfound.usingSameResolvedPath(it) + it.isResolvedParentPath(notfound) && ! notfound.auth.isDifferentFrom(it.auth) } From fb5e649743c2a75bcbd87a00fb7908fb8c552406 Mon Sep 17 00:00:00 2001 From: arcuri82 Date: Fri, 16 Jan 2026 20:44:15 +0100 Subject: [PATCH 7/8] cleaning/refactoring --- .../core/problem/rest/service/SecurityRest.kt | 50 ++++++------------- 1 file changed, 14 insertions(+), 36 deletions(-) diff --git a/core/src/main/kotlin/org/evomaster/core/problem/rest/service/SecurityRest.kt b/core/src/main/kotlin/org/evomaster/core/problem/rest/service/SecurityRest.kt index 3e0191276c..6ac0084de5 100644 --- a/core/src/main/kotlin/org/evomaster/core/problem/rest/service/SecurityRest.kt +++ b/core/src/main/kotlin/org/evomaster/core/problem/rest/service/SecurityRest.kt @@ -682,7 +682,7 @@ class SecurityRest { getOperations.forEach { get -> - val inds403 = RestIndividualSelectorUtils.findIndividuals( + val inds403 = RestIndividualSelectorUtils.findAndSlice( individualsInSolution, HttpVerb.GET, get.path, @@ -692,7 +692,7 @@ class SecurityRest { return@forEach } - val inds404 = RestIndividualSelectorUtils.findIndividuals( + val inds404 = RestIndividualSelectorUtils.findAndSlice( individualsInSolution, HttpVerb.GET, get.path, @@ -703,29 +703,10 @@ class SecurityRest { } //found the bug. - val forbidden = inds403.minBy { it.individual.size() } - val notfound = inds404.minBy { it.individual.size() } + val forbidden = inds403.minBy { it.size() } + val notfound = inds404.minBy { it.size() } - //needs slicing to minimize the newly generated test - val index403 = RestIndividualSelectorUtils.getIndexOfAction( - forbidden, - HttpVerb.GET, - get.path, - 403 - ) - // slice the individual in a way that delete all calls after the chosen verb request - val first = RestIndividualBuilder.sliceAllCallsInIndividualAfterAction(forbidden.individual, index403) - - val index404 = RestIndividualSelectorUtils.getIndexOfAction( - notfound, - HttpVerb.GET, - get.path, - 404 - ) - // slice the individual in a way that delete all calls after the chosen verb request - val second = RestIndividualBuilder.sliceAllCallsInIndividualAfterAction(notfound.individual, index404) - - val final = RestIndividualBuilder.merge(first, second) + val final = RestIndividualBuilder.merge(forbidden, notfound) // Check if the 404 path is a child resource and add parent GET to check access val path404 = get.path @@ -734,24 +715,20 @@ class SecurityRest { // Find the parent path from getOperations (all GET operation definitions) val parentGetOperation = getOperations - .filter { it.path.isDirectOrPossibleAncestorOf(path404) && !it.path.isEquivalent(path404) } - .minByOrNull { it.path.toString().length } // Get the top parent + .filter { it.path.isStrictlyAncestorOf(path404)} + .filter { it.path.isLastElementAParameter() } + .minByOrNull { it.path.levels() } if (parentGetOperation != null) { val parentGetAction = parentGetOperation.copy() as RestCallAction parentGetAction.resetLocalIdRecursively() parentGetAction.forceNewTaints() - parentGetAction.auth = lastAuth - - //FIXME this would not work for dynamic parameters - // Bind to the same path params from the 404 action to ensure same IDs - parentGetAction.bindBasedOn( - action404.path, - action404.parameters.filterIsInstance(), - null - ) parentGetAction.doInitialize() + parentGetAction.auth = lastAuth + // Bind to the same path params from the 404 action to ensure same IDs + //FIXME this would not work for dynamic parameters + parentGetAction.bindToSamePathResolution(action404) final.addResourceCall( restCalls = RestResourceCalls( @@ -759,9 +736,10 @@ class SecurityRest { sqlActions = listOf() ) ) + + //FIXME unclear. should be refactored with dealing of dynamic params final.seeMainExecutableActions() .filter { it.verb == HttpVerb.PUT || it.verb == HttpVerb.POST }.forEach { - //FIXME unclear. should be refactored with dealing of dynamic params it.saveCreatedResourceLocation = true } final.fixResourceForwardLinks() From d994da80022e588e9d61861e8ce096492c91c463 Mon Sep 17 00:00:00 2001 From: arcuri82 Date: Fri, 16 Jan 2026 20:49:14 +0100 Subject: [PATCH 8/8] removed unnecessary code --- .../evomaster/core/problem/rest/service/SecurityRest.kt | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/core/src/main/kotlin/org/evomaster/core/problem/rest/service/SecurityRest.kt b/core/src/main/kotlin/org/evomaster/core/problem/rest/service/SecurityRest.kt index 6ac0084de5..922bf5d685 100644 --- a/core/src/main/kotlin/org/evomaster/core/problem/rest/service/SecurityRest.kt +++ b/core/src/main/kotlin/org/evomaster/core/problem/rest/service/SecurityRest.kt @@ -727,7 +727,7 @@ class SecurityRest { parentGetAction.doInitialize() parentGetAction.auth = lastAuth // Bind to the same path params from the 404 action to ensure same IDs - //FIXME this would not work for dynamic parameters + //FIXME this would currently not work for dynamic parameters parentGetAction.bindToSamePathResolution(action404) final.addResourceCall( @@ -736,13 +736,6 @@ class SecurityRest { sqlActions = listOf() ) ) - - //FIXME unclear. should be refactored with dealing of dynamic params - final.seeMainExecutableActions() - .filter { it.verb == HttpVerb.PUT || it.verb == HttpVerb.POST }.forEach { - it.saveCreatedResourceLocation = true - } - final.fixResourceForwardLinks() } final.modifySampleType(SampleType.SECURITY)