Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
package com.foo.rest.examples.spring.openapi.v3.security.xss.reflected

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.MediaType
import org.springframework.web.bind.annotation.*

data class CommentDto(
val comment: String? = null,
val author: String? = null
)

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

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

// ==== BODY PARAMETER - Comment System ====

@PostMapping(path = ["/comment"], produces = [MediaType.TEXT_HTML_VALUE])
open fun reflectComment(@RequestBody commentDto: CommentDto): String {
// VULNERABLE: Reflects user input without sanitization
val comment = commentDto.comment ?: "No comment"
val author = commentDto.author ?: "Anonymous"

return """
<!DOCTYPE html>
<html>
<head>
<title>Comment Reflected</title>
</head>
<body>
<h2>Comment Received!</h2>
<div class="comment">
<p><strong>Author:</strong> $author</p>
<p><strong>Comment:</strong> $comment</p>
</div>
</body>
</html>
""".trimIndent()
}

// ==== PATH PARAMETER - User Profile System ====

@Operation(
summary = "GET endpoint to display user profile (Reflected XSS with path parameter)",
description = "Displays user profile without sanitization - allows Reflected XSS attacks via path parameter"
)
@ApiResponses(
value = [
ApiResponse(responseCode = "200", description = "User profile displayed"),
ApiResponse(responseCode = "400", description = "Invalid URI with special characters")
]
)
@GetMapping(path = ["/user/{username}"], produces = [MediaType.TEXT_HTML_VALUE])
open fun getUserProfile(@PathVariable username: String): String {
// VULNERABLE: Reflects path parameter without sanitization
return """
<!DOCTYPE html>
<html>
<head>
<title>User Profile</title>
</head>
<body>
<h1>Profile of $username</h1>
<div class="profile-info">
<p><strong>Username:</strong> $username</p>
<p>Welcome to $username's profile page!</p>
</div>
</body>
</html>
""".trimIndent()
}

// ==== QUERY PARAMETER - Search System ====

@GetMapping(path = ["/search"], produces = [MediaType.TEXT_HTML_VALUE])
open fun search(
@RequestParam(name = "query", required = false, defaultValue = "") query: String
): String {
// VULNERABLE: Reflects query parameter without sanitization
return """
<!DOCTYPE html>
<html>
<head>
<title>Search Results</title>
</head>
<body>
<h1>Search Results</h1>
<p>You searched for: <strong>$query</strong></p>
<div class="results">
<p>No results found for "$query"</p>
</div>
</body>
</html>
""".trimIndent()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
package com.foo.rest.examples.spring.openapi.v3.security.xss.stored

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.MediaType
import org.springframework.web.bind.annotation.*

data class CommentDto(
val comment: String? = null,
val author: String? = null
)

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

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

// In-memory storage for stored XSS examples
private val comments = mutableListOf<Pair<String, String>>() // Body parameter
private val userBios = mutableMapOf<String, String>() // Path parameter
private val guestbookEntries = mutableListOf<Pair<String, String>>() // Query parameter
}

// ==== BODY PARAMETER - Comment System ====

@PostMapping(path = ["/comment"], produces = [MediaType.TEXT_HTML_VALUE])
open fun storeComment(@RequestBody commentDto: CommentDto): String {
// VULNERABLE: Stores user input without sanitization
val comment = commentDto.comment ?: "No comment"
val author = commentDto.author ?: "Anonymous"

comments.add(Pair(author, comment))

return """
<!DOCTYPE html>
<html>
<head>
<title>Comment Stored</title>
</head>
<body>
<h2>Comment Stored Successfully!</h2>
<p>Your comment has been saved and will be displayed to other users.</p>
<a href="/api/stored/comments">View all comments</a>
</body>
</html>
""".trimIndent()
}

@GetMapping(path = ["/comments"], produces = [MediaType.TEXT_HTML_VALUE])
open fun getComments(): String {
// VULNERABLE: Displays stored user input without sanitization
val commentsList = comments.joinToString("\n") { (author, comment) ->
"""
<div class="comment">
<p><strong>Author:</strong> $author</p>
<p><strong>Comment:</strong> $comment</p>
<hr>
</div>
""".trimIndent()
}

return """
<!DOCTYPE html>
<html>
<head>
<title>All Comments</title>
</head>
<body>
<h1>All Comments</h1>
${if (comments.isEmpty()) "<p>No comments yet.</p>" else commentsList}
</body>
</html>
""".trimIndent()
}

// ==== PATH PARAMETER - User Bio System ====

