Skip to content

Commit 3eb2108

Browse files
authored
Merge pull request #1452 from WebFuzzing/oracle-failed-modification
side effects of failed modification
2 parents 0baf9f4 + 99d0898 commit 3eb2108

23 files changed

Lines changed: 1968 additions & 24 deletions

File tree

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
package com.foo.rest.examples.spring.openapi.v3.httporacle.failmodification.base
2+
3+
import org.springframework.boot.SpringApplication
4+
import org.springframework.boot.autoconfigure.SpringBootApplication
5+
import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration
6+
import org.springframework.http.ResponseEntity
7+
import org.springframework.web.bind.annotation.GetMapping
8+
import org.springframework.web.bind.annotation.PatchMapping
9+
import org.springframework.web.bind.annotation.PathVariable
10+
import org.springframework.web.bind.annotation.PostMapping
11+
import org.springframework.web.bind.annotation.PutMapping
12+
import org.springframework.web.bind.annotation.RequestBody
13+
import org.springframework.web.bind.annotation.RequestMapping
14+
import org.springframework.web.bind.annotation.RestController
15+
16+
@SpringBootApplication(exclude = [SecurityAutoConfiguration::class])
17+
@RequestMapping(path = ["/api/resources"])
18+
@RestController
19+
open class FailModificationApplication {
20+
21+
companion object {
22+
@JvmStatic
23+
fun main(args: Array<String>) {
24+
SpringApplication.run(FailModificationApplication::class.java, *args)
25+
}
26+
27+
private val data = mutableMapOf<Int, ResourceData>()
28+
private val dataAlreadyExists = mutableMapOf<Int, ResourceData>()
29+
30+
fun reset(){
31+
data.clear()
32+
dataAlreadyExists.clear()
33+
dataAlreadyExists[0] = ResourceData("existing", 42)
34+
}
35+
}
36+
37+
data class ResourceData(
38+
var name: String,
39+
var value: Int
40+
)
41+
42+
data class UpdateRequest(
43+
val name: String,
44+
val value: Int
45+
)
46+
47+
48+
@PostMapping(path = ["/empty"])
49+
open fun create(@RequestBody body: ResourceData): ResponseEntity<ResourceData> {
50+
val id = data.size + 1
51+
data[id] = body.copy()
52+
return ResponseEntity.status(201).body(data[id])
53+
}
54+
55+
@GetMapping(path = ["/empty/{id}"])
56+
open fun get(@PathVariable("id") id: Int): ResponseEntity<ResourceData> {
57+
val resource = data[id]
58+
?: return ResponseEntity.status(404).build()
59+
return ResponseEntity.status(200).body(resource)
60+
}
61+
62+
@PutMapping(path = ["/empty/{id}"])
63+
open fun put(
64+
@PathVariable("id") id: Int,
65+
@RequestBody body: UpdateRequest
66+
): ResponseEntity<Any> {
67+
68+
val resource = data[id]
69+
?: return ResponseEntity.status(404).build()
70+
71+
// bug: modifies data even though it will return 4xx
72+
if(body.name != null) {
73+
resource.name = body.name
74+
}
75+
if(body.value != null) {
76+
resource.value = body.value
77+
}
78+
79+
// returns 400 Bad Request, but the data was already modified above
80+
return ResponseEntity.status(400).body("Invalid request")
81+
}
82+
83+
@PatchMapping(path = ["/empty/{id}"])
84+
open fun patch(
85+
@PathVariable("id") id: Int,
86+
@RequestBody body: UpdateRequest
87+
): ResponseEntity<Any> {
88+
89+
val resource = data[id]
90+
?: return ResponseEntity.status(404).build()
91+
92+
// correct: validation first, reject without modifying
93+
if(body.name == null && body.value == null) {
94+
return ResponseEntity.status(400).body("No fields to update")
95+
}
96+
97+
// correct: does NOT modify data, just returns 4xx
98+
return ResponseEntity.status(403).body("Forbidden")
99+
}
100+
101+
// pre-populated resource to test that it is not modified by failed PUT
102+
103+
@PostMapping(path = ["/notempty"])
104+
open fun createnotempty(@RequestBody body: ResourceData): ResponseEntity<ResourceData> {
105+
val id = dataAlreadyExists.size + 1
106+
dataAlreadyExists[id] = body.copy()
107+
return ResponseEntity.status(201).body(dataAlreadyExists[id])
108+
}
109+
110+
@GetMapping(path = ["/notempty/{id}"])
111+
open fun getnotempty(@PathVariable("id") id: Int): ResponseEntity<ResourceData> {
112+
val resource = dataAlreadyExists[id]
113+
?: return ResponseEntity.status(404).build()
114+
return ResponseEntity.status(200).body(resource)
115+
}
116+
117+
@PutMapping(path = ["/notempty/{id}"])
118+
open fun putnotempty(
119+
@PathVariable("id") id: Int,
120+
@RequestBody body: UpdateRequest
121+
): ResponseEntity<Any> {
122+
123+
val resource = dataAlreadyExists[id]
124+
?: return ResponseEntity.status(404).build()
125+
126+
resource.name = body.name
127+
resource.value = body.value
128+
129+
// returns 400 Bad Request, but the data was already modified above
130+
return ResponseEntity.status(400).body("Invalid request")
131+
}
132+
133+
@PatchMapping(path = ["/notempty/{id}"])
134+
open fun patchnotempty(
135+
@PathVariable("id") id: Int,
136+
@RequestBody body: UpdateRequest
137+
): ResponseEntity<Any> {
138+
139+
val resource = dataAlreadyExists[id]
140+
?: return ResponseEntity.status(404).build()
141+
142+
// correct: validation first, reject without modifying
143+
return ResponseEntity.status(400).body("No fields to update")
144+
145+
// correct: does NOT modify data, just returns 4xx
146+
return ResponseEntity.status(403).body("Forbidden")
147+
}
148+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package com.foo.rest.examples.spring.openapi.v3.httporacle.failmodification.forbidden
2+
3+
import org.springframework.boot.SpringApplication
4+
import org.springframework.boot.autoconfigure.SpringBootApplication
5+
import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration
6+
import org.springframework.http.ResponseEntity
7+
import org.springframework.web.bind.annotation.*
8+
9+
10+
@SpringBootApplication(exclude = [SecurityAutoConfiguration::class])
11+
@RequestMapping("/api/resources")
12+
@RestController
13+
open class FailModificationForbiddenApplication {
14+
15+
companion object {
16+
@JvmStatic
17+
fun main(args: Array<String>) {
18+
SpringApplication.run(FailModificationForbiddenApplication::class.java, *args)
19+
}
20+
21+
val USERS = setOf("FOO", "BAR")
22+
23+
private val data = mutableMapOf<Int, ResourceData>()
24+
25+
fun reset() {
26+
data.clear()
27+
}
28+
}
29+
30+
data class ResourceData(
31+
val name: String,
32+
var value: String
33+
)
34+
35+
data class UpdateRequest(
36+
val value: String
37+
)
38+
39+
private fun isValidUser(auth: String?) = auth != null && USERS.contains(auth)
40+
41+
@PostMapping
42+
open fun create(
43+
@RequestHeader(value = "Authorization", required = false) auth: String?,
44+
@RequestBody body: UpdateRequest
45+
): ResponseEntity<ResourceData> {
46+
if (!isValidUser(auth)) return ResponseEntity.status(401).build()
47+
val id = data.size + 1
48+
data[id] = ResourceData(name = auth!!, value = body.value)
49+
return ResponseEntity.status(201).body(data[id])
50+
}
51+
52+
@GetMapping("/{id}")
53+
open fun get(
54+
@RequestHeader(value = "Authorization", required = false) auth: String?,
55+
@PathVariable("id") id: Int
56+
): ResponseEntity<ResourceData> {
57+
if (!isValidUser(auth)) return ResponseEntity.status(401).build()
58+
val resource = data[id] ?: return ResponseEntity.status(404).build()
59+
return ResponseEntity.status(200).body(resource)
60+
}
61+
62+
@PatchMapping("/{id}")
63+
open fun patch(
64+
@RequestHeader(value = "Authorization", required = false) auth: String?,
65+
@PathVariable("id") id: Int,
66+
@RequestBody body: UpdateRequest
67+
): ResponseEntity<Any> {
68+
if (!isValidUser(auth)) return ResponseEntity.status(401).build()
69+
70+
val resource = data[id] ?: return ResponseEntity.status(404).build()
71+
72+
// BUG: side-effect before ownership check
73+
resource.value = body.value
74+
75+
if (resource.name != auth) return ResponseEntity.status(403).build()
76+
return ResponseEntity.status(200).build()
77+
}
78+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package com.foo.rest.examples.spring.openapi.v3.httporacle.failmodification.notfound
2+
3+
import org.springframework.boot.SpringApplication
4+
import org.springframework.boot.autoconfigure.SpringBootApplication
5+
import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration
6+
import org.springframework.http.ResponseEntity
7+
import org.springframework.web.bind.annotation.*
8+
9+
10+
@SpringBootApplication(exclude = [SecurityAutoConfiguration::class])
11+
@RequestMapping("/api/resources")
12+
@RestController
13+
open class FailModificationNotFoundApplication {
14+
15+
companion object {
16+
@JvmStatic
17+
fun main(args: Array<String>) {
18+
SpringApplication.run(FailModificationNotFoundApplication::class.java, *args)
19+
}
20+
21+
private val data = mutableMapOf<Int, ResourceData>()
22+
23+
fun reset() {
24+
data.clear()
25+
}
26+
}
27+
28+
data class ResourceData(val name: String, val value: Int)
29+
30+
data class UpdateRequest(val name: String, val value: Int)
31+
32+
33+
@GetMapping("/{id}")
34+
open fun get(@PathVariable("id") id: Int): ResponseEntity<ResourceData> {
35+
val resource = data[id] ?: return ResponseEntity.status(404).build()
36+
return ResponseEntity.ok(resource)
37+
}
38+
39+
@PutMapping("/{id}")
40+
open fun put(
41+
@PathVariable("id") id: Int,
42+
@RequestBody body: UpdateRequest
43+
): ResponseEntity<Any> {
44+
if (!data.containsKey(id)) {
45+
// BUG: stores the resource before returning 404
46+
data[id] = ResourceData(body.name, body.value)
47+
return ResponseEntity.status(404).build()
48+
}
49+
data[id] = ResourceData(body.name, body.value)
50+
return ResponseEntity.ok().build()
51+
}
52+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
package com.foo.rest.examples.spring.openapi.v3.httporacle.failmodification.urlencoded
2+
3+
import org.springframework.boot.SpringApplication
4+
import org.springframework.boot.autoconfigure.SpringBootApplication
5+
import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration
6+
import org.springframework.http.ResponseEntity
7+
import org.springframework.web.bind.annotation.GetMapping
8+
import org.springframework.web.bind.annotation.PatchMapping
9+
import org.springframework.web.bind.annotation.PathVariable
10+
import org.springframework.web.bind.annotation.PostMapping
11+
import org.springframework.web.bind.annotation.PutMapping
12+
import org.springframework.web.bind.WebDataBinder
13+
import org.springframework.web.bind.annotation.InitBinder
14+
import org.springframework.web.bind.annotation.ModelAttribute
15+
import org.springframework.web.bind.annotation.RequestMapping
16+
import org.springframework.web.bind.annotation.RestController
17+
import java.beans.PropertyEditorSupport
18+
19+
@SpringBootApplication(exclude = [SecurityAutoConfiguration::class])
20+
@RequestMapping(path = ["/api/resources"])
21+
@RestController
22+
open class UrlencodedFailModificationApplication {
23+
24+
companion object {
25+
@JvmStatic
26+
fun main(args: Array<String>) {
27+
SpringApplication.run(UrlencodedFailModificationApplication::class.java, *args)
28+
}
29+
30+
private val dataAlreadyExists = mutableMapOf<Int, ResourceData>()
31+
32+
fun reset(){
33+
dataAlreadyExists.clear()
34+
dataAlreadyExists[0] = ResourceData("existing", 42)
35+
}
36+
}
37+
38+
open class ResourceData(
39+
var name: String = "",
40+
var value: Int = 0
41+
)
42+
43+
open class UpdateRequest(
44+
var name: String = "",
45+
var value: Int = 0
46+
)
47+
48+
49+
@PostMapping(path = ["/notempty"], consumes = ["application/x-www-form-urlencoded"], produces = ["application/json"])
50+
open fun createnotempty(@ModelAttribute body: ResourceData): ResponseEntity<ResourceData> {
51+
val id = dataAlreadyExists.size + 1
52+
dataAlreadyExists[id] = ResourceData(body.name, body.value)
53+
return ResponseEntity.status(201).body(dataAlreadyExists[id])
54+
}
55+
56+
@GetMapping(path = ["/notempty/{id}"], produces = ["application/json"])
57+
open fun getnotempty(@PathVariable("id") id: Int): ResponseEntity<ResourceData> {
58+
val resource = dataAlreadyExists[id]
59+
?: return ResponseEntity.status(404).build()
60+
return ResponseEntity.status(200).body(resource)
61+
}
62+
63+
@PutMapping(path = ["/notempty/{id}"], consumes = ["application/x-www-form-urlencoded"], produces = ["text/plain"])
64+
open fun putnotempty(
65+
@PathVariable("id") id: Int,
66+
@ModelAttribute body: UpdateRequest
67+
): ResponseEntity<Any> {
68+
69+
val resource = dataAlreadyExists[id]
70+
?: return ResponseEntity.status(404).build()
71+
72+
resource.name = body.name
73+
resource.value = body.value
74+
75+
// returns 400 Bad Request, but the data was already modified above
76+
return ResponseEntity.status(400).body("Invalid request")
77+
}
78+
79+
@PatchMapping(path = ["/notempty/{id}"], consumes = ["application/x-www-form-urlencoded"], produces = ["text/plain"])
80+
open fun patchnotempty(
81+
@PathVariable("id") id: Int,
82+
@ModelAttribute body: UpdateRequest
83+
): ResponseEntity<Any> {
84+
85+
val resource = dataAlreadyExists[id]
86+
?: return ResponseEntity.status(404).build()
87+
88+
// correct: validation first, reject without modifying
89+
return ResponseEntity.status(400).body("No fields to update")
90+
91+
// correct: does NOT modify data, just returns 4xx
92+
return ResponseEntity.status(403).body("Forbidden")
93+
}
94+
}

0 commit comments

Comments
 (0)