Skip to content

Commit 56a57cc

Browse files
authored
Merge pull request #1619 from WebFuzzing/issue-not-supported-body-types
fixed bug in handling of unsupported body types
2 parents 1baddbe + 98df7c2 commit 56a57cc

9 files changed

Lines changed: 258 additions & 62 deletions

File tree

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package com.foo.rest.examples.bb.bodyunsupported
2+
3+
import org.evomaster.e2etests.utils.CoveredTargets
4+
import org.springframework.boot.SpringApplication
5+
import org.springframework.boot.autoconfigure.SpringBootApplication
6+
import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration
7+
import org.springframework.http.MediaType
8+
import org.springframework.http.ResponseEntity
9+
import org.springframework.web.bind.annotation.PostMapping
10+
import org.springframework.web.bind.annotation.RequestBody
11+
import org.springframework.web.bind.annotation.RequestMapping
12+
import org.springframework.web.bind.annotation.RestController
13+
14+
15+
@RestController
16+
@RequestMapping(path = ["/api/bodyunsupported"])
17+
@SpringBootApplication(exclude = [SecurityAutoConfiguration::class])
18+
open class BBBodyUnsupportedApplication {
19+
20+
companion object {
21+
@JvmStatic
22+
fun main(args: Array<String>) {
23+
SpringApplication.run(BBBodyUnsupportedApplication::class.java, *args)
24+
}
25+
}
26+
27+
28+
@PostMapping(path = ["/octets"], consumes = [MediaType.APPLICATION_OCTET_STREAM_VALUE])
29+
fun postOctets(@RequestBody body: String) : ResponseEntity<String> {
30+
31+
if(body.isEmpty()){
32+
return ResponseEntity.status(400).build()
33+
}
34+
35+
CoveredTargets.cover("OCTETS")
36+
return ResponseEntity.ok().body("OK")
37+
}
38+
39+
@PostMapping(path = ["/pdf"], consumes = [MediaType.APPLICATION_PDF_VALUE])
40+
fun postPdf(@RequestBody body: String) : ResponseEntity<String> {
41+
if(body.isEmpty()){
42+
return ResponseEntity.status(400).build()
43+
}
44+
CoveredTargets.cover("PDF")
45+
return ResponseEntity.ok().body("OK")
46+
}
47+
48+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
{
2+
"openapi": "3.0.1",
3+
"info": {
4+
"title": "OpenAPI definition",
5+
"version": "v0"
6+
},
7+
"servers": [
8+
{
9+
"url": "http://localhost:8080",
10+
"description": "Generated server url"
11+
}
12+
],
13+
"paths": {
14+
"/api/bodyunsupported/pdf": {
15+
"post": {
16+
"tags": [
17+
"body-unsupported-application"
18+
],
19+
"operationId": "postPdf",
20+
"requestBody": {
21+
"content": {
22+
"application/pdf": {
23+
"schema": {
24+
"type": "string",
25+
"format": "binary"
26+
}
27+
}
28+
},
29+
"required": true
30+
},
31+
"responses": {
32+
"200": {
33+
"description": "OK",
34+
"content": {
35+
"*/*": {
36+
"schema": {
37+
"type": "string"
38+
}
39+
}
40+
}
41+
}
42+
}
43+
}
44+
},
45+
"/api/bodyunsupported/octets": {
46+
"post": {
47+
"tags": [
48+
"body-unsupported-application"
49+
],
50+
"operationId": "postOctets",
51+
"requestBody": {
52+
"content": {
53+
"application/octet-stream": {
54+
"schema": {
55+
"type": "string",
56+
"format": "binary"
57+
}
58+
}
59+
},
60+
"required": true
61+
},
62+
"responses": {
63+
"200": {
64+
"description": "OK",
65+
"content": {
66+
"*/*": {
67+
"schema": {
68+
"type": "string"
69+
}
70+
}
71+
}
72+
}
73+
}
74+
}
75+
}
76+
},
77+
"components": {}
78+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package com.foo.rest.examples.bb.bodyunsupported
2+
3+
import com.foo.rest.examples.bb.SpringController
4+
import org.evomaster.client.java.controller.problem.ProblemInfo
5+
import org.evomaster.client.java.controller.problem.RestProblem
6+
7+
class BBBodyUnsupportedController : SpringController(BBBodyUnsupportedApplication::class.java){
8+
9+
10+
override fun getProblemInfo(): ProblemInfo {
11+
return RestProblem(
12+
"http://localhost:$sutPort/openapi-bodyunsupported.json",
13+
null
14+
)
15+
}
16+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package org.evomaster.e2etests.spring.rest.bb.bodyunsupported
2+
3+
import com.foo.rest.examples.bb.bodyunsupported.BBBodyUnsupportedController
4+
import org.evomaster.core.output.OutputFormat
5+
import org.evomaster.core.problem.rest.data.HttpVerb
6+
import org.evomaster.e2etests.spring.rest.bb.SpringTestBase
7+
import org.junit.jupiter.api.Assertions.assertTrue
8+
import org.junit.jupiter.api.BeforeAll
9+
import org.junit.jupiter.params.ParameterizedTest
10+
import org.junit.jupiter.params.provider.EnumSource
11+
12+
class BBBodyUnsupportedEMTest : SpringTestBase() {
13+
14+
companion object {
15+
init {
16+
shouldApplyInstrumentation = false
17+
}
18+
19+
@BeforeAll
20+
@JvmStatic
21+
fun init() {
22+
initClass(BBBodyUnsupportedController())
23+
}
24+
}
25+
26+
@ParameterizedTest
27+
@EnumSource
28+
fun testBlackBoxOutput(outputFormat: OutputFormat) {
29+
30+
executeAndEvaluateBBTest(
31+
outputFormat,
32+
"bodyunsupported",
33+
100,
34+
3,
35+
listOf("OCTETS","PDF")
36+
){ args: MutableList<String> ->
37+
38+
setOption(args, "schema", "$baseUrlOfSut/openapi-bodyunsupported.json")
39+
40+
val solution = initAndRun(args)
41+
42+
assertTrue(solution.individuals.size >= 1)
43+
assertNone(solution, HttpVerb.POST, 415, "/api/bodyunsupported/octets", null)
44+
assertHasAtLeastOne(solution, HttpVerb.POST, 200, "/api/bodyunsupported/octets", "OK")
45+
assertNone(solution, HttpVerb.POST, 415, "/api/bodyunsupported/pdf", null)
46+
assertHasAtLeastOne(solution, HttpVerb.POST, 200, "/api/bodyunsupported/pdf", "OK")
47+
}
48+
}
49+
}

core/src/main/kotlin/org/evomaster/core/output/service/HttpWsTestCaseWriter.kt

Lines changed: 40 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -592,58 +592,17 @@ abstract class HttpWsTestCaseWriter : ApiTestCaseWriter() {
592592

593593
} else if (bodyParam.isTextPlain()) {
594594

595-
val body = bodyParam.getValueAsPrintableString(mode = GeneUtils.EscapeMode.TEXT, targetFormat = format)
596-
// handle body only if it is not black
597-
if (body.isNotBlank()) {
598-
if (body != "\"\"") {
599-
when {
600-
format.isCsharp() -> {
601-
lines.append("new StringContent(\"$body\", Encoding.UTF8, \"${bodyParam.contentType()}\")")
602-
}
603-
604-
format.isPython() -> {
605-
if (body.trim().isNullOrBlank()) {
606-
lines.add("body = \"\"")
607-
} else {
608-
lines.add("body = $body")
609-
}
610-
}
611-
612-
else -> lines.add(".$send($body)")
613-
}
614-
} else {
615-
when {
616-
format.isCsharp() -> {
617-
lines.append("new StringContent(\"${"""\"\""""}\", Encoding.UTF8, \"${bodyParam.contentType()}\")")
618-
}
619-
620-
format.isPython() -> {
621-
lines.add("body = \"\"")
622-
}
623-
624-
else -> lines.add(".$send(\"${"""\"\""""}\")")
625-
}
626-
}
627-
}
628-
629-
//BMR: this is needed because, if the string is empty, it causes a 400 (bad request) code on the test end.
630-
// inserting \"\" should prevent that problem
631-
// TODO: get some tests done of this
595+
handleTextBody(bodyParam, lines)
632596

633597
} else if (bodyParam.isForm()) {
634598
val body = bodyParam.gene.getValueAsPrintableString(
635599
mode = GeneUtils.EscapeMode.X_WWW_FORM_URLENCODED,
636600
targetFormat = format
637601
)
638602
when {
639-
format.isCsharp() -> {
640-
lines.append("new StringContent(\"$body\", Encoding.UTF8, \"${bodyParam.contentType()}\")")
641-
}
642-
643603
format.isPython() -> {
644604
lines.add("body = \"$body\"")
645605
}
646-
647606
else -> lines.add(".$send(\"$body\")")
648607
}
649608
} else if (bodyParam.isXml()) {
@@ -660,11 +619,49 @@ abstract class HttpWsTestCaseWriter : ApiTestCaseWriter() {
660619
else -> lines.add(".$send(\"$escapedXml\")")
661620
}
662621
} else {
663-
LoggingUtil.uniqueWarn(log, "Unhandled type for body payload: " + bodyParam.contentType())
622+
LoggingUtil.uniqueWarn(log, "Unhandled type for body payload: " + bodyParam.contentType() +
623+
". It will be handled as TEXT")
624+
handleTextBody(bodyParam, lines)
664625
}
665626

666627
}
667628

