439 lines
18 KiB
Org Mode
Executable File
439 lines
18 KiB
Org Mode
Executable File
:PROPERTIES:
|
||
:ID: b2ac09aa-e888-48d3-8357-2292f9b2526c
|
||
:END:
|
||
#+title: XSS Pentesting Report Fix
|
||
#+filetags: :microlise:security:notes:
|
||
|
||
* 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
|
||
|
||
- [X] review-finding :: Read pages 33–34 and map finding to SaveSearchCriteriaToSession + ScheduleExecutionBoard.aspx flow
|
||
- [X] 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 [[file:d:/_dev/_misc/Pentest-04-26/Microlise TMC PO WA April 2026 v1.0.pdf][Microlise TMC PO WA April 2026 v1.0.pdf]] lists *14 findings* total. Pages 33–34 ([[file:d:/_dev/_misc/Pentest-04-26/33-34.pdf][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 | [[https://cwe.mitre.org/data/definitions/79.html][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.
|
||
|
||
#+begin_src mermaid :exports none
|
||
sequenceDiagram
|
||
participant Attacker
|
||
participant App as WebApplication
|
||
participant Victim as VictimBrowser
|
||
|
||
Attacker->>App: Submit malicious input
|
||
App->>App: Store or reflect input
|
||
App->>Victim: HTML/JS page containing payload
|
||
Victim->>Victim: Browser runs attacker's script
|
||
Note over Victim: Script runs with the site's origin<br/>can access cookies, DOM, APIs
|
||
#+end_src
|
||
|
||
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 | [[file:d:/_dev/WebPortal/src/code/AmberWebUI/SEB/ScheduleExecutionBoard.aspx][ScheduleExecutionBoard.aspx]] |
|
||
| WebMethod + page properties | [[file:d:/_dev/WebPortal/src/code/AmberWebUI/SEB/ScheduleExecutionBoard.aspx.cs][ScheduleExecutionBoard.aspx.cs]] |
|
||
| Session storage | [[file:d:/_dev/WebPortal/src/code/AmberWebUI/SEB/SEBSessionState.cs][SEBSessionState.cs]] |
|
||
|
||
- *Endpoint:* ASP.NET =[WebMethod]= =SaveSearchCriteriaToSession= on =ScheduleExecutionBoard.aspx=
|
||
- *Parameters:* JSON fields =date=, =orderID=, =time= (also =searchID=, =hours=, =quickSearch= in the same flow)
|
||
- *Host (pentest):* =cert.microlise.com=, path =/PENTEST/TMCWebPortal/SEB/...=
|
||
|
||
*** Attack flow (as tested)
|
||
|
||
#+begin_src mermaid
|
||
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
|
||
#+end_src
|
||
|
||
1. *Save:* User (or tester) POSTs JSON to =SaveSearchCriteriaToSession=. The app saves search criteria into the *server-side session*.
|
||
2. *Render:* On the next load of =ScheduleExecutionBoard.aspx=, those values are written into the HTML *inside a =<script>= block*, as JavaScript string literals.
|
||
3. *Bug:* Values are inserted *without JavaScript string encoding*. A crafted =date= can *break out of the string* and run arbitrary JS.
|
||
4. *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*
|
||
|
||
#+begin_src csharp
|
||
// 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;
|
||
// ...
|
||
}
|
||
#+end_src
|
||
|
||
*2. Persist — per-user ASP.NET session*
|
||
|
||
[[file:d:/_dev/WebPortal/src/code/AmberWebUI/SEB/SEBSessionState.cs][SEBSessionState.cs]] stores values under keys =dateID=, =timeID=, =orderByID=.
|
||
|
||
*3. Load — on next full page GET*
|
||
|
||
#+begin_src csharp
|
||
// ScheduleExecutionBoard.aspx.cs lines 267-277
|
||
private void SetupControls()
|
||
{
|
||
var sebState = new SEBSessionState();
|
||
SessionOrderID = sebState.OrderBy;
|
||
SessionDate = sebState.SearchDate;
|
||
SessionTime = sebState.SearchTime;
|
||
// ...
|
||
}
|
||
#+end_src
|
||
|
||
*4. Render — vulnerable inline JavaScript (root cause)*
|
||
|
||
#+begin_src javascript
|
||
// ScheduleExecutionBoard.aspx lines 1853-1875
|
||
if ("<%=SessionDate%>") {
|
||
$('#txtStart').val("<%= SessionDate %>");
|
||
}
|
||
if ("<%=SessionTime%>") {
|
||
$('#inputtime').val("<%=SessionTime%>");
|
||
}
|
||
if ("<%=SessionOrderID%>") {
|
||
$(orderBySelector + ' option[value="<%=SessionOrderID%>"]').attr('selected', 'selected');
|
||
}
|
||
#+end_src
|
||
|
||
Example payload in session: ="); alert(document.domain); //=
|
||
|
||
#+begin_src javascript
|
||
$('#txtStart').val(""); alert(document.domain); //");
|
||
#+end_src
|
||
|
||
*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)
|
||
|
||
#+begin_src mermaid
|
||
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
|
||
#+end_src
|
||
|
||
1. WebMethod returns HTTP 200.
|
||
2. Payload never went through =DateValidation()=.
|
||
3. Full page reload → JS runs (e.g. =alert=).
|
||
4. View Source: payload inside double-quoted JS string, unescaped.
|
||
|
||
*Self-XSS:* only your session is poisoned.
|
||
|
||
*** Step-by-step reproduction
|
||
|
||
*Step 1 — Baseline (optional)*
|
||
|
||
1. Open SEB, perform a search.
|
||
2. DevTools → Network → =SaveSearchCriteriaToSession=.
|
||
3. 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):
|
||
|
||
#+begin_src javascript
|
||
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));
|
||
#+end_src
|
||
|
||
*curl* (replace host, path, cookies):
|
||
|
||
#+begin_src bash
|
||
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\":\"\"}"
|
||
#+end_src
|
||
|
||
*Step 3 — Trigger render (stored XSS)*
|
||
|
||
1. Full navigation reload of =ScheduleExecutionBoard.aspx= (F5).
|
||
2. =SetupControls()= embeds session =date= (~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)
|
||
|
||
1. Request (POST body with payload).
|
||
2. Screenshot of alert (before) or no alert (after).
|
||
3. View Source snippet around =$('#txtStart').val(=.
|
||
4. 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: [[file:d:/_dev/WebPortal/src/code/AmberWebUI/SEB/ScheduleExecutionBoard.aspx][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:*
|
||
|
||
#+begin_src javascript
|
||
$('#txtStart').val("<%= SessionDate %>");
|
||
#+end_src
|
||
|
||
*After:*
|
||
|
||
#+begin_src javascript
|
||
$('#txtStart').val("<%= HttpUtility.JavaScriptStringEncode(SessionDate ?? string.Empty) %>");
|
||
#+end_src
|
||
|
||
Encode =if= guards too, or use code-behind booleans (=HasSessionDate=).
|
||
|
||
*** Fix 2 — Server-side input validation
|
||
|
||
File: [[file:d:/_dev/WebPortal/src/code/AmberWebUI/SEB/ScheduleExecutionBoard.aspx.cs][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 |
|
||
|
||
#+begin_src csharp
|
||
[WebMethod]
|
||
public static void SaveSearchCriteriaToSession(...)
|
||
{
|
||
if (!IsValidSebDate(date) || !IsValidSebTime(time)
|
||
|| !IsValidQueryComponentId(orderID) || !IsValidQueryComponentId(searchID))
|
||
{
|
||
return;
|
||
}
|
||
var sebState = new SEBSessionState();
|
||
// ...
|
||
}
|
||
#+end_src
|
||
|
||
*Date regex:*
|
||
|
||
#+begin_src
|
||
^[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)))$
|
||
#+end_src
|
||
|
||
*** Fix 3 — What not to do
|
||
|
||
- Do not rely on client =DateValidation()= alone.
|
||
- Do not use =HtmlEncode= in JS string literals.
|
||
- Do not use =innerHTML=; keep =.val()=.
|
||
|
||
*** Fix 4 — Verification / test plan
|
||
|
||
Master procedure: *Replicating the vulnerability* section above.
|
||
|
||
*Negative test:*
|
||
|
||
#+begin_src json
|
||
{
|
||
"searchID": "0.X",
|
||
"orderID": "0.X",
|
||
"date": "\"); alert(1);//",
|
||
"time": "00:00",
|
||
"hours": 24,
|
||
"displayPriorityJourneys": false,
|
||
"quickSearch": ""
|
||
}
|
||
#+end_src
|
||
|
||
*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 |
|
||
|--------------------------------+-----------------------------------------------|
|
||
| [[file:d:/_dev/WebPortal/src/code/AmberWebUI/SEB/ScheduleExecutionBoard.aspx][ScheduleExecutionBoard.aspx]] | =JavaScriptStringEncode= in =SetupControls()= |
|
||
| [[file:d:/_dev/WebPortal/src/code/AmberWebUI/SEB/ScheduleExecutionBoard.aspx.cs][ScheduleExecutionBoard.aspx.cs]] | Validation in =SaveSearchCriteriaToSession= |
|
||
|
||
*References:* [[https://owasp.org/www-community/attacks/xss/][OWASP XSS]], [[https://portswigger.net/web-security/cross-site-scripting/stored][PortSwigger Stored XSS]], [[https://learn.microsoft.com/en-us/dotnet/api/system.web.httputility.javascriptstringencode][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
|
||
|
||
1. *Reproduce* on cert/UAT (=replicate-vuln= todo).
|
||
2. *Implement* fixes in =D:\_dev\WebPortal= (=remediate-encode=, =remediate-validate=).
|
||
3. *Replay* steps and fill comparison matrix (=remediate-retest= todo).
|
||
|
||
Confirm execution when ready to change WebPortal code.
|