diff --git a/core-tests/e2e-tests/spring/spring-rest-mysql/src/main/kotlin/com/foo/spring/rest/mysql/sqli/body/BodySQLiApplication.kt b/core-tests/e2e-tests/spring/spring-rest-mysql/src/main/kotlin/com/foo/spring/rest/mysql/sqli/body/BodySQLiApplication.kt new file mode 100644 index 0000000000..b805207e11 --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-mysql/src/main/kotlin/com/foo/spring/rest/mysql/sqli/body/BodySQLiApplication.kt @@ -0,0 +1,100 @@ +package com.foo.spring.rest.mysql.sqli.body + +import com.foo.spring.rest.mysql.SwaggerConfiguration +import io.swagger.v3.oas.annotations.Operation +import io.swagger.v3.oas.annotations.media.ArraySchema +import io.swagger.v3.oas.annotations.media.Content +import io.swagger.v3.oas.annotations.media.Schema +import io.swagger.v3.oas.annotations.parameters.RequestBody +import io.swagger.v3.oas.annotations.responses.ApiResponse +import io.swagger.v3.oas.annotations.responses.ApiResponses +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.SpringApplication +import org.springframework.boot.autoconfigure.SpringBootApplication +import org.springframework.boot.autoconfigure.domain.EntityScan +import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration +import org.springframework.context.annotation.ComponentScan +import org.springframework.data.jpa.repository.config.EnableJpaRepositories +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.* +import springfox.documentation.swagger2.annotations.EnableSwagger2 +import java.sql.Connection +import javax.annotation.PostConstruct +import javax.sql.DataSource + + +@EnableSwagger2 +@SpringBootApplication(exclude = [SecurityAutoConfiguration::class]) +@RequestMapping(path = ["/api/sqli/body"]) +@RestController +open class BodySQLiApplication: SwaggerConfiguration() { + + @Autowired + private lateinit var dataSource: DataSource + + @Autowired + private lateinit var userRepository: UserRepository + + private var connection: Connection? = null + + companion object { + @JvmStatic + fun main(args: Array) { + SpringApplication.run(BodySQLiApplication::class.java, *args) + } + } + + @PostConstruct + fun init() { + connection = dataSource.connection + initializeTestData() + } + + private fun initializeTestData() { + if (userRepository.count() == 0L) { + userRepository.save(UserEntity(null, "admin", "admin123")) + userRepository.save(UserEntity(null, "user1", "password1")) + } + } + + /** + * Safe endpoint - No SQL Injection vulnerability + */ + @GetMapping("/safe") + @Operation(summary = "Safe Query - No SQL Injection") + @ApiResponses(value = [ + ApiResponse(responseCode = "200", description = "Successful operation", + content = [Content(mediaType = "application/json", + array = ArraySchema(schema = Schema(implementation = UserDto::class)))]) + ]) + fun getSafeUsers(): ResponseEntity> { + val users = userRepository.findAll().map { UserDto(it.id, it.username) } + return ResponseEntity.ok(users) + } + + /** + * Attack: POST {"username":"admin' AND (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS A, INFORMATION_SCHEMA.COLUMNS B, INFORMATION_SCHEMA.COLUMNS C)>0--","password":"x"} + */ + @PostMapping("/vulnerable") + @Operation(summary = "SQL Injection - Body Parameter") + @ApiResponses(value = [ + ApiResponse(responseCode = "200", description = "Successful operation", + content = [Content(mediaType = "text/plain", schema = Schema(implementation = String::class))]), + ApiResponse(responseCode = "500", description = "Internal server error", + content = [Content(mediaType = "text/plain", schema = Schema(implementation = String::class))]) + ]) + @RequestBody(description = "Login credentials", required = true, + content = [Content(mediaType = "application/json", + schema = Schema(implementation = LoginDto::class))]) + fun body(@org.springframework.web.bind.annotation.RequestBody loginDto: LoginDto): ResponseEntity { + return try { + val stmt = connection?.createStatement() + val rs = stmt?.executeQuery("SELECT COUNT(*) as cnt FROM users WHERE username = '${loginDto.username}' AND password = '${loginDto.password}'") + val count = if (rs?.next() == true) rs.getInt("cnt") else 0 + ResponseEntity.ok("MATCHED: $count") + } catch (e: Exception) { + ResponseEntity.status(500).body("ERROR: ${e.message}") + } + } + +} diff --git a/core-tests/e2e-tests/spring/spring-rest-mysql/src/main/kotlin/com/foo/spring/rest/mysql/sqli/body/UserDto.kt b/core-tests/e2e-tests/spring/spring-rest-mysql/src/main/kotlin/com/foo/spring/rest/mysql/sqli/body/UserDto.kt new file mode 100644 index 0000000000..c3ff2c756d --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-mysql/src/main/kotlin/com/foo/spring/rest/mysql/sqli/body/UserDto.kt @@ -0,0 +1,13 @@ +package com.foo.spring.rest.mysql.sqli.body + +import com.fasterxml.jackson.annotation.JsonProperty + +data class UserDto( + @JsonProperty("id") var id: Long? = null, + @JsonProperty("username") var username: String? = null, +) + +data class LoginDto( + @JsonProperty("username") var username: String = "", + @JsonProperty("password") var password: String = "" +) diff --git a/core-tests/e2e-tests/spring/spring-rest-mysql/src/main/kotlin/com/foo/spring/rest/mysql/sqli/body/UserEntity.kt b/core-tests/e2e-tests/spring/spring-rest-mysql/src/main/kotlin/com/foo/spring/rest/mysql/sqli/body/UserEntity.kt new file mode 100644 index 0000000000..0814f815de --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-mysql/src/main/kotlin/com/foo/spring/rest/mysql/sqli/body/UserEntity.kt @@ -0,0 +1,19 @@ +package com.foo.spring.rest.mysql.sqli.body + +import javax.persistence.* + + +@Entity +@Table(name = "users") +open class UserEntity( + + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + open var id: Long? = null, + + @Column(name = "username", unique = true, nullable = false) + open var username: String? = null, + + @Column(name = "password", nullable = false) + open var password: String? = null, +) diff --git a/core-tests/e2e-tests/spring/spring-rest-mysql/src/main/kotlin/com/foo/spring/rest/mysql/sqli/body/UserRepository.kt b/core-tests/e2e-tests/spring/spring-rest-mysql/src/main/kotlin/com/foo/spring/rest/mysql/sqli/body/UserRepository.kt new file mode 100644 index 0000000000..4cf41e73f4 --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-mysql/src/main/kotlin/com/foo/spring/rest/mysql/sqli/body/UserRepository.kt @@ -0,0 +1,10 @@ +package com.foo.spring.rest.mysql.sqli.body + +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.stereotype.Repository + + +@Repository +interface UserRepository : JpaRepository { + +} diff --git a/core-tests/e2e-tests/spring/spring-rest-mysql/src/main/kotlin/com/foo/spring/rest/mysql/sqli/path/PathSQLiApplication.kt b/core-tests/e2e-tests/spring/spring-rest-mysql/src/main/kotlin/com/foo/spring/rest/mysql/sqli/path/PathSQLiApplication.kt new file mode 100644 index 0000000000..073dbd92ad --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-mysql/src/main/kotlin/com/foo/spring/rest/mysql/sqli/path/PathSQLiApplication.kt @@ -0,0 +1,80 @@ +package com.foo.spring.rest.mysql.sqli.path + + +import io.swagger.v3.oas.annotations.Operation +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.SpringApplication +import org.springframework.boot.autoconfigure.SpringBootApplication +import org.springframework.boot.autoconfigure.domain.EntityScan +import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration +import org.springframework.context.annotation.ComponentScan +import org.springframework.data.jpa.repository.config.EnableJpaRepositories +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.* +import springfox.documentation.swagger2.annotations.EnableSwagger2 +import java.sql.Connection +import javax.annotation.PostConstruct +import javax.sql.DataSource + + +@EnableSwagger2 +@SpringBootApplication(exclude = [SecurityAutoConfiguration::class]) +@RequestMapping(path = ["/api/sqli/path"]) +@RestController +open class PathSQLiApplication { + + @Autowired + private lateinit var dataSource: DataSource + + @Autowired + private lateinit var userRepository: UserRepository + + private var connection: Connection? = null + + companion object { + @JvmStatic + fun main(args: Array) { + SpringApplication.run(PathSQLiApplication::class.java, *args) + } + } + + @PostConstruct + fun init() { + connection = dataSource.connection + initializeTestData() + } + + private fun initializeTestData() { + if (userRepository.count() == 0L) { + userRepository.save(UserEntity(null, "admin", "admin123")) + userRepository.save(UserEntity(null, "user1", "password1")) + } + } + + /** + * Safe endpoint - No SQL Injection vulnerability + */ + @GetMapping("/safe") + @Operation(summary = "Safe Query - No SQL Injection") + fun getSafeUsers(): ResponseEntity> { + val users = userRepository.findAll().map { UserDto(it.id, it.username) } + return ResponseEntity.ok(users) + } + + /** + * Attack: GET /api/sqli/path/vulnerable/admin' OR (SELECT SUM(a.ORDINAL_POSITION*b.ORDINAL_POSITION*c.ORDINAL_POSITION) FROM INFORMATION_SCHEMA.COLUMNS a, INFORMATION_SCHEMA.COLUMNS b, INFORMATION_SCHEMA.COLUMNS c)>1 -- + */ + @GetMapping("/vulnerable/{id}") + @Operation(summary = "SQL Injection - Path Parameter") + fun timeBasedPath(@PathVariable id: String): ResponseEntity { + return try { + val stmt = connection?.createStatement() + val rs = stmt?.executeQuery("SELECT username FROM users WHERE username = '$id'") + val username = if (rs?.next() == true) rs.getString("username") else "NOT_FOUND" + ResponseEntity.ok("USERNAME: $username") + } catch (e: Exception) { + ResponseEntity.status(500).body("ERROR: ${e.message}") + } + } + +} diff --git a/core-tests/e2e-tests/spring/spring-rest-mysql/src/main/kotlin/com/foo/spring/rest/mysql/sqli/path/UserDto.kt b/core-tests/e2e-tests/spring/spring-rest-mysql/src/main/kotlin/com/foo/spring/rest/mysql/sqli/path/UserDto.kt new file mode 100644 index 0000000000..fed8a7bd87 --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-mysql/src/main/kotlin/com/foo/spring/rest/mysql/sqli/path/UserDto.kt @@ -0,0 +1,12 @@ +package com.foo.spring.rest.mysql.sqli.path + + +data class UserDto( + var id: Long? = null, + var username: String? = null, +) + +data class LoginDto( + var username: String = "", + var password: String = "" +) diff --git a/core-tests/e2e-tests/spring/spring-rest-mysql/src/main/kotlin/com/foo/spring/rest/mysql/sqli/path/UserEntity.kt b/core-tests/e2e-tests/spring/spring-rest-mysql/src/main/kotlin/com/foo/spring/rest/mysql/sqli/path/UserEntity.kt new file mode 100644 index 0000000000..34fc831534 --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-mysql/src/main/kotlin/com/foo/spring/rest/mysql/sqli/path/UserEntity.kt @@ -0,0 +1,19 @@ +package com.foo.spring.rest.mysql.sqli.path + +import javax.persistence.* + + +@Entity +@Table(name = "users") +open class UserEntity( + + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + open var id: Long? = null, + + @Column(name = "username", unique = true, nullable = false) + open var username: String? = null, + + @Column(name = "password", nullable = false) + open var password: String? = null, +) diff --git a/core-tests/e2e-tests/spring/spring-rest-mysql/src/main/kotlin/com/foo/spring/rest/mysql/sqli/path/UserRepository.kt b/core-tests/e2e-tests/spring/spring-rest-mysql/src/main/kotlin/com/foo/spring/rest/mysql/sqli/path/UserRepository.kt new file mode 100644 index 0000000000..fa4ff4ec2a --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-mysql/src/main/kotlin/com/foo/spring/rest/mysql/sqli/path/UserRepository.kt @@ -0,0 +1,10 @@ +package com.foo.spring.rest.mysql.sqli.path + +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.stereotype.Repository + + +@Repository +interface UserRepository : JpaRepository { + +} diff --git a/core-tests/e2e-tests/spring/spring-rest-mysql/src/main/kotlin/com/foo/spring/rest/mysql/sqli/query/QuerySQLiApplication.kt b/core-tests/e2e-tests/spring/spring-rest-mysql/src/main/kotlin/com/foo/spring/rest/mysql/sqli/query/QuerySQLiApplication.kt new file mode 100644 index 0000000000..e42b4685c0 --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-mysql/src/main/kotlin/com/foo/spring/rest/mysql/sqli/query/QuerySQLiApplication.kt @@ -0,0 +1,76 @@ +package com.foo.spring.rest.mysql.sqli.query + +import com.foo.spring.rest.mysql.SwaggerConfiguration +import io.swagger.v3.oas.annotations.Operation +import org.springframework.beans.factory.annotation.Autowired +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 springfox.documentation.swagger2.annotations.EnableSwagger2 +import java.sql.Connection +import javax.annotation.PostConstruct +import javax.sql.DataSource + + +@EnableSwagger2 +@SpringBootApplication(exclude = [SecurityAutoConfiguration::class]) +@RequestMapping(path = ["/api/sqli/query"]) +open class QuerySQLiApplication: SwaggerConfiguration() { + + @Autowired + private lateinit var dataSource: DataSource + + @Autowired + private lateinit var userRepository: UserRepository + + private var connection: Connection? = null + + companion object { + @JvmStatic + fun main(args: Array) { + SpringApplication.run(QuerySQLiApplication::class.java, *args) + } + } + + @PostConstruct + fun init() { + connection = dataSource.connection + initializeTestData() + } + + private fun initializeTestData() { + if (userRepository.count() == 0L) { + userRepository.save(UserEntity(null, "admin", "admin123")) + userRepository.save(UserEntity(null, "user1", "password1")) + } + } + + /** + * Safe endpoint - No SQL Injection vulnerability + */ + @GetMapping("/safe") + @Operation(summary = "Safe Query - No SQL Injection") + fun getSafeUsers(): ResponseEntity> { + val users = userRepository.findAll().map { UserDto(it.id, it.username) } + return ResponseEntity.ok(users) + } + + /** + * Attack: GET /api/sqli/query/vulnerable?username=admin' OR (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS A, INFORMATION_SCHEMA.COLUMNS B, INFORMATION_SCHEMA.COLUMNS C)>0-- + */ + @GetMapping("/vulnerable") + @Operation(summary = "SQL Injection - Query Parameter") + fun timeBasedQuery(@RequestParam username: String): ResponseEntity { + return try { + val stmt = connection?.createStatement() + val rs = stmt?.executeQuery("SELECT COUNT(*) as cnt FROM users WHERE username = '$username'") + val count = if (rs?.next() == true) rs.getInt("cnt") else 0 + ResponseEntity.ok("COUNT: $count") + } catch (e: Exception) { + ResponseEntity.status(500).body("ERROR: ${e.message}") + } + } + +} diff --git a/core-tests/e2e-tests/spring/spring-rest-mysql/src/main/kotlin/com/foo/spring/rest/mysql/sqli/query/UserDto.kt b/core-tests/e2e-tests/spring/spring-rest-mysql/src/main/kotlin/com/foo/spring/rest/mysql/sqli/query/UserDto.kt new file mode 100644 index 0000000000..5cc7cad84f --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-mysql/src/main/kotlin/com/foo/spring/rest/mysql/sqli/query/UserDto.kt @@ -0,0 +1,12 @@ +package com.foo.spring.rest.mysql.sqli.query + + +data class UserDto( + var id: Long? = null, + var username: String? = null, +) + +data class LoginDto( + var username: String, + var password: String +) diff --git a/core-tests/e2e-tests/spring/spring-rest-mysql/src/main/kotlin/com/foo/spring/rest/mysql/sqli/query/UserEntity.kt b/core-tests/e2e-tests/spring/spring-rest-mysql/src/main/kotlin/com/foo/spring/rest/mysql/sqli/query/UserEntity.kt new file mode 100644 index 0000000000..083c6738dc --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-mysql/src/main/kotlin/com/foo/spring/rest/mysql/sqli/query/UserEntity.kt @@ -0,0 +1,19 @@ +package com.foo.spring.rest.mysql.sqli.query + +import javax.persistence.* + + +@Entity +@Table(name = "users") +open class UserEntity( + + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + open var id: Long? = null, + + @Column(name = "username", unique = true, nullable = false) + open var username: String? = null, + + @Column(name = "password", nullable = false) + open var password: String? = null, +) diff --git a/core-tests/e2e-tests/spring/spring-rest-mysql/src/main/kotlin/com/foo/spring/rest/mysql/sqli/query/UserRepository.kt b/core-tests/e2e-tests/spring/spring-rest-mysql/src/main/kotlin/com/foo/spring/rest/mysql/sqli/query/UserRepository.kt new file mode 100644 index 0000000000..94a37dca46 --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-mysql/src/main/kotlin/com/foo/spring/rest/mysql/sqli/query/UserRepository.kt @@ -0,0 +1,10 @@ +package com.foo.spring.rest.mysql.sqli.query + +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.stereotype.Repository + + +@Repository +interface UserRepository : JpaRepository { + +} diff --git a/core-tests/e2e-tests/spring/spring-rest-mysql/src/main/resources/schema/sqli/V1.0__createDB.sql b/core-tests/e2e-tests/spring/spring-rest-mysql/src/main/resources/schema/sqli/V1.0__createDB.sql new file mode 100644 index 0000000000..ecaa651a7f --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-mysql/src/main/resources/schema/sqli/V1.0__createDB.sql @@ -0,0 +1,12 @@ +CREATE TABLE users ( + id BIGINT NOT NULL AUTO_INCREMENT, + username VARCHAR(255) NOT NULL UNIQUE, + password VARCHAR(255) NOT NULL, + PRIMARY KEY (id) +); + +CREATE TABLE hibernate_sequence ( + next_val BIGINT +); + +INSERT INTO hibernate_sequence VALUES (1); diff --git a/core-tests/e2e-tests/spring/spring-rest-mysql/src/test/kotlin/com/foo/spring/rest/mysql/sqli/SQLiMySQLBodyController.kt b/core-tests/e2e-tests/spring/spring-rest-mysql/src/test/kotlin/com/foo/spring/rest/mysql/sqli/SQLiMySQLBodyController.kt new file mode 100644 index 0000000000..0d828c5bb0 --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-mysql/src/test/kotlin/com/foo/spring/rest/mysql/sqli/SQLiMySQLBodyController.kt @@ -0,0 +1,12 @@ +package com.foo.spring.rest.mysql.sqli + +import com.foo.spring.rest.mysql.SpringRestMySqlController +import com.foo.spring.rest.mysql.sqli.body.BodySQLiApplication +import org.evomaster.client.java.sql.DbSpecification + +class SQLiMySQLBodyController : SpringRestMySqlController(BodySQLiApplication::class.java){ + override fun pathToFlywayFiles() = "classpath:/schema/sqli" + override fun getDbSpecifications(): MutableList? { + return null + } +} diff --git a/core-tests/e2e-tests/spring/spring-rest-mysql/src/test/kotlin/com/foo/spring/rest/mysql/sqli/SQLiMySQLPathController.kt b/core-tests/e2e-tests/spring/spring-rest-mysql/src/test/kotlin/com/foo/spring/rest/mysql/sqli/SQLiMySQLPathController.kt new file mode 100644 index 0000000000..8cfa41290a --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-mysql/src/test/kotlin/com/foo/spring/rest/mysql/sqli/SQLiMySQLPathController.kt @@ -0,0 +1,12 @@ +package com.foo.spring.rest.mysql.sqli + +import com.foo.spring.rest.mysql.SpringRestMySqlController +import com.foo.spring.rest.mysql.sqli.path.PathSQLiApplication +import org.evomaster.client.java.sql.DbSpecification + +class SQLiMySQLPathController : SpringRestMySqlController(PathSQLiApplication::class.java){ + override fun pathToFlywayFiles() = "classpath:/schema/sqli" + override fun getDbSpecifications(): MutableList? { + return null + } +} diff --git a/core-tests/e2e-tests/spring/spring-rest-mysql/src/test/kotlin/com/foo/spring/rest/mysql/sqli/SQLiMySQLQueryController.kt b/core-tests/e2e-tests/spring/spring-rest-mysql/src/test/kotlin/com/foo/spring/rest/mysql/sqli/SQLiMySQLQueryController.kt new file mode 100644 index 0000000000..b3b30290bb --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-mysql/src/test/kotlin/com/foo/spring/rest/mysql/sqli/SQLiMySQLQueryController.kt @@ -0,0 +1,12 @@ +package com.foo.spring.rest.mysql.sqli + +import com.foo.spring.rest.mysql.SpringRestMySqlController +import com.foo.spring.rest.mysql.sqli.query.QuerySQLiApplication +import org.evomaster.client.java.sql.DbSpecification + +class SQLiMySQLQueryController : SpringRestMySqlController(QuerySQLiApplication::class.java){ + override fun pathToFlywayFiles() = "classpath:/schema/sqli" + override fun getDbSpecifications(): MutableList? { + return null + } +} diff --git a/core-tests/e2e-tests/spring/spring-rest-mysql/src/test/kotlin/org/evomaster/e2etests/spring/rest/sqli/SQLiMySQLBodyEMTest.kt b/core-tests/e2e-tests/spring/spring-rest-mysql/src/test/kotlin/org/evomaster/e2etests/spring/rest/sqli/SQLiMySQLBodyEMTest.kt new file mode 100644 index 0000000000..602543528c --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-mysql/src/test/kotlin/org/evomaster/e2etests/spring/rest/sqli/SQLiMySQLBodyEMTest.kt @@ -0,0 +1,55 @@ +package org.evomaster.e2etests.spring.rest.sqli + +import com.foo.spring.rest.mysql.sqli.SQLiMySQLBodyController +import com.webfuzzing.commons.faults.DefinedFaultCategory +import org.evomaster.core.problem.enterprise.DetectedFaultUtils +import org.evomaster.e2etests.spring.mysql.entity.SpringTestBase +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.Test + +class SQLiMySQLBodyEMTest : SpringTestBase() { + + companion object { + @BeforeAll + @JvmStatic + fun init() { + initClass(SQLiMySQLBodyController()) + } + } + + @Test + fun testRunEM() { + + runTestHandlingFlakyAndCompilation( + "SQLiMySQLBodyEM", + 100 + ) { args -> + setOption(args, "security", "true") + setOption(args, "sqli", "true") + + val solution = initAndRun(args) + assertTrue(solution.individuals.isNotEmpty()) + + val faults = DetectedFaultUtils.getDetectedFaults(solution) + + assertTrue(faults.size == 1) + + val faultCategories = DetectedFaultUtils.getDetectedFaultCategories(solution) + + assertTrue({ DefinedFaultCategory.SQL_INJECTION in faultCategories }) + + assertTrue(faults.any { + it.category == DefinedFaultCategory.SQL_INJECTION + && it.operationId == "POST:/api/sqli/body/vulnerable" + }) + + assertFalse(faults.any { + it.category == DefinedFaultCategory.SQL_INJECTION + && it.operationId == "GET:/api/sqli/body/safe" + }) + + } + } +} diff --git a/core-tests/e2e-tests/spring/spring-rest-mysql/src/test/kotlin/org/evomaster/e2etests/spring/rest/sqli/SQLiMySQLPathEMTest.kt b/core-tests/e2e-tests/spring/spring-rest-mysql/src/test/kotlin/org/evomaster/e2etests/spring/rest/sqli/SQLiMySQLPathEMTest.kt new file mode 100644 index 0000000000..806f631807 --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-mysql/src/test/kotlin/org/evomaster/e2etests/spring/rest/sqli/SQLiMySQLPathEMTest.kt @@ -0,0 +1,53 @@ +package org.evomaster.e2etests.spring.rest.sqli + +import com.foo.spring.rest.mysql.sqli.SQLiMySQLPathController +import com.webfuzzing.commons.faults.DefinedFaultCategory +import org.evomaster.core.problem.enterprise.DetectedFaultUtils +import org.evomaster.e2etests.spring.mysql.entity.SpringTestBase +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.Test + +class SQLiMySQLPathEMTest : SpringTestBase() { + + companion object { + @BeforeAll + @JvmStatic + fun init() { + initClass(SQLiMySQLPathController()) + } + } + + @Test + fun testRunEM() { + + runTestHandlingFlakyAndCompilation( + "SQLiMySQLPathEM", + 100 + ) { args -> + setOption(args, "security", "true") + setOption(args, "sqli", "true") + + val solution = initAndRun(args) + assertTrue(solution.individuals.isNotEmpty()) + + val faults = DetectedFaultUtils.getDetectedFaults(solution) + + val faultCategories = DetectedFaultUtils.getDetectedFaultCategories(solution) + + assertTrue({ DefinedFaultCategory.SQL_INJECTION in faultCategories }) + + assertTrue(faults.any { + it.category == DefinedFaultCategory.SQL_INJECTION + && it.operationId == "GET:/api/sqli/path/vulnerable/{id}" + }) + + assertFalse(faults.any { + it.category == DefinedFaultCategory.SQL_INJECTION + && it.operationId == "GET:/api/sqli/path/safe" + }) + + } + } +} diff --git a/core-tests/e2e-tests/spring/spring-rest-mysql/src/test/kotlin/org/evomaster/e2etests/spring/rest/sqli/SQLiMySQLQueryEMTest.kt b/core-tests/e2e-tests/spring/spring-rest-mysql/src/test/kotlin/org/evomaster/e2etests/spring/rest/sqli/SQLiMySQLQueryEMTest.kt new file mode 100644 index 0000000000..be34064f50 --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-mysql/src/test/kotlin/org/evomaster/e2etests/spring/rest/sqli/SQLiMySQLQueryEMTest.kt @@ -0,0 +1,55 @@ +package org.evomaster.e2etests.spring.rest.sqli + +import com.foo.spring.rest.mysql.sqli.SQLiMySQLQueryController +import com.webfuzzing.commons.faults.DefinedFaultCategory +import org.evomaster.core.problem.enterprise.DetectedFaultUtils +import org.evomaster.e2etests.spring.mysql.entity.SpringTestBase +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.Test + +class SQLiMySQLQueryEMTest : SpringTestBase() { + + companion object { + @BeforeAll + @JvmStatic + fun init() { + initClass(SQLiMySQLQueryController()) + } + } + + @Test + fun testRunEM() { + + runTestHandlingFlakyAndCompilation( + "SQLiMySQLQueryEM", + 100 + ) { args -> + setOption(args, "security", "true") + setOption(args, "sqli", "true") + + val solution = initAndRun(args) + assertTrue(solution.individuals.isNotEmpty()) + + val faults = DetectedFaultUtils.getDetectedFaults(solution) + + assertTrue(faults.size == 1) + + val faultCategories = DetectedFaultUtils.getDetectedFaultCategories(solution) + + assertTrue({ DefinedFaultCategory.SQL_INJECTION in faultCategories }) + + assertTrue(faults.any { + it.category == DefinedFaultCategory.SQL_INJECTION + && it.operationId == "GET:/api/sqli/query/vulnerable" + }) + + assertFalse(faults.any { + it.category == DefinedFaultCategory.SQL_INJECTION + && it.operationId == "GET:/api/sqli/query/safe" + }) + + } + } +} diff --git a/core-tests/e2e-tests/spring/spring-rest-openapi-v3/pom.xml b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/pom.xml index 30f48eb10e..a8c998a09c 100644 --- a/core-tests/e2e-tests/spring/spring-rest-openapi-v3/pom.xml +++ b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/pom.xml @@ -149,7 +149,6 @@ com.ethlo.time itu - @@ -165,4 +164,4 @@ - \ No newline at end of file + diff --git a/core-tests/e2e-tests/spring/spring-rest-postgres/src/main/kotlin/com/foo/spring/rest/postgres/sqli/body/BodySQLiApplication.kt b/core-tests/e2e-tests/spring/spring-rest-postgres/src/main/kotlin/com/foo/spring/rest/postgres/sqli/body/BodySQLiApplication.kt new file mode 100644 index 0000000000..7848c2a7ce --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-postgres/src/main/kotlin/com/foo/spring/rest/postgres/sqli/body/BodySQLiApplication.kt @@ -0,0 +1,100 @@ +package com.foo.spring.rest.postgres.sqli.body + +import com.foo.spring.rest.postgres.SwaggerConfiguration +import io.swagger.v3.oas.annotations.Operation +import io.swagger.v3.oas.annotations.media.ArraySchema +import io.swagger.v3.oas.annotations.media.Content +import io.swagger.v3.oas.annotations.media.Schema +import io.swagger.v3.oas.annotations.parameters.RequestBody +import io.swagger.v3.oas.annotations.responses.ApiResponse +import io.swagger.v3.oas.annotations.responses.ApiResponses +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.SpringApplication +import org.springframework.boot.autoconfigure.SpringBootApplication +import org.springframework.boot.autoconfigure.domain.EntityScan +import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration +import org.springframework.context.annotation.ComponentScan +import org.springframework.data.jpa.repository.config.EnableJpaRepositories +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.* +import springfox.documentation.swagger2.annotations.EnableSwagger2 +import java.sql.Connection +import javax.annotation.PostConstruct +import javax.sql.DataSource + + +@EnableSwagger2 +@SpringBootApplication(exclude = [SecurityAutoConfiguration::class]) +@RequestMapping(path = ["/api/sqli/body"]) +@RestController +open class BodySQLiApplication: SwaggerConfiguration() { + + @Autowired + private lateinit var dataSource: DataSource + + @Autowired + private lateinit var userRepository: UserRepository + + private var connection: Connection? = null + + companion object { + @JvmStatic + fun main(args: Array) { + SpringApplication.run(BodySQLiApplication::class.java, *args) + } + } + + @PostConstruct + fun init() { + connection = dataSource.connection + initializeTestData() + } + + private fun initializeTestData() { + if (userRepository.count() == 0L) { + userRepository.save(UserEntity(null, "admin", "admin123")) + userRepository.save(UserEntity(null, "user1", "password1")) + } + } + + /** + * Safe endpoint - No SQL Injection vulnerability + */ + @GetMapping("/safe") + @Operation(summary = "Safe Query - No SQL Injection") + @ApiResponses(value = [ + ApiResponse(responseCode = "200", description = "Successful operation", + content = [Content(mediaType = "application/json", + array = ArraySchema(schema = Schema(implementation = UserDto::class)))]) + ]) + fun getSafeUsers(): ResponseEntity> { + val users = userRepository.findAll().map { UserDto(it.id, it.username) } + return ResponseEntity.ok(users) + } + + /** + * Attack: POST {"username":"admin' AND (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS A, INFORMATION_SCHEMA.COLUMNS B, INFORMATION_SCHEMA.COLUMNS C)>0--","password":"x"} + */ + @PostMapping("/vulnerable") + @Operation(summary = "SQL Injection - Body Parameter") + @ApiResponses(value = [ + ApiResponse(responseCode = "200", description = "Successful operation", + content = [Content(mediaType = "text/plain", schema = Schema(implementation = String::class))]), + ApiResponse(responseCode = "500", description = "Internal server error", + content = [Content(mediaType = "text/plain", schema = Schema(implementation = String::class))]) + ]) + @RequestBody(description = "Login credentials", required = true, + content = [Content(mediaType = "application/json", + schema = Schema(implementation = LoginDto::class))]) + fun body(@org.springframework.web.bind.annotation.RequestBody loginDto: LoginDto): ResponseEntity { + return try { + val stmt = connection?.createStatement() + val rs = stmt?.executeQuery("SELECT COUNT(*) as cnt FROM users WHERE username = '${loginDto.username}' AND password = '${loginDto.password}'") + val count = if (rs?.next() == true) rs.getInt("cnt") else 0 + ResponseEntity.ok("MATCHED: $count") + } catch (e: Exception) { + ResponseEntity.status(500).body("ERROR: ${e.message}") + } + } + +} diff --git a/core-tests/e2e-tests/spring/spring-rest-postgres/src/main/kotlin/com/foo/spring/rest/postgres/sqli/body/UserDto.kt b/core-tests/e2e-tests/spring/spring-rest-postgres/src/main/kotlin/com/foo/spring/rest/postgres/sqli/body/UserDto.kt new file mode 100644 index 0000000000..cd673e477f --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-postgres/src/main/kotlin/com/foo/spring/rest/postgres/sqli/body/UserDto.kt @@ -0,0 +1,13 @@ +package com.foo.spring.rest.postgres.sqli.body + +import com.fasterxml.jackson.annotation.JsonProperty + +data class UserDto( + @JsonProperty("id") var id: Long? = null, + @JsonProperty("username") var username: String? = null, +) + +data class LoginDto( + @JsonProperty("username") var username: String = "", + @JsonProperty("password") var password: String = "" +) diff --git a/core-tests/e2e-tests/spring/spring-rest-postgres/src/main/kotlin/com/foo/spring/rest/postgres/sqli/body/UserEntity.kt b/core-tests/e2e-tests/spring/spring-rest-postgres/src/main/kotlin/com/foo/spring/rest/postgres/sqli/body/UserEntity.kt new file mode 100644 index 0000000000..57ab29162d --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-postgres/src/main/kotlin/com/foo/spring/rest/postgres/sqli/body/UserEntity.kt @@ -0,0 +1,19 @@ +package com.foo.spring.rest.postgres.sqli.body + +import javax.persistence.* + + +@Entity +@Table(name = "users") +open class UserEntity( + + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + open var id: Long? = null, + + @Column(name = "username", unique = true, nullable = false) + open var username: String? = null, + + @Column(name = "password", nullable = false) + open var password: String? = null, +) diff --git a/core-tests/e2e-tests/spring/spring-rest-postgres/src/main/kotlin/com/foo/spring/rest/postgres/sqli/body/UserRepository.kt b/core-tests/e2e-tests/spring/spring-rest-postgres/src/main/kotlin/com/foo/spring/rest/postgres/sqli/body/UserRepository.kt new file mode 100644 index 0000000000..597efa2ff0 --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-postgres/src/main/kotlin/com/foo/spring/rest/postgres/sqli/body/UserRepository.kt @@ -0,0 +1,10 @@ +package com.foo.spring.rest.postgres.sqli.body + +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.stereotype.Repository + + +@Repository +interface UserRepository : JpaRepository { + +} diff --git a/core-tests/e2e-tests/spring/spring-rest-postgres/src/main/kotlin/com/foo/spring/rest/postgres/sqli/path/PathSQLiApplication.kt b/core-tests/e2e-tests/spring/spring-rest-postgres/src/main/kotlin/com/foo/spring/rest/postgres/sqli/path/PathSQLiApplication.kt new file mode 100644 index 0000000000..b82f5ac4ba --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-postgres/src/main/kotlin/com/foo/spring/rest/postgres/sqli/path/PathSQLiApplication.kt @@ -0,0 +1,78 @@ +package com.foo.spring.rest.postgres.sqli.path + + +import com.foo.spring.rest.postgres.SwaggerConfiguration +import io.swagger.v3.oas.annotations.Operation +import org.springframework.beans.factory.annotation.Autowired +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 springfox.documentation.swagger2.annotations.EnableSwagger2 +import java.sql.Connection +import javax.annotation.PostConstruct +import javax.sql.DataSource + + +@EnableSwagger2 +@SpringBootApplication(exclude = [SecurityAutoConfiguration::class]) +@RequestMapping(path = ["/api/sqli/path"]) +@RestController +open class PathSQLiApplication: SwaggerConfiguration() { + + @Autowired + private lateinit var dataSource: DataSource + + @Autowired + private lateinit var userRepository: UserRepository + + private var connection: Connection? = null + + companion object { + @JvmStatic + fun main(args: Array) { + SpringApplication.run(PathSQLiApplication::class.java, *args) + } + } + + @PostConstruct + fun init() { + connection = dataSource.connection + initializeTestData() + } + + private fun initializeTestData() { + if (userRepository.count() == 0L) { + userRepository.save(UserEntity(null, "admin", "admin123")) + userRepository.save(UserEntity(null, "user1", "password1")) + } + } + + /** + * Safe endpoint - No SQL Injection vulnerability + */ + @GetMapping("/safe") + @Operation(summary = "Safe Query - No SQL Injection") + fun getSafeUsers(): ResponseEntity> { + val users = userRepository.findAll().map { UserDto(it.id, it.username) } + return ResponseEntity.ok(users) + } + + /** + * Attack: GET /api/sqli/path/vulnerable/admin' OR (SELECT SUM(a.ORDINAL_POSITION*b.ORDINAL_POSITION*c.ORDINAL_POSITION) FROM INFORMATION_SCHEMA.COLUMNS a, INFORMATION_SCHEMA.COLUMNS b, INFORMATION_SCHEMA.COLUMNS c)>1 -- + */ + @GetMapping("/vulnerable/{id}") + @Operation(summary = "SQL Injection - Path Parameter") + fun timeBasedPath(@PathVariable id: String): ResponseEntity { + return try { + val stmt = connection?.createStatement() + val rs = stmt?.executeQuery("SELECT username FROM users WHERE username = '$id'") + val username = if (rs?.next() == true) rs.getString("username") else "NOT_FOUND" + ResponseEntity.ok("USERNAME: $username") + } catch (e: Exception) { + ResponseEntity.status(500).body("ERROR: ${e.message}") + } + } + +} diff --git a/core-tests/e2e-tests/spring/spring-rest-postgres/src/main/kotlin/com/foo/spring/rest/postgres/sqli/path/UserDto.kt b/core-tests/e2e-tests/spring/spring-rest-postgres/src/main/kotlin/com/foo/spring/rest/postgres/sqli/path/UserDto.kt new file mode 100644 index 0000000000..b08e138208 --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-postgres/src/main/kotlin/com/foo/spring/rest/postgres/sqli/path/UserDto.kt @@ -0,0 +1,12 @@ +package com.foo.spring.rest.postgres.sqli.path + + +data class UserDto( + var id: Long? = null, + var username: String? = null, +) + +data class LoginDto( + var username: String = "", + var password: String = "" +) diff --git a/core-tests/e2e-tests/spring/spring-rest-postgres/src/main/kotlin/com/foo/spring/rest/postgres/sqli/path/UserEntity.kt b/core-tests/e2e-tests/spring/spring-rest-postgres/src/main/kotlin/com/foo/spring/rest/postgres/sqli/path/UserEntity.kt new file mode 100644 index 0000000000..b975cf0901 --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-postgres/src/main/kotlin/com/foo/spring/rest/postgres/sqli/path/UserEntity.kt @@ -0,0 +1,19 @@ +package com.foo.spring.rest.postgres.sqli.path + +import javax.persistence.* + + +@Entity +@Table(name = "users") +open class UserEntity( + + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + open var id: Long? = null, + + @Column(name = "username", unique = true, nullable = false) + open var username: String? = null, + + @Column(name = "password", nullable = false) + open var password: String? = null, +) diff --git a/core-tests/e2e-tests/spring/spring-rest-postgres/src/main/kotlin/com/foo/spring/rest/postgres/sqli/path/UserRepository.kt b/core-tests/e2e-tests/spring/spring-rest-postgres/src/main/kotlin/com/foo/spring/rest/postgres/sqli/path/UserRepository.kt new file mode 100644 index 0000000000..af6641136e --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-postgres/src/main/kotlin/com/foo/spring/rest/postgres/sqli/path/UserRepository.kt @@ -0,0 +1,10 @@ +package com.foo.spring.rest.postgres.sqli.path + +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.stereotype.Repository + + +@Repository +interface UserRepository : JpaRepository { + +} diff --git a/core-tests/e2e-tests/spring/spring-rest-postgres/src/main/kotlin/com/foo/spring/rest/postgres/sqli/query/QuerySQLiApplication.kt b/core-tests/e2e-tests/spring/spring-rest-postgres/src/main/kotlin/com/foo/spring/rest/postgres/sqli/query/QuerySQLiApplication.kt new file mode 100644 index 0000000000..75a110df1d --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-postgres/src/main/kotlin/com/foo/spring/rest/postgres/sqli/query/QuerySQLiApplication.kt @@ -0,0 +1,76 @@ +package com.foo.spring.rest.postgres.sqli.query + +import com.foo.spring.rest.postgres.SwaggerConfiguration +import io.swagger.v3.oas.annotations.Operation +import org.springframework.beans.factory.annotation.Autowired +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 springfox.documentation.swagger2.annotations.EnableSwagger2 +import java.sql.Connection +import javax.annotation.PostConstruct +import javax.sql.DataSource + + +@EnableSwagger2 +@SpringBootApplication(exclude = [SecurityAutoConfiguration::class]) +@RequestMapping(path = ["/api/sqli/query"]) +open class QuerySQLiApplication: SwaggerConfiguration() { + + @Autowired + private lateinit var dataSource: DataSource + + @Autowired + private lateinit var userRepository: UserRepository + + private var connection: Connection? = null + + companion object { + @JvmStatic + fun main(args: Array) { + SpringApplication.run(QuerySQLiApplication::class.java, *args) + } + } + + @PostConstruct + fun init() { + connection = dataSource.connection + initializeTestData() + } + + private fun initializeTestData() { + if (userRepository.count() == 0L) { + userRepository.save(UserEntity(null, "admin", "admin123")) + userRepository.save(UserEntity(null, "user1", "password1")) + } + } + + /** + * Safe endpoint - No SQL Injection vulnerability + */ + @GetMapping("/safe") + @Operation(summary = "Safe Query - No SQL Injection") + fun getSafeUsers(): ResponseEntity> { + val users = userRepository.findAll().map { UserDto(it.id, it.username) } + return ResponseEntity.ok(users) + } + + /** + * Attack: GET /api/sqli/query/vulnerable?username=admin' OR (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS A, INFORMATION_SCHEMA.COLUMNS B, INFORMATION_SCHEMA.COLUMNS C)>0-- + */ + @GetMapping("/vulnerable") + @Operation(summary = "SQL Injection - Query Parameter") + fun timeBasedQuery(@RequestParam username: String): ResponseEntity { + return try { + val stmt = connection?.createStatement() + val rs = stmt?.executeQuery("SELECT COUNT(*) as cnt FROM users WHERE username = '$username'") + val count = if (rs?.next() == true) rs.getInt("cnt") else 0 + ResponseEntity.ok("COUNT: $count") + } catch (e: Exception) { + ResponseEntity.status(500).body("ERROR: ${e.message}") + } + } + +} diff --git a/core-tests/e2e-tests/spring/spring-rest-postgres/src/main/kotlin/com/foo/spring/rest/postgres/sqli/query/UserDto.kt b/core-tests/e2e-tests/spring/spring-rest-postgres/src/main/kotlin/com/foo/spring/rest/postgres/sqli/query/UserDto.kt new file mode 100644 index 0000000000..dd353a82bf --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-postgres/src/main/kotlin/com/foo/spring/rest/postgres/sqli/query/UserDto.kt @@ -0,0 +1,12 @@ +package com.foo.spring.rest.postgres.sqli.query + + +data class UserDto( + var id: Long? = null, + var username: String? = null, +) + +data class LoginDto( + var username: String, + var password: String +) diff --git a/core-tests/e2e-tests/spring/spring-rest-postgres/src/main/kotlin/com/foo/spring/rest/postgres/sqli/query/UserEntity.kt b/core-tests/e2e-tests/spring/spring-rest-postgres/src/main/kotlin/com/foo/spring/rest/postgres/sqli/query/UserEntity.kt new file mode 100644 index 0000000000..2cca768353 --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-postgres/src/main/kotlin/com/foo/spring/rest/postgres/sqli/query/UserEntity.kt @@ -0,0 +1,19 @@ +package com.foo.spring.rest.postgres.sqli.query + +import javax.persistence.* + + +@Entity +@Table(name = "users") +open class UserEntity( + + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + open var id: Long? = null, + + @Column(name = "username", unique = true, nullable = false) + open var username: String? = null, + + @Column(name = "password", nullable = false) + open var password: String? = null, +) diff --git a/core-tests/e2e-tests/spring/spring-rest-postgres/src/main/kotlin/com/foo/spring/rest/postgres/sqli/query/UserRepository.kt b/core-tests/e2e-tests/spring/spring-rest-postgres/src/main/kotlin/com/foo/spring/rest/postgres/sqli/query/UserRepository.kt new file mode 100644 index 0000000000..c9f3afd349 --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-postgres/src/main/kotlin/com/foo/spring/rest/postgres/sqli/query/UserRepository.kt @@ -0,0 +1,10 @@ +package com.foo.spring.rest.postgres.sqli.query + +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.stereotype.Repository + + +@Repository +interface UserRepository : JpaRepository { + +} diff --git a/core-tests/e2e-tests/spring/spring-rest-postgres/src/main/resources/schema/sqli/V1.0__createDB.sql b/core-tests/e2e-tests/spring/spring-rest-postgres/src/main/resources/schema/sqli/V1.0__createDB.sql new file mode 100644 index 0000000000..87abc561e2 --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-postgres/src/main/resources/schema/sqli/V1.0__createDB.sql @@ -0,0 +1,7 @@ +CREATE TABLE users ( + id BIGSERIAL PRIMARY KEY, + username VARCHAR(255) NOT NULL UNIQUE, + password VARCHAR(255) NOT NULL +); + +CREATE SEQUENCE hibernate_sequence START WITH 1 INCREMENT BY 1; diff --git a/core-tests/e2e-tests/spring/spring-rest-postgres/src/test/kotlin/com/foo/spring/rest/postgres/sqli/SQLiPostgresBodyController.kt b/core-tests/e2e-tests/spring/spring-rest-postgres/src/test/kotlin/com/foo/spring/rest/postgres/sqli/SQLiPostgresBodyController.kt new file mode 100644 index 0000000000..54ebc9f929 --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-postgres/src/test/kotlin/com/foo/spring/rest/postgres/sqli/SQLiPostgresBodyController.kt @@ -0,0 +1,12 @@ +package com.foo.spring.rest.postgres.sqli + +import com.foo.spring.rest.postgres.SpringRestPostgresController +import com.foo.spring.rest.postgres.sqli.body.BodySQLiApplication +import org.evomaster.client.java.sql.DbSpecification + +class SQLiPostgresBodyController : SpringRestPostgresController(BodySQLiApplication::class.java){ + override fun pathToFlywayFiles() = "classpath:/schema/sqli" + override fun getDbSpecifications(): MutableList? { + return null + } +} diff --git a/core-tests/e2e-tests/spring/spring-rest-postgres/src/test/kotlin/com/foo/spring/rest/postgres/sqli/SQLiPostgresPathController.kt b/core-tests/e2e-tests/spring/spring-rest-postgres/src/test/kotlin/com/foo/spring/rest/postgres/sqli/SQLiPostgresPathController.kt new file mode 100644 index 0000000000..05b6f1c98b --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-postgres/src/test/kotlin/com/foo/spring/rest/postgres/sqli/SQLiPostgresPathController.kt @@ -0,0 +1,12 @@ +package com.foo.spring.rest.postgres.sqli + +import com.foo.spring.rest.postgres.SpringRestPostgresController +import com.foo.spring.rest.postgres.sqli.path.PathSQLiApplication +import org.evomaster.client.java.sql.DbSpecification + +class SQLiPostgresPathController : SpringRestPostgresController(PathSQLiApplication::class.java){ + override fun pathToFlywayFiles() = "classpath:/schema/sqli" + override fun getDbSpecifications(): MutableList? { + return null + } +} diff --git a/core-tests/e2e-tests/spring/spring-rest-postgres/src/test/kotlin/com/foo/spring/rest/postgres/sqli/SQLiPostgresQueryController.kt b/core-tests/e2e-tests/spring/spring-rest-postgres/src/test/kotlin/com/foo/spring/rest/postgres/sqli/SQLiPostgresQueryController.kt new file mode 100644 index 0000000000..5ff8128cc4 --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-postgres/src/test/kotlin/com/foo/spring/rest/postgres/sqli/SQLiPostgresQueryController.kt @@ -0,0 +1,12 @@ +package com.foo.spring.rest.postgres.sqli + +import com.foo.spring.rest.postgres.SpringRestPostgresController +import com.foo.spring.rest.postgres.sqli.query.QuerySQLiApplication +import org.evomaster.client.java.sql.DbSpecification + +class SQLiPostgresQueryController : SpringRestPostgresController(QuerySQLiApplication::class.java){ + override fun pathToFlywayFiles() = "classpath:/schema/sqli" + override fun getDbSpecifications(): MutableList? { + return null + } +} diff --git a/core-tests/e2e-tests/spring/spring-rest-postgres/src/test/kotlin/org/evomaster/e2etests/spring/rest/postgres/sqli/SQLiPostgresBodyEMTest.kt b/core-tests/e2e-tests/spring/spring-rest-postgres/src/test/kotlin/org/evomaster/e2etests/spring/rest/postgres/sqli/SQLiPostgresBodyEMTest.kt new file mode 100644 index 0000000000..470bf6a18c --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-postgres/src/test/kotlin/org/evomaster/e2etests/spring/rest/postgres/sqli/SQLiPostgresBodyEMTest.kt @@ -0,0 +1,55 @@ +package org.evomaster.e2etests.spring.rest.postgres.sqli + +import com.foo.spring.rest.postgres.sqli.SQLiPostgresBodyController +import com.webfuzzing.commons.faults.DefinedFaultCategory +import org.evomaster.core.problem.enterprise.DetectedFaultUtils +import org.evomaster.e2etests.spring.rest.postgres.SpringRestPostgresTestBase +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.Test + +class SQLiPostgresBodyEMTest : SpringRestPostgresTestBase() { + + companion object { + @BeforeAll + @JvmStatic + fun init() { + initClass(SQLiPostgresBodyController()) + } + } + + @Test + fun testRunEM() { + + runTestHandlingFlakyAndCompilation( + "SQLiPostgresBodyEM", + 100 + ) { args -> + setOption(args, "security", "true") + setOption(args, "sqli", "true") + + val solution = initAndRun(args) + assertTrue(solution.individuals.isNotEmpty()) + + val faults = DetectedFaultUtils.getDetectedFaults(solution) + + assertTrue(faults.size == 1) + + val faultCategories = DetectedFaultUtils.getDetectedFaultCategories(solution) + + assertTrue({ DefinedFaultCategory.SQL_INJECTION in faultCategories }) + + assertTrue(faults.any { + it.category == DefinedFaultCategory.SQL_INJECTION + && it.operationId == "POST:/api/sqli/body/vulnerable" + }) + + assertFalse(faults.any { + it.category == DefinedFaultCategory.SQL_INJECTION + && it.operationId == "GET:/api/sqli/body/safe" + }) + + } + } +} diff --git a/core-tests/e2e-tests/spring/spring-rest-postgres/src/test/kotlin/org/evomaster/e2etests/spring/rest/postgres/sqli/SQLiPostgresPathEMTest.kt b/core-tests/e2e-tests/spring/spring-rest-postgres/src/test/kotlin/org/evomaster/e2etests/spring/rest/postgres/sqli/SQLiPostgresPathEMTest.kt new file mode 100644 index 0000000000..12b8bde0fc --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-postgres/src/test/kotlin/org/evomaster/e2etests/spring/rest/postgres/sqli/SQLiPostgresPathEMTest.kt @@ -0,0 +1,53 @@ +package org.evomaster.e2etests.spring.rest.postgres.sqli + +import com.foo.spring.rest.postgres.sqli.SQLiPostgresPathController +import com.webfuzzing.commons.faults.DefinedFaultCategory +import org.evomaster.core.problem.enterprise.DetectedFaultUtils +import org.evomaster.e2etests.spring.rest.postgres.SpringRestPostgresTestBase +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.Test + +class SQLiPostgresPathEMTest : SpringRestPostgresTestBase() { + + companion object { + @BeforeAll + @JvmStatic + fun init() { + initClass(SQLiPostgresPathController()) + } + } + + @Test + fun testRunEM() { + + runTestHandlingFlakyAndCompilation( + "SQLiPostgresPathEM", + 100 + ) { args -> + setOption(args, "security", "true") + setOption(args, "sqli", "true") + + val solution = initAndRun(args) + assertTrue(solution.individuals.isNotEmpty()) + + val faults = DetectedFaultUtils.getDetectedFaults(solution) + + val faultCategories = DetectedFaultUtils.getDetectedFaultCategories(solution) + + assertTrue({ DefinedFaultCategory.SQL_INJECTION in faultCategories }) + + assertTrue(faults.any { + it.category == DefinedFaultCategory.SQL_INJECTION + && it.operationId == "GET:/api/sqli/path/vulnerable/{id}" + }) + + assertFalse(faults.any { + it.category == DefinedFaultCategory.SQL_INJECTION + && it.operationId == "GET:/api/sqli/path/safe" + }) + + } + } +} diff --git a/core-tests/e2e-tests/spring/spring-rest-postgres/src/test/kotlin/org/evomaster/e2etests/spring/rest/postgres/sqli/SQLiPostgresQueryEMTest.kt b/core-tests/e2e-tests/spring/spring-rest-postgres/src/test/kotlin/org/evomaster/e2etests/spring/rest/postgres/sqli/SQLiPostgresQueryEMTest.kt new file mode 100644 index 0000000000..1a14f0ac0d --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-postgres/src/test/kotlin/org/evomaster/e2etests/spring/rest/postgres/sqli/SQLiPostgresQueryEMTest.kt @@ -0,0 +1,55 @@ +package org.evomaster.e2etests.spring.rest.postgres.sqli + +import com.foo.spring.rest.postgres.sqli.SQLiPostgresQueryController +import com.webfuzzing.commons.faults.DefinedFaultCategory +import org.evomaster.core.problem.enterprise.DetectedFaultUtils +import org.evomaster.e2etests.spring.rest.postgres.SpringRestPostgresTestBase +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.Test + +class SQLiPostgresQueryEMTest : SpringRestPostgresTestBase() { + + companion object { + @BeforeAll + @JvmStatic + fun init() { + initClass(SQLiPostgresQueryController()) + } + } + + @Test + fun testRunEM() { + + runTestHandlingFlakyAndCompilation( + "SQLiPostgresQueryEM", + 100 + ) { args -> + setOption(args, "security", "true") + setOption(args, "sqli", "true") + + val solution = initAndRun(args) + assertTrue(solution.individuals.isNotEmpty()) + + val faults = DetectedFaultUtils.getDetectedFaults(solution) + + assertTrue(faults.size == 1) + + val faultCategories = DetectedFaultUtils.getDetectedFaultCategories(solution) + + assertTrue({ DefinedFaultCategory.SQL_INJECTION in faultCategories }) + + assertTrue(faults.any { + it.category == DefinedFaultCategory.SQL_INJECTION + && it.operationId == "GET:/api/sqli/query/vulnerable" + }) + + assertFalse(faults.any { + it.category == DefinedFaultCategory.SQL_INJECTION + && it.operationId == "GET:/api/sqli/query/safe" + }) + + } + } +} diff --git a/core/src/main/kotlin/org/evomaster/core/BaseModule.kt b/core/src/main/kotlin/org/evomaster/core/BaseModule.kt index be237f4c9f..3e503eedde 100644 --- a/core/src/main/kotlin/org/evomaster/core/BaseModule.kt +++ b/core/src/main/kotlin/org/evomaster/core/BaseModule.kt @@ -89,6 +89,9 @@ class BaseModule(val args: Array, val noTests: Boolean = false) : Abstra bind(SMTLibZ3DbConstraintSolver::class.java) .asEagerSingleton() + bind(ExecutionStats::class.java) + .asEagerSingleton() + //no longer needed if TestSuiteWriter is moved out? // if(noTests){ // bind(TestCaseWriter::class.java) diff --git a/core/src/main/kotlin/org/evomaster/core/EMConfig.kt b/core/src/main/kotlin/org/evomaster/core/EMConfig.kt index 03d11140e3..56388e6abe 100644 --- a/core/src/main/kotlin/org/evomaster/core/EMConfig.kt +++ b/core/src/main/kotlin/org/evomaster/core/EMConfig.kt @@ -2605,6 +2605,19 @@ class EMConfig { @Cfg("To apply XSS detection as part of security testing.") var xss = false + @Experimental + @Cfg("To apply SQLi detection as part of security testing.") + var sqli = false + + @Experimental + @Cfg("Injected sleep duration (in seconds) used inside the malicious payload to detect time-based vulnerabilities.") + var sqliInjectedSleepDurationMs = 5500 + + @Experimental + @Cfg("Maximum allowed baseline response time (in milliseconds) before the malicious payload is applied.") + var sqliBaselineMaxResponseTimeMs = 2000 + + @Regex(faultCodeRegex) @Cfg("Disable oracles. Provide a comma-separated list of codes to disable. " + "By default, all oracles are enabled." @@ -2947,6 +2960,14 @@ class EMConfig { * Some might be experimental, while others might be explicitly excluded by the user */ fun isEnabledFaultCategory(category: FaultCategory) : Boolean{ + if(category == DefinedFaultCategory.XSS && !xss){ + return false; + } + + if(category == DefinedFaultCategory.SQL_INJECTION && !sqli){ + return false; + } + return category !in getDisabledOracleCodesList() } diff --git a/core/src/main/kotlin/org/evomaster/core/problem/httpws/HttpWsCallResult.kt b/core/src/main/kotlin/org/evomaster/core/problem/httpws/HttpWsCallResult.kt index f2b0635b17..04ad328f62 100644 --- a/core/src/main/kotlin/org/evomaster/core/problem/httpws/HttpWsCallResult.kt +++ b/core/src/main/kotlin/org/evomaster/core/problem/httpws/HttpWsCallResult.kt @@ -25,6 +25,7 @@ abstract class HttpWsCallResult : EnterpriseActionResult { const val TCP_PROBLEM = "TCP_PROBLEM" const val APPLIED_LINK = "APPLIED_LINK" const val LOCATION = "LOCATION" + const val RESPONSE_TIME_MS = "RESPONSE_TIME_MS" const val VULNERABLE_SSRF = "VULNERABLE_SSRF" } @@ -126,4 +127,7 @@ abstract class HttpWsCallResult : EnterpriseActionResult { */ fun setVulnerableForSSRF(on: Boolean) = addResultValue(VULNERABLE_SSRF, on.toString()) fun getVulnerableForSSRF() : Boolean = getResultValue(VULNERABLE_SSRF)?.toBoolean() ?: false + + fun setResponseTimeMs(responseTime: Long) = addResultValue(RESPONSE_TIME_MS, responseTime.toString()) + fun getResponseTimeMs(): Long? = getResultValue(RESPONSE_TIME_MS)?.toLong() } 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 a01730572d..c889d67ecf 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 @@ -434,4 +434,4 @@ class RestCallAction( this.weakReference = wr return copy } -} \ No newline at end of file +} 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 73426d15f3..1a947c667a 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 @@ -222,7 +222,45 @@ object RestSecurityOracle { return true } - + fun hasSQLiPayload(action: RestCallAction, duration: Double): Boolean { + val allValues = action.seeTopGenes() + .map { it.getValueAsRawString() } + .joinToString(" ") + + return SQLI_PAYLOADS.any { payload -> + allValues.contains(String.format(payload, duration), ignoreCase = true) + } + } + + /** + * Simple SQLi payloads. Used to check for SQL Injection vulnerability. + * The payloads are designed to introduce delays in the database response, + * which can be detected by measuring the response time of the application. + */ + val SQLI_PAYLOADS = listOf( + // Simple sleep-based payloads for MySQL + "' OR SLEEP(%.2f)-- -", + "\" OR SLEEP(%.2f)-- -", + "' OR SLEEP(%.2f)=0-- -", + "\" OR SLEEP(%.2f)=0-- -", + // Integer-based delays + "' OR SLEEP(%.0f)-- -", + "\" OR SLEEP(%.0f)-- -", + "' OR SLEEP(%.0f)=0-- -", + "\" OR SLEEP(%.0f)=0-- -", + // Simple sleep-based payloads for PostgreSQL + "' OR select pg_sleep(%.2f)-- -", + "\" OR select pg_sleep(%.2f)-- -", + "' OR (select pg_sleep(%.2f)) IS NULL-- -", + "\' OR (select pg_sleep(%.2f)) IS NULL-- -", + // Integer-based delays + "' OR select pg_sleep(%.0f)-- -", + "\" OR select pg_sleep(%.0f)-- -", + "' OR (select pg_sleep(%.0f)) IS NULL-- -", + "\' OR (select pg_sleep(%.0f)) IS NULL-- -", + ) + + // Simple XSS payloads inspired by big-list-of-naughty-strings // https://github.com/minimaxir/big-list-of-naughty-strings/blob/master/blns.txt val XSS_PAYLOADS = listOf( 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 13cdacbbc4..85de2a4831 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 @@ -6,7 +6,6 @@ import org.evomaster.core.EMConfig import javax.annotation.PostConstruct import org.evomaster.core.logging.LoggingUtil -import org.evomaster.core.problem.api.param.Param import org.evomaster.core.problem.enterprise.DetectedFaultUtils import org.evomaster.core.problem.enterprise.ExperimentalFaultCategory import org.evomaster.core.problem.enterprise.SampleType @@ -19,10 +18,9 @@ import org.evomaster.core.problem.rest.* import org.evomaster.core.problem.rest.builder.CreateResourceUtils import org.evomaster.core.problem.rest.builder.RestIndividualSelectorUtils import org.evomaster.core.problem.rest.data.* +import org.evomaster.core.problem.rest.oracle.RestSecurityOracle.SQLI_PAYLOADS import org.evomaster.core.problem.rest.oracle.RestSecurityOracle.XSS_PAYLOADS -import org.evomaster.core.problem.rest.param.BodyParam import org.evomaster.core.problem.rest.param.PathParam -import org.evomaster.core.problem.rest.param.QueryParam import org.evomaster.core.problem.rest.resource.RestResourceCalls import org.evomaster.core.problem.rest.service.sampler.AbstractRestSampler import org.evomaster.core.search.gene.string.StringGene @@ -263,7 +261,7 @@ class SecurityRest { private fun accessControlBasedOnRESTGuidelines() { if(!config.isEnabledFaultCategory(DefinedFaultCategory.SECURITY_WRONG_AUTHORIZATION)){ - LoggingUtil.uniqueUserInfo("Skipping security test for forbidden but ok others as disabled in configuration") + log.debug("Skipping security test for forbidden but ok others as disabled in configuration") } else { // quite a few rules here that can be defined handleForbiddenOperationButOKOthers(HttpVerb.DELETE) @@ -272,41 +270,144 @@ class SecurityRest { } if(!config.isEnabledFaultCategory(DefinedFaultCategory.SECURITY_EXISTENCE_LEAKAGE)){ - LoggingUtil.uniqueUserInfo("Skipping security test for existence leakage as disabled in configuration") + log.debug("Skipping security test for existence leakage as disabled in configuration") } else { // getting 404 instead of 403 handleExistenceLeakage() } if(!config.isEnabledFaultCategory(DefinedFaultCategory.SECURITY_NOT_RECOGNIZED_AUTHENTICATED)){ - LoggingUtil.uniqueUserInfo("Skipping security test for not recognized authenticated as disabled in configuration") + log.debug("Skipping security test for not recognized authenticated as disabled in configuration") } else { //authenticated, but wrongly getting 401 (eg instead of 403) handleNotRecognizedAuthenticated() } - if (!config.xss || !config.isEnabledFaultCategory(DefinedFaultCategory.XSS)) { - LoggingUtil.uniqueUserInfo("Skipping security test for XSS as disabled in configuration") + if (!config.isEnabledFaultCategory(DefinedFaultCategory.XSS)) { + log.debug("Skipping security test for XSS as disabled in configuration") } else { handleXSSCheck() } if(!config.isEnabledFaultCategory(ExperimentalFaultCategory.SECURITY_FORGOTTEN_AUTHENTICATION)) { - LoggingUtil.uniqueUserInfo("Skipping experimental security test for forgotten authentication as disabled in configuration") + log.debug("Skipping experimental security test for forgotten authentication as disabled in configuration") } else { handleForgottenAuthentication() } if (!config.isEnabledFaultCategory(ExperimentalFaultCategory.SECURITY_STACK_TRACE)) { - LoggingUtil.uniqueUserInfo("Skipping experimental security test for stack traces as disabled in configuration") + log.debug("Skipping experimental security test for stack traces as disabled in configuration") } else { handleStackTraceCheck() } + if (!config.isEnabledFaultCategory(DefinedFaultCategory.SQL_INJECTION)) { + log.debug("Skipping experimental security test for sql injection as disabled in configuration") + } else { + handleSqlICheck() + } + //TODO other rules. See FaultCategory //etc. } + + private fun handleSqlICheck(){ + + mainloop@ for(action in actionDefinitions){ + + + // Find individuals with 2xx response for this endpoint + val successfulIndividuals = RestIndividualSelectorUtils.findIndividuals( + individualsInSolution, + action.verb, + action.path, + statusGroup = StatusGroup.G_2xx + ) + + if(successfulIndividuals.isEmpty()){ + continue + } + + // Take the smallest successful individual + val target = successfulIndividuals.minBy { it.individual.size() } + + val actionIndex = RestIndividualSelectorUtils.findIndexOfAction( + target, + action.verb, + action.path, + statusGroup = StatusGroup.G_2xx + ) + + if(actionIndex < 0){ + continue + } + + // Slice to keep only up to the target action + val sliced = RestIndividualBuilder.sliceAllCallsInIndividualAfterAction( + target.individual, + actionIndex + ) + + // Try each sqli payload (but only add one test per endpoint) + for(payload in SQLI_PAYLOADS){ + + // Create a copy of the individual + var copy = sliced.copy() as RestIndividual + val actionCopy = copy.seeMainExecutableActions().last() as RestCallAction + + val genes = GeneUtils.getAllStringFields(actionCopy.parameters) + .filter { it.staticCheckIfImpactPhenotype() } + + if(genes.isEmpty()){ + continue + } + var anySuccess = false + + genes.forEach { + gene -> + val leafGene = gene.getLeafGene() + if(leafGene !is StringGene) return@forEach + + //TODO check if gene is linked with previous actions that create resources with IDs + + + // 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 + leafGene.getPhenotype().setFromStringValue(newPayload).also { + if(it) anySuccess = true + } + } + + if(!anySuccess){ + continue + } + + val newInd = RestIndividualBuilder.merge(sliced, copy) + + newInd.modifySampleType(SampleType.SECURITY) + newInd.ensureFlattenedStructure() + + val evaluatedIndividual = fitness.computeWholeAchievedCoverageForPostProcessing(newInd) + + if (evaluatedIndividual == null) { + log.warn("Failed to evaluate constructed individual in handleSqlICheck") + continue@mainloop + } + + val faultsCategories = DetectedFaultUtils.getDetectedFaultCategories(evaluatedIndividual) + + if(DefinedFaultCategory.SQL_INJECTION in faultsCategories){ + val added = archive.addIfNeeded(evaluatedIndividual) + assert(added) + continue@mainloop + } + + } + } + } /** * Checks whether any response body contains a stack trace, which would constitute a security issue. * Stack traces expose internal implementation details that can aid attackers in exploiting vulnerabilities. @@ -958,7 +1059,7 @@ class SecurityRest { val evaluatedIndividual = fitness.computeWholeAchievedCoverageForPostProcessing(copy) if (evaluatedIndividual == null) { - log.warn("Failed to evaluate constructed individual in handleStackTraceCheck") + log.warn("Failed to evaluate constructed individual in handleXSSCheck") continue@mainloop } @@ -975,7 +1076,7 @@ class SecurityRest { // XSS-like payloads are injected or when user input is not properly sanitized. The try-catch block ensures // that these exceptions do not interrupt the main processing loop. - log.warn("Failed to evaluate constructed individual in handleStackTraceCheck: ${e.message}") + log.warn("Failed to evaluate constructed individual in handleXSSCheck: ${e.message}") } } } 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 6f30ba213e..32d7847ffb 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 @@ -32,6 +32,7 @@ import org.evomaster.core.problem.rest.oracle.HttpSemanticsOracle import org.evomaster.core.problem.rest.oracle.RestSchemaOracle import org.evomaster.core.problem.rest.oracle.RestSecurityOracle import org.evomaster.core.problem.rest.oracle.RestSecurityOracle.XSS_PAYLOADS +import org.evomaster.core.problem.rest.oracle.RestSecurityOracle.hasSQLiPayload import org.evomaster.core.problem.rest.param.BodyParam import org.evomaster.core.problem.rest.param.HeaderParam import org.evomaster.core.problem.rest.param.QueryParam @@ -59,6 +60,8 @@ import org.evomaster.core.search.gene.wrapper.OptionalGene import org.evomaster.core.search.gene.string.StringGene import org.evomaster.core.search.gene.utils.GeneUtils import org.evomaster.core.search.service.DataPool +import org.evomaster.core.search.service.ExecutionStats +import org.evomaster.core.search.service.SearchTimeController import org.evomaster.core.taint.TaintAnalysis import org.evomaster.core.utils.StackTraceUtils import org.slf4j.Logger @@ -100,6 +103,8 @@ abstract class AbstractRestFitness : HttpWsFitness() { @Inject protected lateinit var callGraphService: CallGraphService + @Inject + protected lateinit var executionStats: ExecutionStats private lateinit var schemaOracle: RestSchemaOracle @@ -595,7 +600,18 @@ abstract class AbstractRestFitness : HttpWsFitness() { val appliedLink = handleLinks(a, all,actionResults) val response = try { - createInvocation(a, chainState, cookies, tokens).invoke() + val call = createInvocation(a, chainState, cookies, tokens) + + SearchTimeController.measureTimeMillis( + { t, res -> + rcr.setResponseTimeMs(t) + executionStats.record(a.id, t) + }, + { + call.invoke() + } + ) + } catch (e: ProcessingException) { log.debug("There has been an issue in the evaluation of a test: ${e.message}", e) @@ -1245,6 +1261,7 @@ abstract class AbstractRestFitness : HttpWsFitness() { handleNotRecognizedAuthenticated(individual, actionResults, fv) handleForgottenAuthentication(individual, actionResults, fv) handleStackTraceCheck(individual, actionResults, fv) + handleSQLiCheck(individual, actionResults, fv) handleXSSCheck(individual, actionResults, fv) } @@ -1346,6 +1363,100 @@ abstract class AbstractRestFitness : HttpWsFitness() { } } + private fun handleSQLiCheck( + individual: RestIndividual, + actionResults: List, + fv: FitnessValue + ) { + if (!config.isEnabledFaultCategory(DefinedFaultCategory.SQL_INJECTION)) { + return + } + + val foundPair = findSQLiPayloadPair(individual) + + if(foundPair == null){ + //no pair found, cannot do baseline comparison + return + } + + val (actionWithoutPayload, actionWithPayload) = foundPair + val baselineResult = actionResults.find { it.sourceLocalId == actionWithoutPayload.getLocalId() } as? RestCallResult + ?: return + val baselineTime = baselineResult.getResponseTimeMs() ?: return + + val injectedResult = actionResults.find { it.sourceLocalId == actionWithPayload.getLocalId() } as? RestCallResult + ?: return + val injectedTime = injectedResult.getResponseTimeMs() ?: return + + val K = config.sqliBaselineMaxResponseTimeMs // K: maximum allowed baseline response time + val N = config.sqliInjectedSleepDurationMs // N: expected delay introduced by the injected sleep payload + + // Baseline must be fast enough (baseline < K) + val baselineIsFast = baselineTime < K + + // Response after injection must be slow enough (response > N) + val responseIsSlowEnough = injectedTime > N + + // If baseline is fast AND the response after payload is slow enough, + // then we consider this a potential time-based SQL injection vulnerability. + // Otherwise, skip this result. + if (!(baselineIsFast && responseIsSlowEnough)) { + return + } + + // Find the index of the action with payload to report it correctly + val index = individual.seeMainExecutableActions().indexOf(actionWithPayload) + if (index < 0) { + log.warn("Failed to find index of action with SQLi payload") + return + } + + val scenarioId = idMapper.handleLocalTarget( + idMapper.getFaultDescriptiveId(DefinedFaultCategory.SQL_INJECTION, actionWithPayload.getName()) + ) + fv.updateTarget(scenarioId, 1.0, index) + injectedResult.addFault(DetectedFault(DefinedFaultCategory.SQL_INJECTION, actionWithPayload.getName(), null)) + } + + /** + * Finds a pair of actions in the individual that have the same path and verb, + * where one contains a SQLi payload and the other does not. + * + * This is useful for comparing baseline response times (without payload) against + * response times with SQLi payload to detect time-based SQL injection vulnerabilities. + * + * @param individual The test individual to search + * @return A pair of (actionWithoutPayload, actionWithPayload), or null if no such pair exists + */ + private fun findSQLiPayloadPair( + individual: RestIndividual + ): Pair? { + + val actions = individual.seeMainExecutableActions() + .filterIsInstance() + + // Group actions by path and verb + val actionsByPathAndVerb = actions + .groupBy { it.path.toString() to it.verb } + + // Find a pair where one has SQLi payload and one doesn't + for ((pathVerb, actionsForPath) in actionsByPathAndVerb) { + if (actionsForPath.size < 2) continue + + val withPayload = actionsForPath.filter { + hasSQLiPayload(it, config.sqliInjectedSleepDurationMs/1000.0) + } + val withoutPayload = actionsForPath.filter { + !hasSQLiPayload(it, config.sqliInjectedSleepDurationMs/1000.0) + } + + if (withPayload.isNotEmpty() && withoutPayload.isNotEmpty()) { + return Pair(withoutPayload.first(), withPayload.first()) + } + } + + return null + } private fun handleStackTraceCheck( individual: RestIndividual, @@ -1375,7 +1486,7 @@ abstract class AbstractRestFitness : HttpWsFitness() { actionResults: List, fv: FitnessValue ) { - if(!config.xss || !config.isEnabledFaultCategory(DefinedFaultCategory.XSS)){ + if(!config.isEnabledFaultCategory(DefinedFaultCategory.XSS)){ return } diff --git a/core/src/main/kotlin/org/evomaster/core/search/service/ExecutionStats.kt b/core/src/main/kotlin/org/evomaster/core/search/service/ExecutionStats.kt new file mode 100644 index 0000000000..f128b5b377 --- /dev/null +++ b/core/src/main/kotlin/org/evomaster/core/search/service/ExecutionStats.kt @@ -0,0 +1,45 @@ +package org.evomaster.core.search.service + +import java.util.concurrent.ConcurrentHashMap + + +/** + * Holds statistics for a single action: + * min, max, sum, count → mean = sum / count + */ +data class ActionStats( + var min: Long = Long.MAX_VALUE, + var max: Long = Long.MIN_VALUE, + var sum: Long = 0, + var count: Long = 0 +) { + fun mean(): Long = if (count == 0L) 0L else sum / count +} + +class ExecutionStats { + private val stats = ConcurrentHashMap() + + /** + * Record an execution time for an action. + */ + @Synchronized + fun record(actionName: String, durationMs: Long) { + val s = stats.computeIfAbsent(actionName) { ActionStats() } + + // update stats + s.min = kotlin.math.min(s.min, durationMs) + s.max = kotlin.math.max(s.max, durationMs) + s.sum += durationMs + s.count += 1 + } + + /** + * Read computed baseline for an action. + */ + fun getStats(actionName: String): ActionStats? = stats[actionName] + + /** + * Clear all stats (e.g. before a new run) + */ + fun reset() = stats.clear() +} diff --git a/docs/options.md b/docs/options.md index 9980693f46..79533a7fd0 100644 --- a/docs/options.md +++ b/docs/options.md @@ -309,6 +309,9 @@ There are 3 types of options: |`seedTestCases`| __Boolean__. Whether to seed EvoMaster with some initial test cases. These test cases will be used and evolved throughout the search process. *Default value*: `false`.| |`seedTestCasesFormat`| __Enum__. Format of the test cases seeded to EvoMaster. *Valid values*: `POSTMAN`. *Default value*: `POSTMAN`.| |`seedTestCasesPath`| __String__. File path where the seeded test cases are located. *Default value*: `postman.postman_collection.json`.| +|`sqli`| __Boolean__. To apply SQLi detection as part of security testing. *Default value*: `false`.| +|`sqliBaselineMaxResponseTimeMs`| __Int__. Maximum allowed baseline response time (in milliseconds) before the malicious payload is applied. *Default value*: `2000`.| +|`sqliInjectedSleepDurationMs`| __Int__. Injected sleep duration (in seconds) used inside the malicious payload to detect time-based vulnerabilities. *Default value*: `5500`.| |`ssrf`| __Boolean__. To apply SSRF detection as part of security testing. *Default value*: `false`.| |`structureMutationProFS`| __Double__. Specify a probability of applying structure mutator during the focused search. *Constraints*: `probability 0.0-1.0`. *Default value*: `0.0`.| |`structureMutationProbStrategy`| __Enum__. Specify a strategy to handle a probability of applying structure mutator during the focused search. *Valid values*: `SPECIFIED, SPECIFIED_FS, DPC_TO_SPECIFIED_BEFORE_FS, DPC_TO_SPECIFIED_AFTER_FS, ADAPTIVE_WITH_IMPACT`. *Default value*: `SPECIFIED`.|