CVE-2026-4378 — Stored XSS — shaggy

In an e-commerce platform widely used in Türkiye, the adi parameter on the profile update endpoint allows stored XSS. The payload is rendered both in the storefront and in the member list in the admin panel, so it reaches a shop administrator’s session from a customer account.

I do not name the vendor in this writeup, the official references are below. I did not leave the screenshots untouched either, I masked third-party email addresses and phone numbers.

Vulnerability: CWE-79 Stored XSS · Version: 4.5.001 · CVE: CVE-2026-4378

Coordinated disclosure through USOM, Türkiye’s national CERT. CVE assigned 18 March 2026, published 28 August 2026 as advisory TR-26-0944 . As of publication, it has not been verified whether the vulnerability has been fixed.


The environment

October 2025. I filled in the demo request form on the vendor’s site and they opened me an account. The access covered both a customer account on the storefront and the admin panel.

The server returns Server: Microsoft-IIS/10.0 and X-Powered-By: ASP.NET, and the endpoints carry .asp extensions. So this is classic ASP, not ASP.NET. The bottom left corner of the admin panel showed the version: 4.5.001. I took a screenshot of it.

After logging in, the cookie jar looked like this (trimmed):

1indirim=0; lang=1; at=87260779dba97bdb6554cd7ca39f6cb614; notification=0;
2customerlastname=KURTMAN; customername=KORAY; kad=demo%40[redacted]%2Ecom;
3idY=6; statusY=1; HttpOnly; ASPSESSIONIDQGBSRRRA=PKEHGPBDDNHAEHCBJJDAOFOL;
4usertype=user; uyeliksizIslem=; uname=test+test; kid=470756;
5username=testtest%40hotmail%2Ecom; KutuGorunumu=katalog; posetistemiyorum=1;
6chkohash=dGVtcHx8dGVtcHx8fHx8fHx8...; gilce=2000746000; gil=ADIYAMAN;
7ccek=True; ccek2=True; sipno=AT%2D000000100653; sipbildirim=0

Three things caught my attention here:

usertype=user is a role indicator held client side. If server decisions rest on it, that is a privilege escalation surface. I did not try it with a value like admin, I did not want to attempt privilege escalation on a demo environment.

There is a cookie named HttpOnly. That is its name. It has no value. Where it comes from is visible in the response headers, I will get to it shortly.

chkohash looks base64 and decodes to a pipe-delimited structure along the lines of temp||temp||...||350||1901011010 1||1||. Serialized cart or session data with no visible signature. I left that alone too.

Where I looked

In products like this I go for the fields where a user can write free text, because that data is rendered both in the customer interface and in the admin panel, and the two sides are usually built with separate templates. Encoding on one side does not commit the other.

Places I walked: account details, address book, order note, order history, product review. I put the payload in the name field on the account details page first, because the name sits permanently in the storefront’s top bar and is likely to land in member lists in the admin panel.

The adi parameter

Hitting save sends a POST to /usrinf.asp, with the name field carried in a parameter called adi. I put this in the field:

1<img src=x onmouseover=prompt(1)>

I picked <img> over <script> because where the data is written through innerHTML a <script> tag will not run anyway, and that produces a false negative. I picked onmouseover over onerror because I was in a shared demo environment with other demo users and company staff in it. I did not want to leave a stored payload that fires on its own. prompt instead of alert, so I could tell my own trace apart from anyone who walked through the same environment before me.

POST /usrinf.asp HTTP/2
Host: demo.[redacted].com
Origin: https://demo.[redacted].com
Referer: https://demo.[redacted].com/usrinf.asp
Content-Type: application/x-www-form-urlencoded
Content-Length: 105

adi=%3Cimg+src%3Dx+onmouseover%3Dprompt%281%29%3E&soyadi=test&tel=4545455555&action=update&sifre=&sifre2=

Note that the form sends sifre and sifre2, the password fields, empty alongside the profile update. There is no field asking for the current password. I did not examine this under a separate heading, but a password change being possible in the same request is worth a look in a CSRF or session takeover scenario.

The response:

HTTP/2 200 OK
Server: Microsoft-IIS/10.0
X-Powered-By: ASP.NET
Set-Cookie: uname=%3Cimg+src%3Dx+onmouseover%3Dprompt%281%29%3E+test; path=/
Set-Cookie: HttpOnly; Secure
Strict-Transport-Security: max-age=31536000; includeSubDomains
X-Frame-Options: SAMEORIGIN
X-Content-Type-Options: nosniff
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: geolocation=(), microphone=(), camera=()
Content-Length: 73200

Request and response for the manipulated adi parameter

200 OK. The record was accepted, no filtering or length limit visible. I did not try the soyadi and tel fields in the same form. They most likely behave the same way, but I cannot claim it without testing.

Other things that stood out in the response

The second Set-Cookie line is malformed. Set-Cookie: HttpOnly; Secure is not a valid cookie definition, there is no name/value pair. The browser stores it as a valueless cookie named HttpOnly, which is why it shows up in the request jar. Some piece of code trying to append flags is probably emitting them wrong. The real consequence is this: the code that is supposed to add those flags is not adding them to the uname cookie on the line above.

The payload is written into the uname cookie, and there is no HttpOnly flag. The cookie is set with path=/. As it stands, JavaScript running on the same origin can read it. I did not report this as a separate finding, I note it as a detail that increases the impact alongside the XSS.

