Skip to content

Commit 39d2fcb

Browse files
authored
Merge pull request #1383 from WebFuzzing/xss-starter
XSS Starter
2 parents ae2abed + d5dd32f commit 39d2fcb

26 files changed

Lines changed: 1627 additions & 27 deletions

File tree

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)