Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
8 changes: 7 additions & 1 deletion src/ui/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,14 @@ Several "utility" endpoints are provided with useful functionality for various s

| Method | Name | Description |
| ------ | ------------------------------ | --------------------------------------------------------------------------- |
| `GET` | `/utility/stress/{iterations}` | Stress the CPU with the number of iterations increasing the CPU consumption |
| `GET` | `/utility/status/{code}` | Returns HTTP response with given HTTP status code |
| `GET` | `/utility/headers` | Print the HTTP headers of the inbound request |
| `GET` | `/utility/panic` | Shutdown the application with an error code |
| `POST` | `/utility/echo` | Write back the POST payload sent |
| `POST` | `/utility/store` | Write the payload to a file and return a hash |
| `GET` | `/utility/store/{hash}` | Return the payload from the file system previously written |
| `GET` | `/utility/stress/{iterations}` | Stress the CPU with the number of iterations increasing the CPU consumption |


## Running

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,32 @@
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import java.util.List;
import java.util.Map;

import org.springframework.boot.SpringApplication;
import org.springframework.context.ApplicationContext;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;

@Controller
@RequestMapping("/utility")
public class UtilityController {

private final ApplicationContext context;

public UtilityController(ApplicationContext context) {
this.context = context;
}

@GetMapping("/stress/{iterations}")
@ResponseBody
public double stress(@PathVariable int iterations) {
Expand All @@ -51,4 +69,62 @@ private double monteCarloPi(int iterations) {
public ResponseEntity<String> status(@PathVariable int code) {
return ResponseEntity.status(code).body("OK");
}

@GetMapping(value = "/headers", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<Map<String, List<String>>> statusHeaders(@RequestHeader HttpHeaders headers) {
return ResponseEntity.ok()
.body(headers);
}

@GetMapping("/panic")
public ResponseEntity<String> panic() {
Thread thread = new Thread(() -> {
try {
Thread.sleep(500); // Small delay to allow response to be sent
SpringApplication.exit(context, () -> 1);
System.exit(255);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
thread.start();
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Shutting down...");
}

// function /echo that answer the hash provided as POST input
@PostMapping("/echo")
@ResponseBody
public ResponseEntity<String> echo(@RequestBody String body) {
return ResponseEntity.ok()
.body(body);
}

// function /store what take a POST hash to write to a locally created file
@PostMapping("/store")
@ResponseBody
public ResponseEntity<String> store(@RequestBody String body) {
String filename = String.valueOf(Math.abs(body.hashCode())); // ensure positive number
try (java.io.FileWriter fileWriter = new java.io.FileWriter("/tmp/" + filename + ".json")) {
fileWriter.write(body);
return ResponseEntity.ok()
.body("{\"hash\": \"" + filename + "\"}");
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body("Error writing file: " + e.getMessage());
}
}

// function /store/{hash} that read a local hash file
@GetMapping("/store/{hash}")
@ResponseBody
public ResponseEntity<String> read(@PathVariable String hash) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

@guikcd can we put in protections to make sure this can only open files within /tmp? Right now you could probably use something like ../root/whatever to traverse the filesystem. You'll probably have to get the absolute path to the file it tries to load and verify its in /tmp.

Copy link
Contributor Author

@guikcd guikcd Feb 7, 2025

Choose a reason for hiding this comment

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

2 mechanisms already prevented us having "complete" path traversal:

  • GetMapping /store/{hash} didn't understand ../root/...
  • The code always add a suffix .json only these files was subject to path traversal.

As it is still a threat, I've changed the code to:

  • filter the hash format to numbers only
  • verify that the file always start from /tmp/. Due to previous control, nearly not possible to reach here

try (java.util.Scanner scanner = new java.util.Scanner(new java.io.File("/tmp/" + hash + ".json"))) {
String body = scanner.useDelimiter("\\A").hasNext() ? scanner.next() : "";
return ResponseEntity.ok()
.body(body);
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body("Error reading file: " + e.getMessage());
}
}
}