CSP is the only missing security header. HSTS, X-Frame-Options, nosniff, Referrer-Policy and Permissions-Policy are there. Content-Security-Policy is not. I would not draw a firm conclusion about the team’s engineering culture from this, but as an observation: those five headers can be added at the server layer without touching code, while output encoding and a usable CSP require going into the templates. I think that distinction is a signal worth using when deciding where to look.

There is a broken character in the page title: the <title> reads à yelik Bilgilerim where it should read Üyelik Bilgilerim. Even though <meta charset="UTF-8"> is declared, the content is produced in a different encoding. Charset confusion is sometimes useful for filter evasion, though it was not needed here.

The meta tags carry values left over from another shop. The og:site_name, og:title and og:description fields belong not to this shop but to a different site on the same platform. It shows that templates are not fully separated between tenants. I did not chase this.

The CSS is pulled from //satis.[redacted].com/v3-cdn/..., over a protocol-relative path.

The second render point

After saving I went back to the storefront. In the account menu on the top bar, where my name should have been, there was a broken image icon, and hovering over it ran the payload:

JavaScript executing in the user’s browser

As it stands this is self-XSS. My own account, my own browser. Nothing to report, because it affects nobody. The payload has one write point but its read points can be elsewhere, so I kept walking the other screens where the same data appears.

In the admin panel, when I opened the member list under the Üye (Members) menu (/yonetimV2/3100), the payload ran there too:

Admin panel member list, payload executing

In the list my row is rendered as a broken image in the name column. When the administrator moves a cursor across that row, the JavaScript runs in their session. Because the payload is stored server side, the victim does not need to click any link, it fires during their routine work. And it moves from a customer account into an administrator session. Had it been a line that exfiltrated session data instead of prompt(1), the result would have been session takeover in the shop panel.

There is a limit on the verification, and I wrote it this way in the report too: the demo account I used gave me access to the admin panel, which is how I could see the second render point myself. In a real deployment somebody else opens that panel. The impact stays the same, but it would be an inference rather than my direct observation.

By this point the application had left me a few “I’ll come back to that” items besides the XSS: chkohash, the password fields riding empty, and render points I never walked such as order notes, invoice output and CSV exports. Then the environment was shut down and I got to none of them. Taking the version screenshot on day one turned out to be lucky, the rest of the case rested on it.

The disclosure process

October 2025. I sent the finding to USOM with screenshots. The reply said no action would be taken, on the grounds that the vulnerability was found in a demo environment:

“No action is taken on vulnerabilities found in pre-release versions or test (demo) environments of the software in question.”

The policy itself is defensible. Issuing a CVE for half-finished code a company pushed to a test server protects nobody.

March 2026. In my objection I argued the fact the policy was applied to, not the policy. On the vendor’s own software updates page, the latest published version was listed as 4.4.004. The demo environment where I found the bug was running 4.5.001, and I had the screenshot for it.

So the demo was newer than the version the vendor shipped to its customers. Not a pre-release, but the code due to reach customers with the next update. And the vulnerability came from the output of adi not being encoded, not from any environment-specific configuration. In my second email I gave the version history link and put the two numbers side by side.

18 March 2026. The CVE was assigned the same day:

CVE-2026-4378 has been assigned for the CWE-79 … vulnerability. The CVE content will be filled in and announced following remediation of the vulnerability.”

Retest. For the CVE to be published, the vulnerability had to be closed. When I tried to get into the environment to test again, the demo had been shut down. I do not know whether it is closed today, I could not verify it. I have nothing beyond the screenshots taken in October 2025.

28 August 2026. The CVE was published.

The “Solution” field in the advisory

The Solution section of the advisory

“Due to the vulnerability not being remediated by the vendor, the Cyber Security Directorate recommends that users of the product use an equivalent alternative application.”

The solution field of a security advisory normally holds a version number or a configuration change. Here it holds advice to drop the product and move to another one. There is not much to say technically: the XSS was reported, the authority followed it for ten months, no patch shipped, and this was the only advice left. Possibly the most expensive workaround thirty characters in a name field can produce.

For shops running the product the practical outcome is this: there is no patch, but the vulnerability is now public, and changing platforms means data migration and integration cost for a small business. I do not have a clean proposal for how to fix this. The one thing I can say is that what was missing here was not code quality. XSS comes out of the code of companies of every size in every country. What sets this case apart is that the vendor never answering cost nobody anything.

Remediation

The vulnerability comes from missing output encoding. On the classic ASP side:

<%= Server.HTMLEncode(rs("adi")) %>

The encoding belongs on the output side rather than the input side, because the same data is rendered in at least two separate templates and sanitising on save does nothing for the rows already sitting in the database.

Alongside it:

  • Content-Security-Policy. Depending on how the application loads its JavaScript, a policy restricting inline event handlers could prevent this payload from running. If inline script is widespread in the current code base, rolling out such a policy is not a one-line change and needs to be staged.
  • Admin panel lists. Administrator screens rendering user data should follow the same encoding discipline as the customer interface.
  • Cookie flags. The malformed Set-Cookie: HttpOnly; Secure line should be fixed and the flags attached to the definitions of cookies like uname.

Closing

Technically there is nothing new here, user input is rendered unencoded in two separate interfaces. What is left unfinished on my side is that I never got to see the finding closed, so the report still stands half done.

Next time I will start from the same place. A demo request, a name field, thirty characters. Until I get to that part, goodbye. :)