Three Stored XSS in SourceCodester Laboratory Management System
Stored Cross-Site Scripting (CWE-79) — three instances in the Add Borrow workflow of the SourceCodester Computer Laboratory Management System (PHP/MySQL).
TL;DR
The Add Borrow workflow in this application stores three separate user-supplied fields — Borrower Name, Department, and Remarks — without sanitizing input or encoding output. Each is echoed back unescaped on the borrow view page, so JavaScript placed in any of the three is stored and then executed in the browser of every user who later opens that record, including administrators. Each field received its own CVE:
| CVE | Field | Parameter |
|---|---|---|
| CVE-2024-35581 | Borrower Name | lname |
| CVE-2024-35582 | Department | department |
| CVE-2024-35583 | Remarks | remarks |
Individually, each is a textbook stored XSS. Together, they show what happens when an application trusts an entire form instead of an individual field — and why methodical source→sink enumeration finds every instance, not just the first.
Background
SourceCodester's Laboratory Management System is a small PHP/MySQL app for managing lab equipment. Its Borrowing feature records who borrowed an item, from which department, with free-text remarks. The record is created via a POST request and later rendered in the admin panel at:
/php-lms/admin/?page=borrow/view_borrow&id=<record_id>
All three fields — Borrower Name, Department, Remarks — are written to the database on create and echoed back into that view page on read. None is encoded at output. That single missing control, repeated across three fields, is three vulnerabilities.
The vulnerability
The create-borrow handler takes each field from the POST body and:
- Stores it without input sanitization, and
- Renders it without output encoding on the borrow view page.
At render time the stored value is treated as trusted HTML. Supplying markup instead of a plain value causes the browser to parse and execute it — every time the record is viewed. Because the values persist, all three are stored (persistent) XSS, not reflected: the victim doesn't need to follow a crafted link; simply opening the record in the normal admin workflow triggers execution.
Proof of concept
The same technique confirms all three; only the target field changes.
- Configure your browser to proxy through Burp Suite (or any intercepting proxy).
- In the admin panel, begin creating a Borrow record and fill in the fields.
- Intercept the create request and set the target parameter to the payload below:
"><img src=x onerror=alert(document.cookie)>
- For CVE-2024-35581, set
lname(Borrower Name) - For CVE-2024-35582, set
department(Department) - For CVE-2024-35583, set
remarks(Remarks)
- Forward the request to store the record.
- Open the borrow view page:
/php-lms/admin/?page=borrow/view_borrow&id=<id>
The img element fails to load src=x, firing the onerror handler, which runs the JavaScript. alert(document.cookie) demonstrates read access to the session cookie; a real attacker would exfiltrate it to an attacker-controlled endpoint. Because each value persists, the payload re-fires for any user — including higher-privileged accounts — who opens the record.
Impact
Stored XSS executing in an authenticated admin context allows an attacker to:
- Steal session cookies and hijack authenticated sessions (account takeover).
- Act as the victim within the application (create/modify/delete records).
- Read sensitive data rendered in the victim's session.
- Deface or redirect — modify page content or send users to malicious sites.
The persistence plus the privileged rendering context are what make these High severity: no social engineering is required, and the code runs wherever an admin reviews borrow records. Three injectable fields simply give an attacker three independent ways in.
The hunt — why all three, not just one
The interesting part isn't the payload; it's the coverage. Finding one stored XSS and stopping is a missed opportunity — applications that fail to encode one field usually fail to encode its neighbors, because the flaw is architectural (no output-encoding layer), not a one-off typo.
- Enumerate source→sink pairs, not fields at random. For a CRUD app, list every input that gets stored and later displayed. The Add Borrow form has three free-text inputs, all rendered back on one view page — three candidate pairs.
- Test every candidate, not just the first hit. Once Borrower Name proved injectable, Department and Remarks were tested with the same payload rather than assumed safe. All three failed identically — confirming a systemic missing control.
- Tamper below the UI. Client-side field types and length limits are irrelevant once you intercept the POST; the payload goes straight onto the parameter.
- Confirm at the sink. The test is "does it execute when rendered," not "did it save." Loading the view page and watching each
onerrorfire is the confirmation.
Remediation
Standard XSS defense, applied at both ends and to all user-controlled fields — not just the one that was reported:
- Output encoding (primary fix): HTML-entity-encode every user-supplied value before rendering. In PHP,
htmlspecialchars($value, ENT_QUOTES, 'UTF-8')at the output point neutralizes the payload regardless of what was stored. Apply it consistently across Borrower Name, Department, Remarks, and every other rendered field. - Input validation (defense in depth): constrain each field to expected characters server-side; reject or strip markup.
- Context-aware escaping: encode for the context the value lands in (HTML body vs. attribute vs. JS).
- Content-Security-Policy: a restrictive CSP limits the blast radius of any XSS that slips through.
// before — value echoed raw into the page
echo $row['lname'];
// after — entity-encoded, payload rendered inert
echo htmlspecialchars($row['lname'], ENT_QUOTES, 'UTF-8');
Disclosure timeline
- Vulnerabilities identified in SourceCodester Laboratory Management System.
- Reported to the vendor / disclosure channel.
- CVE-2024-35581, CVE-2024-35582, and CVE-2024-35583 assigned.
(Replace with exact dates: found → reported → acknowledged → CVEs assigned.)
References
- CWE-79: Improper Neutralization of Input During Web Page Generation — cwe.mitre.org/data/definitions/79.html
- OWASP — Cross-Site Scripting (XSS) — owasp.org/www-community/attacks/xss
- PortSwigger — Stored XSS — portswigger.net/web-security/cross-site-scripting/stored
- NVD — CVE-2024-35581 · CVE-2024-35582 · CVE-2024-35583
Part of a series documenting my published CVEs. More of my work and tooling at github.com/r04i7.