Skip to content
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package com.foo.rest.examples.spring.openapi.v3.security.ssrf.uri

import io.swagger.v3.oas.annotations.Operation
import io.swagger.v3.oas.annotations.responses.ApiResponse
import io.swagger.v3.oas.annotations.responses.ApiResponses
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.GetMapping
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.RestController
import java.net.HttpURLConnection
import java.net.URI

@SpringBootApplication(exclude = [SecurityAutoConfiguration::class])
@RequestMapping(path = ["/api"])
@RestController
open class SSRFUriApplication {

companion object {
@JvmStatic
fun main(args: Array<String>) {
SpringApplication.run(SSRFUriApplication::class.java, *args)
}
}

@Operation(
summary = "GET endpoint to fetch data from remote source",
description = "Can be used to fetch data from remote source."
)
@ApiResponses(
value = [
ApiResponse(responseCode = "200", description = "Successful response"),
ApiResponse(responseCode = "204", description = "No data to fetch"),
ApiResponse(responseCode = "400", description = "Invalid request"),
ApiResponse(responseCode = "500", description = "Invalid server error")
]
)
@GetMapping(path = ["/uri"])
open fun uriTest(@RequestParam dataSource: String): ResponseEntity<String> {
if (dataSource != null) {
return try {
val uri = URI(dataSource)
if (uri.scheme == "http") {
val connection = uri.toURL().openConnection() as HttpURLConnection
connection.requestMethod = "GET"
connection.connectTimeout = 1000

if (connection.responseCode == 200) {
return ResponseEntity.status(200).body("OK")
}
ResponseEntity.status(204).body("Unable to fetch.")
}
ResponseEntity.status(204).body("Unable to fetch.")
} catch (e: Exception) {
ResponseEntity.status(204).body("Unable to fetch.")
}
}

return ResponseEntity.badRequest().body("Invalid request")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.foo.rest.examples.spring.openapi.v3.security.ssrf.uri

import com.foo.rest.examples.spring.openapi.v3.SpringController

class SSRFUriController: SpringController(SSRFUriApplication::class.java)
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package org.evomaster.e2etests.spring.openapi.v3.security.ssrf.uri

import com.foo.rest.examples.spring.openapi.v3.security.ssrf.uri.SSRFUriController
import org.evomaster.core.EMConfig
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.BeforeAll
import org.junit.jupiter.api.Test

class SSRFUriEMTest: SpringTestBase() {

companion object {
@BeforeAll
@JvmStatic
fun init() {
val config = EMConfig()
config.instrumentMR_NET = false
initClass(SSRFUriController(), config)
}
}

@Test
fun testSSRFUri() {
runTestHandlingFlakyAndCompilation(
"SSRFUriGeneratedTest",
30,
) { args: MutableList<String> ->

// If mocking enabled, it'll spin new services each time when there is a valid URL.
setOption(args, "externalServiceIPSelectionStrategy", "NONE")

setOption(args, "security", "true")
setOption(args, "ssrf", "true")
setOption(args, "vulnerableInputClassificationStrategy", "MANUAL")
setOption(args, "schemaOracles", "false")

val solution = initAndRun(args)

assertTrue(solution.individuals.isNotEmpty())
assertTrue(solution.hasSsrfFaults())

assertHasAtLeastOne(solution, HttpVerb.GET, 200, "/api/uri", "OK")
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,6 @@ class Base64StringGene(
data.randomize(randomness, tryToForceNewValue)
}



override fun getValueAsPrintableString(previousGenes: List<Gene>, mode: GeneUtils.EscapeMode?, targetFormat: OutputFormat?, extraCheck: Boolean): String {
return Base64.getEncoder().encodeToString(data.value.toByteArray())
}
Expand All @@ -58,6 +56,24 @@ class Base64StringGene(
}
}

@Deprecated("Do not call directly outside this package. Call setFromStringValue")
override fun unsafeSetFromStringValue(value: String): Boolean {
// Since getValueAsPrintableString() uses encodeToString(), if the given
// string is base64 encoded, value will be decoded to original string.
// Otherwise, given value will be set.
return try {
// Charset is set to UTF_8 to ensure decoding works properly
val value = Base64
.getDecoder()
.decode(value)
.toString(Charsets.UTF_8)

data.unsafeSetFromStringValue(value)
} catch (_: Exception) {
false
}
}

override fun customShouldApplyShallowMutation(
randomness: Randomness,
selectionStrategy: SubsetGeneMutationSelectionStrategy,
Expand All @@ -66,5 +82,4 @@ class Base64StringGene(
): Boolean {
return false
}

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,6 @@ class UriDataGene(
getViewOfChildren().forEach { it.randomize(randomness, tryToForceNewValue) }
}




override fun getValueAsPrintableString(
previousGenes: List<Gene>,
mode: GeneUtils.EscapeMode?,
Expand Down Expand Up @@ -90,4 +87,29 @@ class UriDataGene(
return false
}

}
@Deprecated("Do not call directly outside this package. Call setFromStringValue")
override fun unsafeSetFromStringValue(value: String): Boolean {
// TODO: Charset value is not handled in UriDataGene.
// If the encoded string uses a different Charset test will fail,
// since the Base64StringGene.unsafeSetFromStringValue() use UTF_8 to decode the value.
return try {
val uri = URI(value)

if (uri.scheme == "data") {
val uriParts = uri.schemeSpecificPart
val parts = uriParts.split(",", limit = 2)
val metadata = parts[0].split(";")
val b64Value = metadata[2].equals("base64", ignoreCase = true)

val t = type.unsafeSetFromStringValue(metadata[0])
val b64 = base64.unsafeSetFromStringValue(b64Value.toString())
val data = data.unsafeSetFromStringValue(parts[1])
t && b64 && data
} else {
false
}
} catch (_: Exception) {
false
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,10 @@ class UriGene(name: String,
return gene.unsafeCopyValueFrom(other)
}

@Deprecated("Do not call directly outside this package. Call setFromStringValue")
override fun unsafeSetFromStringValue(value: String): Boolean {
return gene.unsafeSetFromStringValue(value)
}

override fun customShouldApplyShallowMutation(
randomness: Randomness,
Expand All @@ -97,4 +101,4 @@ class UriGene(name: String,
return false
}

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package org.evomaster.core.search.gene
import org.evomaster.core.search.gene.string.Base64StringGene
import org.evomaster.core.search.gene.string.StringGene
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test

internal class Base64StringGeneTest{
Expand All @@ -18,5 +19,16 @@ internal class Base64StringGeneTest{

assertEquals(expected, gene.getValueAsPrintableString(targetFormat = null))
}

@Test
fun testUnsafeSetFromString(){
val gene = Base64StringGene("gene", StringGene("data", "kotlin"))

assertEquals("a290bGlu", gene.getValueAsPrintableString(targetFormat = null))

assertTrue(gene.unsafeSetFromStringValue("a290bGluIDEyMw=="))

assertEquals("a290bGluIDEyMw==", gene.getValueAsPrintableString(targetFormat = null))
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package org.evomaster.core.search.gene.uri

import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test

class UriGeneTest {

@Test
fun testSetValueBasedOnForUriDataGene() {
val gene = UriGene("data:content/type;base64,")

assertTrue(gene.unsafeSetFromStringValue("data:text/plain;charset=UTF-8;base64,R0lGODdh"))

// TODO: Charset value is not handled in UriDataGene.
// If the encoded string uses a different Charset test will fail,
// since the Base64StringGene.unsafeSetFromStringValue() use UTF_8 to decode the value.
assertEquals("data:text/plain;base64,R0lGODdh", gene.getValueAsRawString())
}
}