Skip to content

Commit 4f56d35

Browse files
authored
Merge branch 'master' into dse/memoization
2 parents 455d28a + 92ded20 commit 4f56d35

15 files changed

Lines changed: 545 additions & 116 deletions

File tree

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
package com.foo.rest.examples.spring.openapi.v3.httporacle.invalidallow.auth
2+
3+
import org.springframework.boot.SpringApplication
4+
import org.springframework.boot.autoconfigure.SpringBootApplication
5+
import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration
6+
import org.springframework.http.ResponseEntity
7+
import org.springframework.web.bind.annotation.GetMapping
8+
import org.springframework.web.bind.annotation.PathVariable
9+
import org.springframework.web.bind.annotation.PutMapping
10+
import org.springframework.web.bind.annotation.RequestHeader
11+
import org.springframework.web.bind.annotation.RequestMapping
12+
import org.springframework.web.bind.annotation.RequestMethod
13+
import org.springframework.web.bind.annotation.RestController
14+
15+
16+
@SpringBootApplication(exclude = [SecurityAutoConfiguration::class])
17+
@RequestMapping(path = ["/api"])
18+
@RestController
19+
open class HttpInvalidAllowAuthApplication {
20+
21+
companion object {
22+
@JvmStatic
23+
fun main(args: Array<String>) {
24+
SpringApplication.run(HttpInvalidAllowAuthApplication::class.java, *args)
25+
}
26+
27+
val USERS = setOf("FOO", "BAR")
28+
29+
private val products = mutableMapOf<Int, String>()
30+
private val orders = mutableMapOf<Int, String>()
31+
32+
fun reset() {
33+
products.clear()
34+
orders.clear()
35+
}
36+
}
37+
38+
private fun isValidUser(auth: String?) = auth != null && USERS.contains(auth)
39+
40+
// OPTIONS is gated behind auth: without a valid user it returns 401, so the Allow
41+
// header can only be read after the oracle retries with an authenticated user.
42+
// The Allow lists DELETE, which is not declared in the schema (extra verb) -> fault.
43+
@RequestMapping(path = ["/products/{id}"], method = [RequestMethod.OPTIONS])
44+
open fun optionsProduct(
45+
@RequestHeader(value = "Authorization", required = false) auth: String?
46+
): ResponseEntity<Any> {
47+
if (!isValidUser(auth)) return ResponseEntity.status(401).build()
48+
return ResponseEntity.status(200).header("Allow", "GET,PUT,DELETE,HEAD,OPTIONS").build()
49+
}
50+
51+
@GetMapping(path = ["/products/{id}"])
52+
open fun getProduct(
53+
@RequestHeader(value = "Authorization", required = false) auth: String?,
54+
@PathVariable("id") id: Int
55+
): ResponseEntity<String> {
56+
if (!isValidUser(auth)) return ResponseEntity.status(401).build()
57+
val value = products[id] ?: return ResponseEntity.status(404).build()
58+
return ResponseEntity.status(200).body(value)
59+
}
60+
61+
@PutMapping(path = ["/products/{id}"])
62+
open fun putProduct(
63+
@RequestHeader(value = "Authorization", required = false) auth: String?,
64+
@PathVariable("id") id: Int
65+
): ResponseEntity<Any> {
66+
if (!isValidUser(auth)) return ResponseEntity.status(401).build()
67+
val isNew = !products.containsKey(id)
68+
products[id] = "$id"
69+
return ResponseEntity.status(if (isNew) 201 else 200).build()
70+
}
71+
72+
// Clean resource: Allow matches the schema (ignoring HEAD/OPTIONS).
73+
@RequestMapping(path = ["/orders/{id}"], method = [RequestMethod.OPTIONS])
74+
open fun optionsOrder(
75+
@RequestHeader(value = "Authorization", required = false) auth: String?
76+
): ResponseEntity<Any> {
77+
if (!isValidUser(auth)) return ResponseEntity.status(401).build()
78+
return ResponseEntity.status(200).header("Allow", "GET,PUT,HEAD,OPTIONS").build()
79+
}
80+
81+
@GetMapping(path = ["/orders/{id}"])
82+
open fun getOrder(
83+
@RequestHeader(value = "Authorization", required = false) auth: String?,
84+
@PathVariable("id") id: Int
85+
): ResponseEntity<String> {
86+
if (!isValidUser(auth)) return ResponseEntity.status(401).build()
87+
val value = orders[id] ?: return ResponseEntity.status(404).build()
88+
return ResponseEntity.status(200).body(value)
89+
}
90+
91+
@PutMapping(path = ["/orders/{id}"])
92+
open fun putOrder(
93+
@RequestHeader(value = "Authorization", required = false) auth: String?,
94+
@PathVariable("id") id: Int
95+
): ResponseEntity<Any> {
96+
if (!isValidUser(auth)) return ResponseEntity.status(401).build()
97+
val isNew = !orders.containsKey(id)
98+
orders[id] = "$id"
99+
return ResponseEntity.status(if (isNew) 201 else 200).build()
100+
}
101+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package com.foo.rest.examples.spring.openapi.v3.httporacle.invalidallow.auth
2+
3+
import com.foo.rest.examples.spring.openapi.v3.SpringController
4+
import org.evomaster.client.java.controller.AuthUtils
5+
import org.evomaster.client.java.controller.api.dto.auth.AuthenticationDto
6+
7+
8+
class HttpInvalidAllowAuthController : SpringController(HttpInvalidAllowAuthApplication::class.java) {
9+
10+
override fun getInfoForAuthentication(): List<AuthenticationDto> {
11+
return listOf(
12+
AuthUtils.getForAuthorizationHeader("FOO", "FOO"),
13+
AuthUtils.getForAuthorizationHeader("BAR", "BAR"),
14+
)
15+
}
16+
17+
override fun resetStateOfSUT() {
18+
HttpInvalidAllowAuthApplication.reset()
19+
}
20+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package org.evomaster.e2etests.spring.openapi.v3.httporacle.invalidallow
2+
3+
import com.foo.rest.examples.spring.openapi.v3.httporacle.invalidallow.auth.HttpInvalidAllowAuthController
4+
import org.evomaster.core.problem.enterprise.DetectedFaultUtils
5+
import org.evomaster.core.problem.enterprise.ExperimentalFaultCategory
6+
import org.evomaster.e2etests.spring.openapi.v3.SpringTestBase
7+
import org.junit.jupiter.api.Assertions.assertTrue
8+
import org.junit.jupiter.api.BeforeAll
9+
import org.junit.jupiter.api.Test
10+
11+
class HttpInvalidAllowAuthEMTest : SpringTestBase() {
12+
13+
companion object {
14+
@BeforeAll
15+
@JvmStatic
16+
fun init() {
17+
initClass(HttpInvalidAllowAuthController())
18+
}
19+
}
20+
21+
22+
@Test
23+
fun testRunEM() {
24+
25+
runTestHandlingFlakyAndCompilation(
26+
"HttpInvalidAllowAuthEM",
27+
20
28+
) { args: MutableList<String> ->
29+
30+
setOption(args, "security", "false")
31+
setOption(args, "schemaOracles", "false")
32+
setOption(args, "httpOracles", "true")
33+
setOption(args, "useExperimentalOracles", "true")
34+
35+
val solution = initAndRun(args)
36+
37+
assertTrue(solution.individuals.size >= 1)
38+
39+
// OPTIONS is 401 without auth: the fault is only found once the oracle
40+
// retries with an authenticated user and reads the 2xx Allow header.
41+
val faults = DetectedFaultUtils.getDetectedFaultCategories(solution)
42+
assertTrue({ ExperimentalFaultCategory.HTTP_INVALID_ALLOW in faults })
43+
44+
val allowFaults = DetectedFaultUtils.getDetectedFaults(solution)
45+
.filter { it.category == ExperimentalFaultCategory.HTTP_INVALID_ALLOW }
46+
47+
assertTrue(allowFaults.any { it.operationId.contains("/api/products/") })
48+
assertTrue(allowFaults.none { it.operationId.contains("/api/orders/") })
49+
}
50+
}
51+
}

core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/org/evomaster/e2etests/spring/openapi/v3/httporacle/invalidallow/missing/HttpMissingAllowEMTest.kt renamed to core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/org/evomaster/e2etests/spring/openapi/v3/httporacle/invalidallow/HttpMissingAllowEMTest.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package org.evomaster.e2etests.spring.openapi.v3.httporacle.invalidallow.missing
1+
package org.evomaster.e2etests.spring.openapi.v3.httporacle.invalidallow
22

33
import com.foo.rest.examples.spring.openapi.v3.httporacle.invalidallow.missing.HttpMissingAllowController
44
import org.evomaster.core.problem.enterprise.DetectedFaultUtils
@@ -46,4 +46,4 @@ class HttpMissingAllowEMTest : SpringTestBase() {
4646
assertTrue(allowFaults.none { it.operationId.contains("/api/orders/") })
4747
}
4848
}
49-
}
49+
}