629+
private fun handleTextBody(
630+
bodyParam: BodyParam,
631+
lines: Lines,
632+
) {
633+
val send = sendBodyCommand()
634+
635+
val body = bodyParam.getValueAsPrintableString(mode = GeneUtils.EscapeMode.TEXT, targetFormat = format)
636+
637+
val text = GeneUtils.applyEscapes(body, mode = GeneUtils.EscapeMode.TEXT, format = format)
638+
639+
// handle body only if it is not black
640+
if (body.isNotBlank()) {
641+
if (body != "\"\"") {
642+
when {
643+
format.isPython() -> {
644+
if (body.trim().isBlank()) {
645+
lines.add("body = \"\"")
646+
} else {
647+
lines.add("body = \"$text\"")
648+
}
649+
}
650+
651+
else -> lines.add(".$send(\"$text\")")
652+
}
653+
} else {
654+
when {
655+
format.isPython() -> {
656+
lines.add("body = \"\"")
657+
}
658+
//TODO isn't this valid just for Kotlin???
659+
else -> lines.add(".$send(\"${"""\"\""""}\")")
660+
}
661+
}
662+
}
663+
}
664+
668665
fun printSendJsonBody(json: String, lines: Lines, dtoVar: String? = null) {
669666

670667
if(json.isEmpty()){

core/src/main/kotlin/org/evomaster/core/output/service/TestSuiteWriter.kt

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -502,19 +502,20 @@ class TestSuiteWriter {
502502
addImport(RedisInsertionDto::class.qualifiedName!!, lines)
503503
}
504504

505+
if (useRestAssured()) {
506+
addImport("io.restassured.config.JsonConfig", lines)
507+
addImport("io.restassured.path.json.config.JsonPathConfig", lines)
508+
addImport("io.restassured.config.RedirectConfig.redirectConfig", lines, true)
509+
addImport("io.restassured.config.EncoderConfig", lines)
510+
addImport("io.restassured.http.ContentType", lines)
511+
}
512+
505513
if (config.enableBasicAssertions) {
506514

507515
if(useHamcrest()) {
508516
addImport("org.hamcrest.Matchers.*", lines, true)
509517
}
510518

511-
//addImport("org.hamcrest.core.AnyOf.anyOf", lines, true)
512-
if (useRestAssured()) {
513-
addImport("io.restassured.config.JsonConfig", lines)
514-
addImport("io.restassured.path.json.config.JsonPathConfig", lines)
515-
addImport("io.restassured.config.RedirectConfig.redirectConfig", lines, true)
516-
}
517-
518519
addImport("org.evomaster.client.java.controller.contentMatchers.NumberMatcher.*", lines, true)
519520
addImport("org.evomaster.client.java.controller.contentMatchers.StringMatcher.*", lines, true)
520521
addImport("org.evomaster.client.java.controller.contentMatchers.SubStringMatcher.*", lines, true)
@@ -843,6 +844,7 @@ class TestSuiteWriter {
843844
lines.indented {
844845
lines.add(".jsonConfig(JsonConfig.jsonConfig().numberReturnType(JsonPathConfig.NumberReturnType.DOUBLE))")
845846
lines.add(".redirect(redirectConfig().followRedirects(false))")
847+
lines.add(".encoderConfig(EncoderConfig.encoderConfig().encodeContentTypeAs(\"application/octet-stream\", ContentType.TEXT))")
846848
}
847849
lines.appendSemicolon()
848850
}

core/src/main/kotlin/org/evomaster/core/problem/rest/builder/RestActionBuilderV3.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -790,7 +790,8 @@ object RestActionBuilderV3 {
790790

791791
val ns = bodyParam.notSupportedContentTypes
792792
if(ns.isNotEmpty()){
793-
messages.add("Not supported content types for body payload in $verb:$restPath : ${ns.joinToString()}")
793+
messages.add("Not supported content types for body payload in $verb:$restPath : ${ns.joinToString()}." +
794+
" It will be treated as TEXT.")
794795
}
795796

796797
params.add(bodyParam)

core/src/main/kotlin/org/evomaster/core/problem/rest/param/BodyParam.kt

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -50,10 +50,12 @@ class BodyParam(gene: Gene,
5050

5151
notSupportedContentTypes = typeGene.values.filter { !isSupportedType(it)}
5252

53-
val options = typeGene.values.filter { isSupportedType(it) }.toMutableList()
54-
if(options.isEmpty()){
53+
val options = typeGene.values
54+
//.filter { isSupportedType(it) } //not supported will be treated as TEXT, to avoid useless 415 requests
55+
.toMutableList()
56+
5557

56-
if(typeGene.values.any { isContentTypeMultipartForm(it) }){
58+
if(typeGene.values.any { isContentTypeMultipartForm(it) }){
5759
/*
5860
This is tricky... we have seen cases in V2 in which formData without an explicit
5961
application/x-www-form-urlencoded turns by V3 parser into a multipart/form-data.
@@ -64,14 +66,13 @@ class BodyParam(gene: Gene,
6466
TODO handle it properly
6567
*/
6668
options.add("application/x-www-form-urlencoded")
69+
}
6770

68-
} else {
69-
70-
/*
71-
If no info, or not supported, we just try with JSON
72-
*/
73-
options.add("application/json")
74-
}
71+
if(options.isEmpty()){
72+
/*
73+
If no info, we just try with JSON
74+
*/
75+
options.add("application/json")
7576
}
7677

7778
contentTypeGene = EnumGene(typeGene.name, options, typeGene.index)

0 commit comments

Comments
 (0)