18 KiB
Executable File
XSS Pentesting Report Fix
- Metadata
- Understanding the Session Stored XSS Finding (Pages 33–34)
Metadata
- Name
- Understanding Session XSS
- Overview
- Pages 33–34 document an informational “Session stored XSS” finding on the TMC Schedule Execution Board. The issue is real (unescaped user input in a JavaScript context) but impact is limited because only the submitting user’s session is affected—classic self-XSS, not cross-user attack.
Todos
- review-finding
- Read pages 33–34 and map finding to SaveSearchCriteriaToSession + ScheduleExecutionBoard.aspx flow
- locate-source
- Open TMC Web Portal repo and find session save + inline script render for date/orderID/time
- remediate-encode
- Apply HttpUtility.JavaScriptStringEncode to all session values in SetupControls() (~1849-1879)
- remediate-validate
- Add server-side validation in SaveSearchCriteriaToSession before writing SEBSessionState
- remediate-retest
- Retest with direct POST payload + normal UI search flow on ScheduleExecutionBoard
Understanding the Session Stored XSS Finding (Pages 33–34)
Where this sits in the report
The Microlise TMC PO WA April 2026 v1.0.pdf lists 14 findings total. Pages 33–34 (33-34.pdf) are the last technical finding before “END OF DOCUMENT”:
| Field | Value |
|---|---|
| Title | Session stored XSS |
| Severity | Informational (lowest tier; 4 informational findings in the report) |
| Status | Open |
| CWE | CWE-79 — Improper Neutralization of Input |
| Environment | cert.microlise.com (cert/UAT), path prefix /PENTEST/TMCWebPortal/ |
Higher-severity items in the same report (SQLi, IDOR, BFLA, etc.) are separate; this finding is documented as technically valid but low business risk.
What XSS is (general)
Cross-Site Scripting (XSS) means untrusted data ends up in a web page in a way the browser treats as executable JavaScript, instead of inert text.
The name “cross-site” is historical: classic attacks trick a victim into loading a page on your app so script runs in your origin (stealing session cookies, performing actions as the user, etc.).
Common types:
| Type | Persistence | Typical delivery |
|---|---|---|
| Reflected | Not stored; one-off response | Malicious link/query param |
| Stored | Saved server-side (DB, file, session) | Victim loads a normal page later |
| DOM-based | Client-side only | Unsafe innerHTML, eval, etc. |
Defense in depth: validate input on the server (whitelist formats), and encode output for the exact context (HTML, attribute, JavaScript string, URL).
What happened in this finding (TMC context)
Affected surface (source located)
| Role | Path |
|---|---|
| Page + inline JS | ScheduleExecutionBoard.aspx |
| WebMethod + page properties | ScheduleExecutionBoard.aspx.cs |
| Session storage | SEBSessionState.cs |
- Endpoint: ASP.NET
[WebMethod]SaveSearchCriteriaToSessiononScheduleExecutionBoard.aspx - Parameters: JSON fields
date,orderID,time(alsosearchID,hours,quickSearchin the same flow) - Host (pentest):
cert.microlise.com, path/PENTEST/TMCWebPortal/SEB/...
Attack flow (as tested)
flowchart LR
subgraph submit [Step1_Submit]
A[Tester sends POST directly]
B[SaveSearchCriteriaToSession]
C[Values stored in server session]
end
subgraph render [Step2_Render]
D[User loads ScheduleExecutionBoard.aspx]
E[Server embeds session values in script block]
F[Browser executes unescaped JS]
end
A --> B --> C
C --> D --> E --> F
- Save: User (or tester) POSTs JSON to
SaveSearchCriteriaToSession. The app saves search criteria into the server-side session. - Render: On the next load of
ScheduleExecutionBoard.aspx, those values are written into the HTML inside a<script>block, as JavaScript string literals. - Bug: Values are inserted without JavaScript string encoding. A crafted
datecan break out of the string and run arbitrary JS. - Proof: Pentesters confirmed execution in the browser; screenshots in the PDF show the POST and page source.
Code path (matches report exactly)
1. Save — no server-side validation
// ScheduleExecutionBoard.aspx.cs lines 336-348
[WebMethod]
public static void SaveSearchCriteriaToSession(string searchID, string orderID, string date, string time, int hours, bool displayPriorityJourneys, string quickSearch)
{
var sebState = new SEBSessionState();
sebState.ComplexSearch = searchID;
sebState.OrderBy = orderID;
sebState.SearchDate = date;
sebState.SearchTime = time;
// ...
}
2. Persist — per-user ASP.NET session
SEBSessionState.cs stores values under keys dateID, timeID, orderByID.
3. Load — on next full page GET
// ScheduleExecutionBoard.aspx.cs lines 267-277
private void SetupControls()
{
var sebState = new SEBSessionState();
SessionOrderID = sebState.OrderBy;
SessionDate = sebState.SearchDate;
SessionTime = sebState.SearchTime;
// ...
}
4. Render — vulnerable inline JavaScript (root cause)
// ScheduleExecutionBoard.aspx lines 1853-1875
if ("<%=SessionDate%>") {
$('#txtStart').val("<%= SessionDate %>");
}
if ("<%=SessionTime%>") {
$('#inputtime').val("<%=SessionTime%>");
}
if ("<%=SessionOrderID%>") {
$(orderBySelector + ' option[value="<%=SessionOrderID%>"]').attr('selected', 'selected');
}
Example payload in session: "); alert(document.domain); //
$('#txtStart').val(""); alert(document.domain); //");
Related: QuickSearch at line ~1879 — fix in the same pass.
Why “stored”, “session”, and “self-XSS”
- Stored: Payload survives page navigation in session state.
- Self-XSS: Only the submitter’s session is affected; no normal cross-user path.
- Severity Informational because threat model is weak vs shared stored XSS.
Client vs server validation gap
Pentesters bypassed browser validation via direct POST. No server-side validation blocked arbitrary strings.
Replicating the vulnerability (hands-on)
Use this section to see the bug work on an authorized environment (e.g. cert/UAT), then repeat the same steps after fixes and compare outcomes.
Prerequisites
| Requirement | Detail |
|---|---|
| Authorization | Pentest scope or internal security test policy only |
| Permission | Microlise:TMC:SEB:Read |
| URL | e.g. https://<host>/TMCWebPortal/SEB/ScheduleExecutionBoard.aspx |
| Tools | Browser + DevTools or Burp Suite |
| Build | Before-fix build first; redeploy with remediation for after-fix runs |
Must be logged in (valid session cookie on POST).
What you should observe (before fix)
sequenceDiagram
participant You as Tester_browser
participant API as SaveSearchCriteriaToSession
participant Sess as ASP.NET_session
participant Page as ScheduleExecutionBoard_GET
You->>API: POST JSON with malicious date
API->>Sess: Store raw date in session
You->>Page: Reload SEB page
Page->>You: HTML with unescaped date inside script
You->>You: alert or other JS runs
- WebMethod returns HTTP 200.
- Payload never went through
DateValidation(). - Full page reload → JS runs (e.g.
alert). - View Source: payload inside double-quoted JS string, unescaped.
Self-XSS: only your session is poisoned.
Step-by-step reproduction
Step 1 — Baseline (optional)
- Open SEB, perform a search.
- DevTools → Network →
SaveSearchCriteriaToSession. - Note POST,
application/json, body shape, Cookie header.
Step 2 — Inject via direct POST (bypass UI)
| Parameter | Suggested test value |
|---|---|
searchID |
0.X |
orderID |
0.X |
date |
"); alert(document.domain);// |
time |
00:00 |
hours |
24 |
displayPriorityJourneys |
false |
quickSearch |
"" |
Burp: Repeater → replace JSON body → send.
Browser console (on SEB page, same origin):
fetch('ScheduleExecutionBoard.aspx/SaveSearchCriteriaToSession', {
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/json; charset=utf-8' },
body: JSON.stringify({
searchID: '0.X',
orderID: '0.X',
date: '"); alert(document.domain);//',
time: '00:00',
hours: 24,
displayPriorityJourneys: false,
quickSearch: ''
})
}).then(r => console.log('status', r.status));
curl (replace host, path, cookies):
curl -s -o /dev/null -w "%{http_code}" \
-X POST "https://<host>/<TMCWebPortal>/SEB/ScheduleExecutionBoard.aspx/SaveSearchCriteriaToSession" \
-H "Content-Type: application/json; charset=utf-8" \
-H "Cookie: <paste-session-cookies>" \
-d "{\"searchID\":\"0.X\",\"orderID\":\"0.X\",\"date\":\"\\\"); alert(document.domain);//\",\"time\":\"00:00\",\"hours\":24,\"displayPriorityJourneys\":false,\"quickSearch\":\"\"}"
Step 3 — Trigger render (stored XSS)
- Full navigation reload of
ScheduleExecutionBoard.aspx(F5). SetupControls()embeds sessiondate(~lines 1854–1855).
Step 4 — Confirm
| Check | Before fix (expected) |
|---|---|
| Popup / console | alert(document.domain) runs |
| View Source | Literal "); alert(...) inside $('#txtStart').val("...") unescaped |
| Network on reload | Normal GET only; XSS from inline script |
| Other users | No effect (different session) |
Step 5 — Optional: malicious time or orderID.
Step 6 — Clean up: log out/in or POST valid date/time.
Comparison matrix (before vs after fixes)
Run the same Steps 2–4 after each change:
| Observation | Before fix | After encoding only | After validation only | After both |
|---|---|---|---|---|
POST malicious date accepted? |
Yes (200) | Yes (200) | No / not stored | No |
alert on reload? |
Yes | No | Depends* | No |
| Executable JS in View Source? | Yes | No (escaped) | Depends* | No |
#txtStart shows attack text? |
Maybe | Escaped/safe | Default/empty | Default/empty |
| Normal UI search + reload works? | Yes | Yes | Yes | Yes |
*If only validation: reload may show no XSS without encoding — still apply both fixes.
Why UI-only testing misses the bug
| Path | DateValidation() runs? |
Payload reaches session? |
|---|---|---|
| Click Search in UI | Yes | No (normal typing) |
| Direct POST / Burp / fetch | No | Yes |
Reproduction must use direct POST to match the pentest.
Evidence to capture (for fix sign-off)
- Request (POST body with payload).
- Screenshot of alert (before) or no alert (after).
- View Source snippet around
$('#txtStart').val(. - Regression: legitimate date, reload, criteria restored.
Safety and scope
- No production without approval.
- Prefer
alert(document.domain)over exfiltration demos. - Self-XSS: coding defect demo, not mass compromise.
How to fix it
Use two layers: output encoding + server-side validation.
Fix 1 — Output encoding (required)
File: ScheduleExecutionBoard.aspx, SetupControls() (~1847–1884).
| Line (approx) | Field | Encode |
|---|---|---|
| 1849–1850 | SessionSearchID | Yes |
| 1854–1855 | SessionDate | Yes |
| 1860–1861 | SessionTime | Yes |
| 1872–1874 | SessionOrderID | Yes |
| 1878–1879 | QuickSearch | Yes |
Before:
$('#txtStart').val("<%= SessionDate %>");
After:
$('#txtStart').val("<%= HttpUtility.JavaScriptStringEncode(SessionDate ?? string.Empty) %>");
Encode if guards too, or use code-behind booleans (HasSessionDate).
Fix 2 — Server-side input validation
File: ScheduleExecutionBoard.aspx.cs, SaveSearchCriteriaToSession (~337).
| Parameter | Validation rule |
|---|---|
date |
Same regex as client DateValidation() |
time |
^[0-2][0-9]:[0-5][0-9]$ |
orderID |
^[0-9]+(\.X)?$ |
searchID |
Same as orderID |
hours |
Clamp 1–999 |
[WebMethod]
public static void SaveSearchCriteriaToSession(...)
{
if (!IsValidSebDate(date) || !IsValidSebTime(time)
|| !IsValidQueryComponentId(orderID) || !IsValidQueryComponentId(searchID))
{
return;
}
var sebState = new SEBSessionState();
// ...
}
Date regex:
^[0-9]{4}-(((0[13578]|(10|12))-(0[1-9]|[1-2][0-9]|3[0-1]))|(02-(0[1-9]|[1-2][0-9]))|((0[469]|11)-(0[1-9]|[1-2][0-9]|30)))$
Fix 3 — What not to do
- Do not rely on client
DateValidation()alone. - Do not use
HtmlEncodein JS string literals. - Do not use
innerHTML; keep.val().
Fix 4 — Verification / test plan
Master procedure: Replicating the vulnerability section above.
Negative test:
{
"searchID": "0.X",
"orderID": "0.X",
"date": "\"); alert(1);//",
"time": "00:00",
"hours": 24,
"displayPriorityJourneys": false,
"quickSearch": ""
}
Pass: No alert; escaped in source; invalid date not stored (with Fix 2).
Positive test: UI search + reload restores criteria.
Files to change (summary)
| File | Change |
|---|---|
| ScheduleExecutionBoard.aspx | JavaScriptStringEncode in SetupControls() |
| ScheduleExecutionBoard.aspx.cs | Validation in SaveSearchCriteriaToSession |
References: OWASP XSS, PortSwigger Stored XSS, JavaScriptStringEncode
Mental model: severity vs correctness
| Question | Answer |
|---|---|
| Real coding flaw? | Yes (CWE-79) |
| Cross-user session hijack? | Not under normal use (self-XSS) |
| Should it still be fixed? | Yes, as hygiene |
| Priority vs SQLi / IDOR? | Much lower (informational) |
Next step
- Reproduce on cert/UAT (
replicate-vulntodo). - Implement fixes in
D:\_dev\WebPortal(remediate-encode,remediate-validate). - Replay steps and fill comparison matrix (
remediate-retesttodo).
Confirm execution when ready to change WebPortal code.