
LimeSurvey Community Edition 7.0.14 - Reflected XSS in participant CSV import result via invalid attribute column name
7.4
High
Discovered by
Offensive Team, Fluid Attacks
Summary
Full name
LimeSurvey Community Edition 7.0.14 - Reflected XSS in participant CSV import result via invalid attribute column name
Code name
State
Public
Release date
Affected product
LimeSurvey
Vendor
LimeSurvey
Affected version(s)
7.0.14
Fixed version(s)
7.1.2
Vulnerability name
Reflected cross-site scripting (XSS)
Vulnerability type
Remotely exploitable
Yes
CVSS v4.0 vector string
CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:A/VC:H/VI:H/VA:N/SC:L/SI:L/SA:N
CVSS v4.0 base score
7.4
Exploit available
Yes
CVE ID(s)
Description
LimeSurvey contains a reflected cross-site scripting vulnerability in the administrative survey-participant CSV import result page. An external attacker who has no LimeSurvey account can craft a malicious participant-import CSV file and rely on social engineering to have an authenticated user with tokens:import permission import it with "Display attribute warnings" enabled. The injected markup is echoed back unescaped in the very same HTTP response and executes JavaScript in the victim's authenticated administrative session. If the victim already holds the superadmin permission, the demonstrated payload rides that session to create a brand-new, attacker-controlled superadministrator account, fully compromising the application.
Vulnerability
Root Cause
The uploaded participant CSV is only validated on its .csv extension (application/controllers/admin/Tokens.php:2342); the content itself is never validated or sanitized as HTML.
The header line is parsed with str_getcsv() (Tokens.php:2394) and each column name is trim()-ed (Tokens.php:2395), then passed through what looks like a cleanup step but is not an HTML sanitizer (Tokens.php:2399):
This pattern only strips a trailing " <...>" annotation — the classic "Display Name <address>" mail-header idiom — and only when it is preceded by a literal space and sits at the very end of the string. It has no notion of HTML tags, attributes, or event handlers, and it does not fire at all when the payload doesn't match that exact "space + angle-bracket-block-at-end" shape — which is the case for a column such as attribute_<img src=x onerror=...>, since there is no space before <img. The value is never passed through CHtml::encode(), htmlspecialchars(), or any HTML purifier.
Any column that merely contains the substring attribute_ but does not match a name actually configured for the survey ($aAttrFieldNames, from getAttributeFieldNames($iSurveyId), Tokens.php:2326) is classified as an "invalid attribute" and stored verbatim, unmodified — classifying it as invalid does not cause rejection or encoding, only routing into a warning list (Tokens.php:2408-2409):
This branch only runs when the request includes showwarningtoken=1, i.e. when the importing user ticks the standard, documented "Display attribute warnings" checkbox on the upload form (application/views/admin/token/csvupload.php:88-91) — not a hidden or unsupported configuration.
The sink is application/views/admin/token/csvimportresult.php:146-148, where the collected values are echoed with no output encoding of any kind:
Confirmed source-to-sink path
Source — attacker-controlled CSV header row, uploaded via
POST /index.php/admin/tokens/sa/import/surveyid/<id>(Tokens.php:2342, only.csvextension is checked).Parse —
str_getcsv()splits the header row;array_map('trim', ...)trims each name (Tokens.php:2394-2395).Ineffective transform —
preg_replace("/(.*) <[^,]*>$/", "$1", $sFieldname)(Tokens.php:2399) does not match/strip the payload (no leading space before<img).Classification —
strpos(..., 'attribute_') !== false and !in_array(..., $aAttrFieldNames) and getPost('showwarningtoken')routes the untouched value into$aInvalideAttrFieldName[](Tokens.php:2408-2409).Handoff to view —
$aData['aInvalideAttrFieldName'] = $aInvalideAttrFieldName;(Tokens.php:2588).Sink —
<li><?php echo $sModelError; ?></li>(application/views/admin/token/csvimportresult.php:147), rendered with zero output encoding in the HTTP response returned for the same import request.
Impact
The injected JavaScript executes in LimeSurvey's administrative origin, in the authenticated session of whoever performed the import — it can read page content and anti-CSRF tokens and submit any administrative request available to that victim.
This is session-riding, not a flaw in the permission model itself: the PoC payload calls userManagement/applyedit and userManagement/saveUserPermissions (application/controllers/UserManagementController.php:132,657). applyPermissionFromArray() (UserManagementController.php:1826-1866) filters the requested permission grants against the caller's own current global permissions — a request to grant superadmin is only honored if the acting session already has superadmin:create. Consequently:
If the victim is a superadministrator, the demonstrated payload creates a new, fully-privileged, attacker-controlled superadmin account, compromising all application data and configuration.
If the victim only holds
tokens:import(no elevated global permissions), the same JavaScript still executes with full read/write access to whatever that victim's session can reach, but the account-takeover step is capped by the victim's own permissions.
PoC
Preconditions
LimeSurvey Community Edition
7.0.14+260904deployed per vendor-standard Docker install, available athttp://127.0.0.1:8081.The victim has an authenticated LimeSurvey session with, at minimum,
survey:readandtokens:importfor the target survey.The external attacker who crafts and delivers the CSV needs no LimeSurvey account and no application permission.
Highest impact requires the victim to already hold the global
superadminpermission.
Step-by-step
Craft the malicious CSV (
import_xss.csv) — a standard 3-mandatory-field participant file plus one attacker-controlledattribute_*header:The 4th column is treated as an invalid participant attribute; because the malicious content contains no space directly before
<img, the "cleanup" regex atTokens.php:2399never matches, so the tag survives intact.Victim imports the file: sign in as the victim (
http://127.0.0.1:8081/index.php/admin) → create/open a survey → Participants → Add… → Import participants from: CSV file → selectimport_xss.csv→ set Display attribute warnings: On → Upload.Result: the response page (
csvimportresult.php) renders the<img onerror=...>tag unescaped inside the "invalid attributes" warning list. The browser parses and executes it immediately: it fetches the current CSRF token, POSTs touserManagement/applyeditto createattackerAdmin:attackerAdmin!aA1, POSTs touserManagement/saveUserPermissionsto grant that new user every listed permission (includingsuperadminandauth_db, which only actually take effect if the victim's session already hassuperadmin:create), and setsdocument.titletoTOKEN-CSV-ADMIN:attackerAdminas a success marker.
Evidence of Exploitation
Video of exploitation:
Static evidence:

Our security policy
We have reserved the ID CVE-2026-92730 to refer to this issue from now on.
System Information
LimeSurvey
Version: 7.1.2
Operating System: Any
References
GitHub Repository: https://github.com/LimeSurvey/LimeSurvey/
Patch: https://github.com/LimeSurvey/LimeSurvey/commit/0523de7bbb0282af33a0bf9e08a1fc6333ae6851
Mitigation
An updated version of LimeSurvey is available on the vendor page.
Credits
The vulnerability was discovered by Miguel Gomez from Fluid Attacks' Offensive Team.
Timeline
Vulnerability discovered
Vendor contacted
Vendor replied
Vendor confirmed
Vulnerability patched
Public disclosure
Does your application use this vulnerable software?
During our free trial, our tools assess your application, identify vulnerabilities, and provide recommendations for their remediation.