core/src/main/kotlin/org/evomaster/core/parser/GeneRegexJavaVisitor.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -420,7 +420,7 @@ class GeneRegexJavaVisitor(externalRegexFlags: RegexFlags = RegexFlags()) : Rege
420420
}
421421

422422
if(ctx.DOT() != null){
423-
return VisitResult(AnyCharacterRxGene())
423+
return VisitResult(AnyCharacterRxGene(currentFlags))
424424
}
425425

426426
if(ctx.characterClass() != null){

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

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -11,38 +11,29 @@ enum class ExperimentalFaultCategory(
1111

1212
//9xx for experimental, work-in-progress oracles
1313

14-
/*
15-
TODO Is this one still relevant? or subsumed by SCHEMA_INVALID_RESPONSE?
16-
old comment was:
17-
syntactically invalid response (eg, non-quoted text when expecting JSON. this happens in pet-clinic for example)
18-
*/
19-
HTTP_INVALID_PAYLOAD_SYNTAX(911, "Invalid Payload Syntax", "rejectedWithInvalidPayloadSyntax",
20-
"TODO"),
21-
22-
23-
HTTP_INVALID_LOCATION(912, "Invalid Location HTTP Header", "returnsInvalidLocationHeader",
24-
"TODO"),
25-
HTTP_NONWORKING_DELETE(913,"Resource Still Accessible After Successful DELETE", "deleteDoesNotWork",
14+
HTTP_NONWORKING_DELETE(900,"Resource Still Accessible After Successful DELETE", "deleteDoesNotWork",
2615
"If a resource is deleted, and the API responds that such request was successful, then such" +
2716
" resource should no longer being available." +
2817
" New requests to access it should fail." +
2918
" Otherwise, if it is still possible to access the resource, then it was not really deleted." +
3019
" Then, as such, it means that the delete operation is faulty."),
31-
HTTP_REPEATED_CREATE_PUT(914, "Repeated PUT Creates Resource With 201 Instead of Updating", "repeatedCreatePut",
20+
HTTP_SIDE_EFFECTS_FAILED_MODIFICATION(901, "A Failed PUT or PATCH Must Not Change The Resource", "sideEffectsFailedModification",
3221
"TODO"),
33-
HTTP_SIDE_EFFECTS_FAILED_MODIFICATION(915, "A Failed PUT or PATCH Must Not Change The Resource", "sideEffectsFailedModification",
22+
HTTP_REPEATED_CREATE_PUT(902, "Repeated PUT Creates Resource With 201 Instead of Updating", "repeatedCreatePut",
3423
"TODO"),
35-
HTTP_PARTIAL_UPDATE_PUT(916, "The Verb PUT Must Make a Full Replacement", "partialUpdatePut",
24+
HTTP_MISLEADING_CREATE_PUT(903, "Misleading PUT 201 Creates When Resource Already Exists", "misleadingCreatePut",
3625
"TODO"),
37-
HTTP_MISLEADING_CREATE_PUT(917, "If a PUT Creates a Resource, It Must Return 201", "misleadingCreatePut",
26+
HTTP_PARTIAL_UPDATE_PUT(904, "The Verb PUT Must Make a Full Replacement", "partialUpdatePut",
3827
"TODO"),
39-
HTTP_NON_IDEMPOTENT_PUT(918, "PUT Implementation Must be Idempotent", "nonIdempotentPut",
28+
HTTP_NON_IDEMPOTENT_PUT(905, "PUT Implementation Must be Idempotent", "nonIdempotentPut",
4029
"TODO"),
41-
HTTP_INVALID_ALLOW(919, "Invalid Allow HTTP Header", "invalidAllow",
30+
HTTP_INVALID_MERGE_PATCH(906, "Invalid JSON Merge Patch", "invalidMergePatch",
4231
"TODO"),
43-
HTTP_INVALID_MERGE_PATCH(920, "Invalid JSON Merge Patch", "invalidMergePatch",
32+
HTTP_INVALID_LOCATION(907, "Invalid Location HTTP Header", "returnsInvalidLocationHeader",
4433
"TODO"),
45-
HTTP_TIMEOUT(921, "Request Timeout", "requestTimeout", "TODO"),
34+
HTTP_INVALID_ALLOW(908, "Invalid Allow HTTP Header", "invalidAllow",
35+
"TODO"),
36+
HTTP_TIMEOUT(909, "Request Timeout", "requestTimeout", "TODO"),
4637

4738
HTTP_STATUS_NO_NON_STANDARD_CODES(950, "HTTP/REST-Design Violation: no-non-standard-codes", "invalidStatusCode", "TODO"),
4839
HTTP_STATUS_NO_201_IF_DELETE(951, "HTTP/REST-Design Violation: no-201-if-delete", "201OnDelete", "TODO"),
@@ -61,6 +52,15 @@ enum class ExperimentalFaultCategory(
6152
HTTP_STATUS_NO_205_IF_CONTENT(964,"HTTP/REST-Design Violation: no-205-if-content","205WhenContent", "TODO"),
6253
HTTP_STATUS_NO_426_IF_NO_UPGRADE(965,"HTTP/REST-Design Violation: no-426-if-no-upgrade","426MissingUpgrade", "TODO"),
6354

55+
56+
/*
57+
TODO Is this one still relevant? or subsumed by SCHEMA_INVALID_RESPONSE?
58+
old comment was:
59+
syntactically invalid response (eg, non-quoted text when expecting JSON. this happens in pet-clinic for example)
60+
*/
61+
HTTP_INVALID_PAYLOAD_SYNTAX(929, "Invalid Payload Syntax", "rejectedWithInvalidPayloadSyntax",
62+
"TODO"),
63+
6464
//3xx: GraphQL
6565
GQL_ERROR_FIELD(930, "Error Field", "returnedErrors",
6666
"TODO"),

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

Lines changed: 33 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -154,18 +154,36 @@ class HttpSemanticsService : TimeBoxedPhase{
154154

155155
if (hasPhaseTimedOut()) return
156156

157-
val pathVariables = a.parameters
158-
.filterIsInstance<PathParam>()
159-
.map { it.copy() }
160-
.toMutableList()
157+
// own auth first, then every other user: on 401/403 retry until authorized
158+
val authCandidates = mutableListOf(a.auth)
159+
authCandidates.addAll(
160+
sampler.authentications.getOfType(HttpWsAuthenticationInfo::class.java)
161+
.filter { it.name != a.auth.name }
162+
)
161163

162-
val path = a.path.copy()
163-
val options = RestCallAction("${HttpVerb.OPTIONS}:$path", HttpVerb.OPTIONS, path, pathVariables, a.auth)
164-
options.doInitialize(randomness)
164+
for (auth in authCandidates) {
165165

166-
val ind = RestIndividual(mutableListOf(options), SampleType.HTTP_SEMANTICS)
167-
ind.doGlobalInitialize(globalState)
168-
prepareEvaluateAndSave(ind)
166+
if (hasPhaseTimedOut()) return
167+
168+
val pathVariables = a.parameters
169+
.filterIsInstance<PathParam>()
170+
.map { it.copy() }
171+
.toMutableList()
172+
173+
val path = a.path.copy()
174+
val options = RestCallAction("${HttpVerb.OPTIONS}:$path", HttpVerb.OPTIONS, path, pathVariables, auth)
175+
options.doInitialize(randomness)
176+
177+
val ind = RestIndividual(mutableListOf(options), SampleType.HTTP_SEMANTICS)
178+
ind.doGlobalInitialize(globalState)
179+
180+
val ei = evaluate(ind) ?: continue
181+
val status = (ei.evaluatedMainActions().firstOrNull()?.result as? RestCallResult)?.getStatusCode()
182+
183+
if (status == 401 || status == 403) continue // try next user, discard attempt
184+
archive.addIfNeeded(ei)
185+
break
186+
}
169187
}
170188
}
171189

@@ -204,7 +222,7 @@ class HttpSemanticsService : TimeBoxedPhase{
204222
}
205223
}
206224

207-
private fun prepareEvaluateAndSave(ind: RestIndividual): EvaluatedIndividual<RestIndividual>? {
225+
private fun evaluate(ind: RestIndividual): EvaluatedIndividual<RestIndividual>? {
208226
ind.modifySampleType(SampleType.HTTP_SEMANTICS)
209227
ind.ensureFlattenedStructure()
210228

@@ -213,7 +231,11 @@ class HttpSemanticsService : TimeBoxedPhase{
213231
log.warn("Failed to evaluate constructed individual in HTTP semantics testing phase")
214232
return null
215233
}
234+
return evaluatedIndividual
235+
}
216236

237+
private fun prepareEvaluateAndSave(ind: RestIndividual): EvaluatedIndividual<RestIndividual>? {
238+
val evaluatedIndividual = evaluate(ind) ?: return null
217239
archive.addIfNeeded(evaluatedIndividual)
218240
return evaluatedIndividual
219241
}

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1401,6 +1401,9 @@ abstract class AbstractRestFitness : HttpWsFitness<RestIndividual>() {
14011401
if (a.verb != HttpVerb.OPTIONS) continue
14021402

14031403
val r = actionResults.find { it.sourceLocalId == a.getLocalId() } as RestCallResult? ?: continue
1404+
// The Allow header can only be trusted on an authorized, successful response.
1405+
// On non-2xx (eg 401/403/404) it may be absent or reflect an error, so skip it.
1406+
if (!StatusGroup.G_2xx.isInGroup(r.getStatusCode())) continue
14041407
// The Allow header is not mandatory in an OPTIONS response
14051408
// (see https://httpwg.org/specs/rfc9110.html#OPTIONS), so if it is
14061409
// missing we cannot conclude anything, ie it is not a fault.

0 commit comments

Comments
 (0)