Skip to content

Commit 580b45a

Browse files
authored
Merge branch 'master' into feature/dynamosa-3
2 parents 18b4f1e + 0c9d33c commit 580b45a

163 files changed

Lines changed: 4469 additions & 1870 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
package com.foo.rest.examples.spring.openapi.v3.security.xss.reflected
2+
3+
import io.swagger.v3.oas.annotations.Operation
4+
import io.swagger.v3.oas.annotations.responses.ApiResponse
5+
import io.swagger.v3.oas.annotations.responses.ApiResponses
6+
import org.springframework.boot.SpringApplication
7+
import org.springframework.boot.autoconfigure.SpringBootApplication
8+
import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration
9+
import org.springframework.http.MediaType
10+
import org.springframework.web.bind.annotation.*
11+
12+
data class CommentDto(
13+
val comment: String? = null,
14+
val author: String? = null
15+
)
16+
17+
@SpringBootApplication(exclude = [SecurityAutoConfiguration::class])
18+
@RequestMapping(path = ["/api/reflected"])
19+
@RestController
20+
open class XSSReflectedApplication {
21+
22+
companion object {
23+
@JvmStatic
24+
fun main(args: Array<String>) {
25+
SpringApplication.run(XSSReflectedApplication::class.java, *args)
26+
}
27+
}
28+
29+
// ==== BODY PARAMETER - Comment System ====
30+
31+
@PostMapping(path = ["/comment"], produces = [MediaType.TEXT_HTML_VALUE])
32+
open fun reflectComment(@RequestBody commentDto: CommentDto): String {
33+
// VULNERABLE: Reflects user input without sanitization
34+
val comment = commentDto.comment ?: "No comment"
35+
val author = commentDto.author ?: "Anonymous"
36+
37+
return """
38+
<!DOCTYPE html>
39+
<html>
40+
<head>
41+
<title>Comment Reflected</title>
42+
</head>
43+
<body>
44+
<h2>Comment Received!</h2>
45+
<div class="comment">
46+
<p><strong>Author:</strong> $author</p>
47+
<p><strong>Comment:</strong> $comment</p>
48+
</div>
49+
</body>
50+
</html>
51+
""".trimIndent()
52+
}
53+
54+
// ==== PATH PARAMETER - User Profile System ====
55+
56+
@Operation(
57+
summary = "GET endpoint to display user profile (Reflected XSS with path parameter)",
58+
description = "Displays user profile without sanitization - allows Reflected XSS attacks via path parameter"
59+
)
60+
@ApiResponses(
61+
value = [
62+
ApiResponse(responseCode = "200", description = "User profile displayed"),
63+
ApiResponse(responseCode = "400", description = "Invalid URI with special characters")
64+
]
65+
)
66+
@GetMapping(path = ["/user/{username}"], produces = [MediaType.TEXT_HTML_VALUE])
67+
open fun getUserProfile(@PathVariable username: String): String {
68+
// VULNERABLE: Reflects path parameter without sanitization
69+
return """
70+
<!DOCTYPE html>
71+
<html>
72+
<head>
73+
<title>User Profile</title>
74+
</head>
75+
<body>
76+
<h1>Profile of $username</h1>
77+
<div class="profile-info">
78+
<p><strong>Username:</strong> $username</p>
79+
<p>Welcome to $username's profile page!</p>
80+
</div>
81+
</body>
82+
</html>
83+
""".trimIndent()
84+
}
85+
86+
// ==== QUERY PARAMETER - Search System ====
87+
88+
@GetMapping(path = ["/search"], produces = [MediaType.TEXT_HTML_VALUE])
89+
open fun search(
90+
@RequestParam(name = "query", required = false, defaultValue = "") query: String
91+
): String {
92+
// VULNERABLE: Reflects query parameter without sanitization
93+
return """
94+
<!DOCTYPE html>
95+
<html>
96+
<head>
97+
<title>Search Results</title>
98+
</head>
99+
<body>
100+
<h1>Search Results</h1>
101+
<p>You searched for: <strong>$query</strong></p>
102+
<div class="results">
103+
<p>No results found for "$query"</p>
104+
</div>
105+
</body>
106+
</html>
107+
""".trimIndent()
108+
}
109+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
package com.foo.rest.examples.spring.openapi.v3.security.xss.stored
2+
3+
import io.swagger.v3.oas.annotations.Operation
4+
import io.swagger.v3.oas.annotations.responses.ApiResponse
5+
import io.swagger.v3.oas.annotations.responses.ApiResponses
6+
import org.springframework.boot.SpringApplication
7+
import org.springframework.boot.autoconfigure.SpringBootApplication
8+
import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration
9+
import org.springframework.http.MediaType
10+
import org.springframework.web.bind.annotation.*
11+
12+
data class CommentDto(
13+
val comment: String? = null,
14+
val author: String? = null
15+
)
16+
17+
@SpringBootApplication(exclude = [SecurityAutoConfiguration::class])
18+
@RequestMapping(path = ["/api/stored"])
19+
@RestController
20+
open class XSSStoredApplication {
21+
22+
companion object {
23+
@JvmStatic
24+
fun main(args: Array<String>) {
25+
SpringApplication.run(XSSStoredApplication::class.java, *args)
26+
}
27+
28+
// In-memory storage for stored XSS examples
29+
private val comments = mutableListOf<Pair<String, String>>() // Body parameter
30+
private val userBios = mutableMapOf<String, String>() // Path parameter
31+
private val guestbookEntries = mutableListOf<Pair<String, String>>() // Query parameter
32+
}
33+
34+
// ==== BODY PARAMETER - Comment System ====
35+
36+
@PostMapping(path = ["/comment"], produces = [MediaType.TEXT_HTML_VALUE])
37+
open fun storeComment(@RequestBody commentDto: CommentDto): String {
38+
// VULNERABLE: Stores user input without sanitization
39+
val comment = commentDto.comment ?: "No comment"
40+
val author = commentDto.author ?: "Anonymous"
41+
42+
comments.add(Pair(author, comment))
43+
44+
return """
45+
<!DOCTYPE html>
46+
<html>
47+
<head>
48+
<title>Comment Stored</title>
49+
</head>
50+
<body>
51+
<h2>Comment Stored Successfully!</h2>
52+
<p>Your comment has been saved and will be displayed to other users.</p>
53+
<a href="/api/stored/comments">View all comments</a>
54+
</body>
55+
</html>
56+
""".trimIndent()
57+
}
58+
59+
@GetMapping(path = ["/comments"], produces = [MediaType.TEXT_HTML_VALUE])
60+
open fun getComments(): String {
61+
// VULNERABLE: Displays stored user input without sanitization
62+
val commentsList = comments.joinToString("\n") { (author, comment) ->
63+
"""
64+
<div class="comment">
65+
<p><strong>Author:</strong> $author</p>
66+
<p><strong>Comment:</strong> $comment</p>
67+
<hr>
68+
</div>
69+
""".trimIndent()
70+
}
71+
72+
return """
73+
<!DOCTYPE html>
74+
<html>
75+
<head>
76+
<title>All Comments</title>
77+
</head>
78+
<body>
79+
<h1>All Comments</h1>
80+
${if (comments.isEmpty()) "<p>No comments yet.</p>" else commentsList}
81+
</body>
82+
</html>
83+
""".trimIndent()
84+
}
85+
86+
// ==== PATH PARAMETER - User Bio System ====
87+
88+
@Operation(
89+
summary = "POST endpoint to store user bio (Stored XSS with path parameter)",
90+
description = "Stores user bio in memory without sanitization - allows Stored XSS attacks via path parameter"
91+
)
92+
@ApiResponses(
93+
value = [
94+
ApiResponse(responseCode = "200", description = "Bio stored successfully"),
95+
ApiResponse(responseCode = "400", description = "Invalid URI with special characters")
96+
]
97+
)
98+
@PostMapping(path = ["/user/{username}"], produces = [MediaType.TEXT_HTML_VALUE])
99+
open fun storeBio(
100+
@PathVariable username: String,
101+
@RequestParam(name = "bio", required = false, defaultValue = "") bio: String
102+
): String {
103+
// VULNERABLE: Stores user input from both path parameter and query parameter without sanitization
104+
userBios[username] = bio
105+
106+
return """
107+
<!DOCTYPE html>
108+
<html>
109+
<head>
110+
<title>Bio Stored</title>
111+
</head>
112+
</html>
113+
""".trimIndent()
114+
}
115+
116+
@Operation(
117+
summary = "GET endpoint to retrieve user profile with bio (Stored XSS)",
118+
description = "Displays stored user bio without sanitization - executes stored XSS from path parameter data"
119+
)
120+
@ApiResponses(
121+
value = [
122+
ApiResponse(responseCode = "200", description = "User profile displayed"),
123+
ApiResponse(responseCode = "400", description = "Invalid URI with special characters")
124+
]
125+
)
126+
@GetMapping(path = ["/user/{username}"], produces = [MediaType.TEXT_HTML_VALUE])
127+
open fun getUserProfile(@PathVariable username: String): String {
128+
// VULNERABLE: Displays stored user input without sanitization
129+
val bio = userBios[username] ?: "No bio available"
130+
131+
return """
132+
<!DOCTYPE html>
133+
<html>
134+
<head>
135+
<title>User Profile</title>
136+
</head>
137+
<body>
138+
<div class="profile-info">
139+
<p><strong>Bio:</strong> $bio</p>
140+
</div>
141+
</body>
142+
</html>
143+
""".trimIndent()
144+
}
145+
146+
// ==== QUERY PARAMETER - Guestbook System ====
147+
148+
@PostMapping(path = ["/guestbook"], produces = [MediaType.TEXT_HTML_VALUE])
149+
open fun storeGuestbookEntry(
150+
@RequestParam(name = "name", required = false, defaultValue = "Anonymous") name: String,
151+
@RequestParam(name = "entry", required = false, defaultValue = "") entry: String
152+
): String {
153+
// VULNERABLE: Stores user input from query parameters without sanitization
154+
guestbookEntries.add(Pair(name, entry))
155+
156+
return """
157+
<!DOCTYPE html>
158+
<html>
159+
<head>
160+
<title>Entry Stored</title>
161+
</head>
162+
<body>
163+
<h2>Guestbook Entry Stored!</h2>
164+
<p>Thank you for signing our guestbook!</p>
165+
<a href="/api/stored/guestbook">View guestbook</a>
166+
</body>
167+
</html>
168+
""".trimIndent()
169+
}
170+
171+
@GetMapping(path = ["/guestbook"], produces = [MediaType.TEXT_HTML_VALUE])
172+
open fun getGuestbook(): String {
173+
// VULNERABLE: Displays stored user input without sanitization
174+
val entriesList = guestbookEntries.joinToString("\n") { (name, entry) ->
175+
"""
176+
<div class="entry">
177+
<p><strong>$name</strong> wrote:</p>
178+
<p>$entry</p>
179+
<hr>
180+
</div>
181+
""".trimIndent()
182+
}
183+
184+
return """
185+
<!DOCTYPE html>
186+
<html>
187+
<head>
188+
<title>Guestbook</title>
189+
</head>
190+
<body>
191+
<h1>Guestbook</h1>
192+
${if (guestbookEntries.isEmpty()) "<p>No entries yet. Be the first to sign!</p>" else entriesList}
193+
</body>
194+
</html>
195+
""".trimIndent()
196+
}
197+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package com.foo.rest.examples.spring.openapi.v3.security.xss.reflected
2+
3+
import com.foo.rest.examples.spring.openapi.v3.SpringController
4+
5+
class XSSReflectedController: SpringController(XSSReflectedApplication::class.java)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package com.foo.rest.examples.spring.openapi.v3.security.xss.stored
2+
3+
import com.foo.rest.examples.spring.openapi.v3.SpringController
4+
5+
class XSSStoredController: SpringController(XSSStoredApplication::class.java)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package org.evomaster.e2etests.spring.openapi.v3.security.xss.reflected
2+
3+
import com.foo.rest.examples.spring.openapi.v3.security.xss.reflected.XSSReflectedController
4+
import com.webfuzzing.commons.faults.DefinedFaultCategory
5+
import org.evomaster.core.EMConfig
6+
import org.evomaster.core.problem.enterprise.DetectedFaultUtils
7+
import org.evomaster.e2etests.spring.openapi.v3.SpringTestBase
8+
import org.junit.jupiter.api.Assertions.assertTrue
9+
import org.junit.jupiter.api.BeforeAll
10+
import org.junit.jupiter.api.Test
11+
12+
class XSSReflectedEMTest : SpringTestBase() {
13+
14+
companion object {
15+
@BeforeAll
16+
@JvmStatic
17+
fun init() {
18+
val config = EMConfig()
19+
config.instrumentMR_NET = false
20+
initClass(XSSReflectedController(), config)
21+
}
22+
}
23+
24+
@Test
25+
fun testXSSReflectedEM() {
26+
runTestHandlingFlakyAndCompilation(
27+
"XSSReflectedEMTest",
28+
50,
29+
) { args: MutableList<String> ->
30+
31+
setOption(args, "security", "true")
32+
33+
34+
val solution = initAndRun(args)
35+
36+
assertTrue(solution.individuals.isNotEmpty())
37+
38+
val faultsCategories = DetectedFaultUtils.getDetectedFaultCategories(solution)
39+
val faults = DetectedFaultUtils.getDetectedFaults(solution)
40+
41+
assertTrue(DefinedFaultCategory.XSS in faultsCategories)
42+
43+
assertTrue(faults.any {
44+
it.category == DefinedFaultCategory.XSS
45+
&& it.operationId == "POST:/api/reflected/comment"
46+
})
47+
48+
assertTrue(faults.any {
49+
it.category == DefinedFaultCategory.XSS
50+
&& it.operationId == "GET:/api/reflected/search"
51+
})
52+
53+
assertTrue(faults.any {
54+
it.category == DefinedFaultCategory.XSS
55+
&& it.operationId == "GET:/api/reflected/user/{username}"
56+
})
57+
}
58+
}
59+
}

0 commit comments

Comments
 (0)