@Operation(
summary = "POST endpoint to store user bio (Stored XSS with path parameter)",
description = "Stores user bio in memory without sanitization - allows Stored XSS attacks via path parameter"
)
@ApiResponses(
value = [
ApiResponse(responseCode = "200", description = "Bio stored successfully"),
ApiResponse(responseCode = "400", description = "Invalid URI with special characters")
]
)
@PostMapping(path = ["/user/{username}"], produces = [MediaType.TEXT_HTML_VALUE])
open fun storeBio(
@PathVariable username: String,
@RequestParam(name = "bio", required = false, defaultValue = "") bio: String
): String {
// VULNERABLE: Stores user input from both path parameter and query parameter without sanitization
userBios[username] = bio

return """
<!DOCTYPE html>
<html>
<head>
<title>Bio Stored</title>
</head>
</html>
""".trimIndent()
}

@Operation(
summary = "GET endpoint to retrieve user profile with bio (Stored XSS)",
description = "Displays stored user bio without sanitization - executes stored XSS from path parameter data"
)
@ApiResponses(
value = [
ApiResponse(responseCode = "200", description = "User profile displayed"),
ApiResponse(responseCode = "400", description = "Invalid URI with special characters")
]
)
@GetMapping(path = ["/user/{username}"], produces = [MediaType.TEXT_HTML_VALUE])
open fun getUserProfile(@PathVariable username: String): String {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

these examples are fine... but remember XSS in APIs is not only when response are in HTML, but also in JSON (or XML). eg, think about SPA where the HTML is built on browser. add at least one test where response is JSON

// VULNERABLE: Displays stored user input without sanitization
val bio = userBios[username] ?: "No bio available"

return """
<!DOCTYPE html>
<html>
<head>
<title>User Profile</title>
</head>
<body>
<div class="profile-info">
<p><strong>Bio:</strong> $bio</p>
</div>
</body>
</html>
""".trimIndent()
}

// ==== QUERY PARAMETER - Guestbook System ====

@PostMapping(path = ["/guestbook"], produces = [MediaType.TEXT_HTML_VALUE])
open fun storeGuestbookEntry(
@RequestParam(name = "name", required = false, defaultValue = "Anonymous") name: String,
@RequestParam(name = "entry", required = false, defaultValue = "") entry: String
): String {
// VULNERABLE: Stores user input from query parameters without sanitization
guestbookEntries.add(Pair(name, entry))

return """
<!DOCTYPE html>
<html>
<head>
<title>Entry Stored</title>
</head>
<body>
<h2>Guestbook Entry Stored!</h2>
<p>Thank you for signing our guestbook!</p>
<a href="/api/stored/guestbook">View guestbook</a>
</body>
</html>
""".trimIndent()
}

@GetMapping(path = ["/guestbook"], produces = [MediaType.TEXT_HTML_VALUE])
open fun getGuestbook(): String {
// VULNERABLE: Displays stored user input without sanitization
val entriesList = guestbookEntries.joinToString("\n") { (name, entry) ->
"""
<div class="entry">
<p><strong>$name</strong> wrote:</p>
<p>$entry</p>
<hr>
</div>
""".trimIndent()
}

return """
<!DOCTYPE html>
<html>
<head>
<title>Guestbook</title>
</head>
<body>
<h1>Guestbook</h1>
${if (guestbookEntries.isEmpty()) "<p>No entries yet. Be the first to sign!</p>" else entriesList}
</body>
</html>
""".trimIndent()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.foo.rest.examples.spring.openapi.v3.security.xss.reflected

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

class XSSReflectedController: SpringController(XSSReflectedApplication::class.java)
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.foo.rest.examples.spring.openapi.v3.security.xss.stored

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

class XSSStoredController: SpringController(XSSStoredApplication::class.java)
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package org.evomaster.e2etests.spring.openapi.v3.security.xss.reflected

import com.foo.rest.examples.spring.openapi.v3.security.xss.reflected.XSSReflectedController
import com.webfuzzing.commons.faults.DefinedFaultCategory
import org.evomaster.core.EMConfig
import org.evomaster.core.problem.enterprise.DetectedFaultUtils
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 XSSReflectedEMTest : SpringTestBase() {

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

@Test
fun testXSSReflectedEM() {
runTestHandlingFlakyAndCompilation(
"XSSReflectedEMTest",
50,
) { args: MutableList<String> ->

setOption(args, "security", "true")


val solution = initAndRun(args)

assertTrue(solution.individuals.isNotEmpty())

val faultsCategories = DetectedFaultUtils.getDetectedFaultCategories(solution)
val faults = DetectedFaultUtils.getDetectedFaults(solution)

assertTrue(DefinedFaultCategory.XSS in faultsCategories)

assertTrue(faults.any {
it.category == DefinedFaultCategory.XSS
&& it.operationId == "POST:/api/reflected/comment"
})

assertTrue(faults.any {
it.category == DefinedFaultCategory.XSS
&& it.operationId == "GET:/api/reflected/search"
})

assertTrue(faults.any {
it.category == DefinedFaultCategory.XSS
&& it.operationId == "GET:/api/reflected/user/{username}"
})
}
}
}
Loading