Moodle: moodle: cross-site scripting vulnerability via inadequate input filtering in formula editor
Description
A flaw was found in moodle. This vulnerability, known as Cross-Site Scripting (XSS), occurs due to insufficient checks on user-provided data in the formula editor's arithmetic expression fields. A remote attacker could inject malicious code into these fields. When other users view these expressions, the malicious code would execute in their web browsers, potentially compromising their data or leading to unauthorized actions.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Moodle's formula editor fails to sanitize arithmetic expression input, allowing stored XSS that executes when other users view the expressions.
Vulnerability
Details CVE-2025-67850 is a stored Cross-Site Scripting (XSS) vulnerability in Moodle's formula editor. The root cause is insufficient validation of user-provided data in the arithmetic expression fields of the tiny_equation plugin [1][2]. This allows an attacker to inject arbitrary HTML or JavaScript code that is stored and later rendered to other users.
Exploitation
A remote attacker can inject malicious code into the formula editor's arithmetic expression fields without requiring special privileges beyond the ability to create or edit content that uses the equation editor [1][2]. When other users view the affected expressions, the injected code executes in their web browsers, enabling the attacker to perform actions on their behalf or steal sensitive data.
Impact
Successful exploitation can lead to compromise of user data, session hijacking, or unauthorized actions within the Moodle instance [1][2]. The XSS is stored, meaning every user who views the crafted expression is affected, amplifying the potential damage.
Mitigation
The Moodle project has addressed this vulnerability in commit c85f153, which sanitizes equation preview input [3]. Administrators should update Moodle to a version that includes this fix (tracked as MDL-85557). No workaround is documented; upgrading is the recommended action.
AI Insight generated on May 19, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
moodle/moodlePackagist | < 4.1.22 | 4.1.22 |
moodle/moodlePackagist | >= 4.4.0-beta, < 4.4.12 | 4.4.12 |
moodle/moodlePackagist | >= 4.5.0-beta, < 4.5.8 | 4.5.8 |
moodle/moodlePackagist | >= 5.0.0-beta, < 5.0.4 | 5.0.4 |
moodle/moodlePackagist | >= 5.1.0-beta, < 5.1.1 | 5.1.1 |
Affected products
2Patches
1c85f153068a7MDL-85557 tiny_equation: Sanitise equation preview input
7 files changed · +21 −10
public/lib/editor/tiny/plugins/equation/amd/build/repository.min.js+1 −1 modified@@ -5,6 +5,6 @@ define("tiny_equation/repository",["exports","core/ajax"],(function(_exports,_aj * @module tiny_equation/repository * @copyright 2022 Huong Nguyen <huongnv13@gmail.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.filterEquation=void 0,_ajax=(obj=_ajax)&&obj.__esModule?obj:{default:obj};_exports.filterEquation=(contextId,content)=>{const request={methodname:"tiny_equation_filter",args:{contextid:contextId,content:content}};return _ajax.default.call([request])[0]}})); + */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.filterEquation=void 0,_ajax=(obj=_ajax)&&obj.__esModule?obj:{default:obj};_exports.filterEquation=function(contextId,content){let stripTags=arguments.length>2&&void 0!==arguments[2]&&arguments[2];const request={methodname:"tiny_equation_filter",args:{contextid:contextId,content:content,striptags:stripTags}};return _ajax.default.call([request])[0]}})); //# sourceMappingURL=repository.min.js.map \ No newline at end of file
public/lib/editor/tiny/plugins/equation/amd/build/repository.min.js.map+1 −1 modified@@ -1 +1 @@ -{"version":3,"file":"repository.min.js","sources":["../src/repository.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * A javascript module to handle TinyMCE Equation ajax actions.\n *\n * @module tiny_equation/repository\n * @copyright 2022 Huong Nguyen <huongnv13@gmail.com>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nimport Ajax from 'core/ajax';\n\n/**\n * Filter the equation for given content.\n *\n * @param {Number} contextId The context id\n * @param {String} content Content to filter\n * @return {promise}\n */\nexport const filterEquation = (contextId, content) => {\n const request = {\n methodname: 'tiny_equation_filter',\n args: {\n contextid: contextId,\n content: content,\n }\n };\n\n return Ajax.call([request])[0];\n};\n"],"names":["contextId","content","request","methodname","args","contextid","Ajax","call"],"mappings":";;;;;;;wKA+B8B,CAACA,UAAWC,iBAChCC,QAAU,CACZC,WAAY,uBACZC,KAAM,CACFC,UAAWL,UACXC,QAASA,iBAIVK,cAAKC,KAAK,CAACL,UAAU"} \ No newline at end of file +{"version":3,"file":"repository.min.js","sources":["../src/repository.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * A javascript module to handle TinyMCE Equation ajax actions.\n *\n * @module tiny_equation/repository\n * @copyright 2022 Huong Nguyen <huongnv13@gmail.com>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nimport Ajax from 'core/ajax';\n\n/**\n * Filter the equation for given content.\n *\n * @param {Number} contextId The context id\n * @param {String} content Content to filter\n * @param {Bool} stripTags Whether to strip HTML tags (optional)\n * @return {promise}\n */\nexport const filterEquation = (contextId, content, stripTags = false) => {\n const request = {\n methodname: 'tiny_equation_filter',\n args: {\n contextid: contextId,\n content: content,\n striptags: stripTags,\n }\n };\n\n return Ajax.call([request])[0];\n};\n"],"names":["contextId","content","stripTags","request","methodname","args","contextid","striptags","Ajax","call"],"mappings":";;;;;;;wKAgC8B,SAACA,UAAWC,aAASC,wEACzCC,QAAU,CACZC,WAAY,uBACZC,KAAM,CACFC,UAAWN,UACXC,QAASA,QACTM,UAAWL,mBAIZM,cAAKC,KAAK,CAACN,UAAU"} \ No newline at end of file
public/lib/editor/tiny/plugins/equation/amd/build/ui.min.js+1 −1 modified@@ -5,6 +5,6 @@ define("tiny_equation/ui",["exports","tiny_equation/modal","core/modal_events"," * @module tiny_equation/ui * @copyright 2022 Huong Nguyen <huongnv13@gmail.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */let currentForm;Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.handleAction=void 0,_modal=_interopRequireDefault(_modal),_modal_events=_interopRequireDefault(_modal_events),TinyEquationRepository=function(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}newObj.default=obj,cache&&cache.set(obj,newObj);return newObj}(TinyEquationRepository),_selectors=_interopRequireDefault(_selectors);let lastCursorPos=0;_exports.handleAction=editor=>{displayDialogue(editor)};const displayDialogue=async editor=>{let data={};const currentEquationData=(0,_equation.getCurrentEquationData)(editor);currentEquationData&&Object.assign(data,currentEquationData);const modal=await _modal.default.create({templateContext:getTemplateContext(editor,data)}),$root=await modal.getRoot(),root=$root[0];currentForm=root.querySelector(_selectors.default.elements.form);const contextId=(0,_options.getContextId)(editor),debouncedPreviewUpdater=(0,_utils.debounce)((()=>updatePreview((0,_options.getContextId)(editor))),500);$root.on(_modal_events.default.shown,(()=>{const library=root.querySelector(_selectors.default.elements.library);TinyEquationRepository.filterEquation(contextId,library.innerHTML).then((async data=>(library.innerHTML=data.content,updatePreview(contextId),notifyFilter(library),data))).catch(_notification.exception)})),root.addEventListener("click",(e=>{const libraryItem=e.target.closest(_selectors.default.elements.libraryItem),submitAction=e.target.closest(_selectors.default.actions.submit),textArea=e.target.closest(".tiny_equation_equation");libraryItem&&(e.preventDefault(),selectLibraryItem(libraryItem,contextId)),submitAction&&(e.preventDefault(),(0,_equation.setEquation)(currentForm,editor),modal.destroy()),textArea&&debouncedPreviewUpdater()})),root.addEventListener("keyup",(e=>{e.target.closest(_selectors.default.elements.equationTextArea)&&debouncedPreviewUpdater()})),root.addEventListener("keydown",(e=>{e.target.closest(_selectors.default.elements.libraryItem)&&(37!=e.keyCode&&39!=e.keyCode||groupNavigation(e))}))},getTemplateContext=(editor,data)=>{const libraries=(0,_options.getLibraries)(editor),texDocsUrl=(0,_options.getTexDocsUrl)(editor);return Object.assign({},{elementid:editor.id,elementidescaped:CSS.escape(editor.id),libraries:libraries,texdocsurl:texDocsUrl,delimiters:_selectors.default.delimiters},data)},selectLibraryItem=(libraryItem,contextId)=>{const tex=libraryItem.getAttribute("data-tex"),input=currentForm.querySelector(_selectors.default.elements.equationTextArea);let oldValue,newValue,focusPoint=0;oldValue=input.value,newValue=oldValue.substring(0,lastCursorPos)," "!==newValue.charAt(newValue.length-1)&&(newValue+=" "),newValue+=tex,focusPoint=newValue.length," "!==oldValue.charAt(lastCursorPos)&&(newValue+=" "),newValue+=oldValue.substring(lastCursorPos,oldValue.length),input.value=newValue,input.focus(),input.selectionStart=input.selectionEnd=focusPoint,updatePreview(contextId)},updatePreview=contextId=>{const textarea=currentForm.querySelector(_selectors.default.elements.equationTextArea),preview=currentForm.querySelector(_selectors.default.elements.preview),cursorLatex=_selectors.default.cursorLatex,isChar=/[a-zA-Z{]/;let currentPos=textarea.selectionStart,equation=textarea.value;for(currentPos||(currentPos=0),(0,_equation.getSourceEquation)()&&(currentPos=equation.length);"\\"===equation.charAt(currentPos)&¤tPos>=0;)currentPos-=1;if(0!==currentPos&&"{"!=equation.charAt(currentPos-1))for(;isChar.test(equation.charAt(currentPos))&¤tPos<equation.length&&isChar.test(equation.charAt(currentPos-1));)currentPos+=1;lastCursorPos=currentPos,equation=""+equation.substring(0,currentPos)+cursorLatex+equation.substring(currentPos),equation=_selectors.default.delimiters.start+" "+equation+" "+_selectors.default.delimiters.end,TinyEquationRepository.filterEquation(contextId,equation).then((data=>(preview.innerHTML=data.content,notifyFilter(preview),data))).catch(_notification.exception)},notifyFilter=element=>{(0,_event.notifyFilterContentUpdated)(element)},groupNavigation=e=>{e.preventDefault();const current=e.target.closest(_selectors.default.elements.libraryItem),parent=current.parentNode,buttons=Array.prototype.slice.call(parent.querySelectorAll(_selectors.default.elements.libraryItem)),direction=37!==e.keyCode?1:-1;let nextButton,index=buttons.indexOf(current);index<0&&(index=0),index+=direction,index<0?index=buttons.length-1:index>=buttons.length&&(index=0),nextButton=buttons[index],nextButton.focus()}})); + */let currentForm;Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.handleAction=void 0,_modal=_interopRequireDefault(_modal),_modal_events=_interopRequireDefault(_modal_events),TinyEquationRepository=function(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}newObj.default=obj,cache&&cache.set(obj,newObj);return newObj}(TinyEquationRepository),_selectors=_interopRequireDefault(_selectors);let lastCursorPos=0;_exports.handleAction=editor=>{displayDialogue(editor)};const displayDialogue=async editor=>{let data={};const currentEquationData=(0,_equation.getCurrentEquationData)(editor);currentEquationData&&Object.assign(data,currentEquationData);const modal=await _modal.default.create({templateContext:getTemplateContext(editor,data)}),$root=await modal.getRoot(),root=$root[0];currentForm=root.querySelector(_selectors.default.elements.form);const contextId=(0,_options.getContextId)(editor),debouncedPreviewUpdater=(0,_utils.debounce)((()=>updatePreview((0,_options.getContextId)(editor))),500);$root.on(_modal_events.default.shown,(()=>{const library=root.querySelector(_selectors.default.elements.library);TinyEquationRepository.filterEquation(contextId,library.innerHTML).then((async data=>(library.innerHTML=data.content,updatePreview(contextId),notifyFilter(library),data))).catch(_notification.exception)})),root.addEventListener("click",(e=>{const libraryItem=e.target.closest(_selectors.default.elements.libraryItem),submitAction=e.target.closest(_selectors.default.actions.submit),textArea=e.target.closest(".tiny_equation_equation");libraryItem&&(e.preventDefault(),selectLibraryItem(libraryItem,contextId)),submitAction&&(e.preventDefault(),(0,_equation.setEquation)(currentForm,editor),modal.destroy()),textArea&&debouncedPreviewUpdater()})),root.addEventListener("keyup",(e=>{e.target.closest(_selectors.default.elements.equationTextArea)&&debouncedPreviewUpdater()})),root.addEventListener("keydown",(e=>{e.target.closest(_selectors.default.elements.libraryItem)&&(37!=e.keyCode&&39!=e.keyCode||groupNavigation(e))}))},getTemplateContext=(editor,data)=>{const libraries=(0,_options.getLibraries)(editor),texDocsUrl=(0,_options.getTexDocsUrl)(editor);return Object.assign({},{elementid:editor.id,elementidescaped:CSS.escape(editor.id),libraries:libraries,texdocsurl:texDocsUrl,delimiters:_selectors.default.delimiters},data)},selectLibraryItem=(libraryItem,contextId)=>{const tex=libraryItem.getAttribute("data-tex"),input=currentForm.querySelector(_selectors.default.elements.equationTextArea);let oldValue,newValue,focusPoint=0;oldValue=input.value,newValue=oldValue.substring(0,lastCursorPos)," "!==newValue.charAt(newValue.length-1)&&(newValue+=" "),newValue+=tex,focusPoint=newValue.length," "!==oldValue.charAt(lastCursorPos)&&(newValue+=" "),newValue+=oldValue.substring(lastCursorPos,oldValue.length),input.value=newValue,input.focus(),input.selectionStart=input.selectionEnd=focusPoint,updatePreview(contextId)},updatePreview=contextId=>{const textarea=currentForm.querySelector(_selectors.default.elements.equationTextArea),preview=currentForm.querySelector(_selectors.default.elements.preview),cursorLatex=_selectors.default.cursorLatex,isChar=/[a-zA-Z{]/;let currentPos=textarea.selectionStart,equation=textarea.value;for(currentPos||(currentPos=0),(0,_equation.getSourceEquation)()&&(currentPos=equation.length);"\\"===equation.charAt(currentPos)&¤tPos>=0;)currentPos-=1;if(0!==currentPos&&"{"!=equation.charAt(currentPos-1))for(;isChar.test(equation.charAt(currentPos))&¤tPos<equation.length&&isChar.test(equation.charAt(currentPos-1));)currentPos+=1;lastCursorPos=currentPos,equation=""+equation.substring(0,currentPos)+cursorLatex+equation.substring(currentPos),equation=_selectors.default.delimiters.start+" "+equation+" "+_selectors.default.delimiters.end,TinyEquationRepository.filterEquation(contextId,equation,!0).then((data=>(preview.innerHTML=data.content,notifyFilter(preview),data))).catch(_notification.exception)},notifyFilter=element=>{(0,_event.notifyFilterContentUpdated)(element)},groupNavigation=e=>{e.preventDefault();const current=e.target.closest(_selectors.default.elements.libraryItem),parent=current.parentNode,buttons=Array.prototype.slice.call(parent.querySelectorAll(_selectors.default.elements.libraryItem)),direction=37!==e.keyCode?1:-1;let nextButton,index=buttons.indexOf(current);index<0&&(index=0),index+=direction,index<0?index=buttons.length-1:index>=buttons.length&&(index=0),nextButton=buttons[index],nextButton.focus()}})); //# sourceMappingURL=ui.min.js.map \ No newline at end of file
public/lib/editor/tiny/plugins/equation/amd/build/ui.min.js.map+1 −1 modified@@ -1 +1 @@ -{"version":3,"file":"ui.min.js","sources":["../src/ui.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * Tiny Equation UI.\n *\n * @module tiny_equation/ui\n * @copyright 2022 Huong Nguyen <huongnv13@gmail.com>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport EquationModal from 'tiny_equation/modal';\nimport ModalEvents from 'core/modal_events';\nimport {getContextId, getLibraries, getTexDocsUrl} from 'tiny_equation/options';\nimport {notifyFilterContentUpdated} from 'core/event';\nimport * as TinyEquationRepository from 'tiny_equation/repository';\nimport {exception as displayException} from 'core/notification';\nimport {debounce} from 'core/utils';\nimport Selectors from 'tiny_equation/selectors';\nimport {getSourceEquation, getCurrentEquationData, setEquation} from 'tiny_equation/equation';\n\nlet currentForm;\nlet lastCursorPos = 0;\n\n/**\n * Handle action\n * @param {TinyMCE} editor\n */\nexport const handleAction = (editor) => {\n displayDialogue(editor);\n};\n\n/**\n * Display the equation editor\n * @param {TinyMCE} editor\n * @returns {Promise<void>}\n */\nconst displayDialogue = async(editor) => {\n let data = {};\n const currentEquationData = getCurrentEquationData(editor);\n if (currentEquationData) {\n Object.assign(data, currentEquationData);\n }\n const modal = await EquationModal.create({\n templateContext: getTemplateContext(editor, data),\n });\n\n const $root = await modal.getRoot();\n const root = $root[0];\n currentForm = root.querySelector(Selectors.elements.form);\n\n const contextId = getContextId(editor);\n const debouncedPreviewUpdater = debounce(() => updatePreview(getContextId(editor)), 500);\n\n $root.on(ModalEvents.shown, () => {\n const library = root.querySelector(Selectors.elements.library);\n TinyEquationRepository.filterEquation(contextId, library.innerHTML).then(async data => {\n library.innerHTML = data.content;\n updatePreview(contextId);\n notifyFilter(library);\n return data;\n }).catch(displayException);\n });\n\n root.addEventListener('click', (e) => {\n const libraryItem = e.target.closest(Selectors.elements.libraryItem);\n const submitAction = e.target.closest(Selectors.actions.submit);\n const textArea = e.target.closest('.tiny_equation_equation');\n if (libraryItem) {\n e.preventDefault();\n selectLibraryItem(libraryItem, contextId);\n }\n if (submitAction) {\n e.preventDefault();\n setEquation(currentForm, editor);\n modal.destroy();\n }\n if (textArea) {\n debouncedPreviewUpdater();\n }\n });\n\n root.addEventListener('keyup', (e) => {\n const textArea = e.target.closest(Selectors.elements.equationTextArea);\n if (textArea) {\n debouncedPreviewUpdater();\n }\n });\n\n root.addEventListener('keydown', (e) => {\n const libraryItem = e.target.closest(Selectors.elements.libraryItem);\n if (libraryItem) {\n if (e.keyCode == 37 || e.keyCode == 39) {\n groupNavigation(e);\n }\n }\n });\n};\n\n/**\n * Get template context.\n * @param {TinyMCE} editor\n * @param {Object} data\n * @returns {Object}\n */\nconst getTemplateContext = (editor, data) => {\n const libraries = getLibraries(editor);\n const texDocsUrl = getTexDocsUrl(editor);\n\n return Object.assign({}, {\n elementid: editor.id,\n elementidescaped: CSS.escape(editor.id),\n libraries: libraries,\n texdocsurl: texDocsUrl,\n delimiters: Selectors.delimiters,\n }, data);\n};\n\n/**\n * Handle select library item.\n * @param {Object} libraryItem\n * @param {number} contextId\n */\nconst selectLibraryItem = (libraryItem, contextId) => {\n const tex = libraryItem.getAttribute('data-tex');\n const input = currentForm.querySelector(Selectors.elements.equationTextArea);\n let oldValue;\n let newValue;\n let focusPoint = 0;\n\n oldValue = input.value;\n\n newValue = oldValue.substring(0, lastCursorPos);\n if (newValue.charAt(newValue.length - 1) !== ' ') {\n newValue += ' ';\n }\n newValue += tex;\n focusPoint = newValue.length;\n\n if (oldValue.charAt(lastCursorPos) !== ' ') {\n newValue += ' ';\n }\n newValue += oldValue.substring(lastCursorPos, oldValue.length);\n\n input.value = newValue;\n input.focus();\n\n input.selectionStart = input.selectionEnd = focusPoint;\n\n updatePreview(contextId);\n};\n\n/**\n * Update the preview section.\n * @param {number} contextId\n */\nconst updatePreview = (contextId) => {\n const textarea = currentForm.querySelector(Selectors.elements.equationTextArea);\n const preview = currentForm.querySelector(Selectors.elements.preview);\n const prefix = '';\n const cursorLatex = Selectors.cursorLatex;\n const isChar = /[a-zA-Z{]/;\n let currentPos = textarea.selectionStart;\n let equation = textarea.value;\n\n // Move the cursor so it does not break expressions.\n // Start at the very beginning.\n if (!currentPos) {\n currentPos = 0;\n }\n\n if (getSourceEquation()) {\n currentPos = equation.length;\n }\n\n // First move back to the beginning of the line.\n while (equation.charAt(currentPos) === '\\\\' && currentPos >= 0) {\n currentPos -= 1;\n }\n if (currentPos !== 0) {\n if (equation.charAt(currentPos - 1) != '{') {\n // Now match to the end of the line.\n while (isChar.test(equation.charAt(currentPos)) &&\n currentPos < equation.length &&\n isChar.test(equation.charAt(currentPos - 1))) {\n currentPos += 1;\n }\n }\n }\n // Save the cursor position - for insertion from the library.\n lastCursorPos = currentPos;\n equation = prefix + equation.substring(0, currentPos) + cursorLatex + equation.substring(currentPos);\n\n equation = Selectors.delimiters.start + ' ' + equation + ' ' + Selectors.delimiters.end;\n TinyEquationRepository.filterEquation(contextId, equation).then((data) => {\n preview.innerHTML = data.content;\n notifyFilter(preview);\n\n return data;\n }).catch(displayException);\n};\n\n/**\n * Notify the filters about the modified nodes\n * @param {Element} element\n */\nconst notifyFilter = (element) => {\n notifyFilterContentUpdated(element);\n};\n\n/**\n * Callback handling the keyboard navigation in the groups of the library.\n * @param {Event} e\n */\nconst groupNavigation = (e) => {\n e.preventDefault();\n\n const current = e.target.closest(Selectors.elements.libraryItem);\n const parent = current.parentNode; // This must be the <div> containing all the buttons of the group.\n const buttons = Array.prototype.slice.call(parent.querySelectorAll(Selectors.elements.libraryItem));\n const direction = e.keyCode !== 37 ? 1 : -1;\n let index = buttons.indexOf(current);\n let nextButton;\n\n if (index < 0) {\n index = 0;\n }\n\n index += direction;\n if (index < 0) {\n index = buttons.length - 1;\n } else if (index >= buttons.length) {\n index = 0;\n }\n nextButton = buttons[index];\n nextButton.focus();\n};\n"],"names":["currentForm","lastCursorPos","editor","displayDialogue","async","data","currentEquationData","Object","assign","modal","EquationModal","create","templateContext","getTemplateContext","$root","getRoot","root","querySelector","Selectors","elements","form","contextId","debouncedPreviewUpdater","updatePreview","on","ModalEvents","shown","library","TinyEquationRepository","filterEquation","innerHTML","then","content","notifyFilter","catch","displayException","addEventListener","e","libraryItem","target","closest","submitAction","actions","submit","textArea","preventDefault","selectLibraryItem","destroy","equationTextArea","keyCode","groupNavigation","libraries","texDocsUrl","elementid","id","elementidescaped","CSS","escape","texdocsurl","delimiters","tex","getAttribute","input","oldValue","newValue","focusPoint","value","substring","charAt","length","focus","selectionStart","selectionEnd","textarea","preview","cursorLatex","isChar","currentPos","equation","test","start","end","element","current","parent","parentNode","buttons","Array","prototype","slice","call","querySelectorAll","direction","nextButton","index","indexOf"],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"ui.min.js","sources":["../src/ui.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * Tiny Equation UI.\n *\n * @module tiny_equation/ui\n * @copyright 2022 Huong Nguyen <huongnv13@gmail.com>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport EquationModal from 'tiny_equation/modal';\nimport ModalEvents from 'core/modal_events';\nimport {getContextId, getLibraries, getTexDocsUrl} from 'tiny_equation/options';\nimport {notifyFilterContentUpdated} from 'core/event';\nimport * as TinyEquationRepository from 'tiny_equation/repository';\nimport {exception as displayException} from 'core/notification';\nimport {debounce} from 'core/utils';\nimport Selectors from 'tiny_equation/selectors';\nimport {getSourceEquation, getCurrentEquationData, setEquation} from 'tiny_equation/equation';\n\nlet currentForm;\nlet lastCursorPos = 0;\n\n/**\n * Handle action\n * @param {TinyMCE} editor\n */\nexport const handleAction = (editor) => {\n displayDialogue(editor);\n};\n\n/**\n * Display the equation editor\n * @param {TinyMCE} editor\n * @returns {Promise<void>}\n */\nconst displayDialogue = async(editor) => {\n let data = {};\n const currentEquationData = getCurrentEquationData(editor);\n if (currentEquationData) {\n Object.assign(data, currentEquationData);\n }\n const modal = await EquationModal.create({\n templateContext: getTemplateContext(editor, data),\n });\n\n const $root = await modal.getRoot();\n const root = $root[0];\n currentForm = root.querySelector(Selectors.elements.form);\n\n const contextId = getContextId(editor);\n const debouncedPreviewUpdater = debounce(() => updatePreview(getContextId(editor)), 500);\n\n $root.on(ModalEvents.shown, () => {\n const library = root.querySelector(Selectors.elements.library);\n TinyEquationRepository.filterEquation(contextId, library.innerHTML).then(async data => {\n library.innerHTML = data.content;\n updatePreview(contextId);\n notifyFilter(library);\n return data;\n }).catch(displayException);\n });\n\n root.addEventListener('click', (e) => {\n const libraryItem = e.target.closest(Selectors.elements.libraryItem);\n const submitAction = e.target.closest(Selectors.actions.submit);\n const textArea = e.target.closest('.tiny_equation_equation');\n if (libraryItem) {\n e.preventDefault();\n selectLibraryItem(libraryItem, contextId);\n }\n if (submitAction) {\n e.preventDefault();\n setEquation(currentForm, editor);\n modal.destroy();\n }\n if (textArea) {\n debouncedPreviewUpdater();\n }\n });\n\n root.addEventListener('keyup', (e) => {\n const textArea = e.target.closest(Selectors.elements.equationTextArea);\n if (textArea) {\n debouncedPreviewUpdater();\n }\n });\n\n root.addEventListener('keydown', (e) => {\n const libraryItem = e.target.closest(Selectors.elements.libraryItem);\n if (libraryItem) {\n if (e.keyCode == 37 || e.keyCode == 39) {\n groupNavigation(e);\n }\n }\n });\n};\n\n/**\n * Get template context.\n * @param {TinyMCE} editor\n * @param {Object} data\n * @returns {Object}\n */\nconst getTemplateContext = (editor, data) => {\n const libraries = getLibraries(editor);\n const texDocsUrl = getTexDocsUrl(editor);\n\n return Object.assign({}, {\n elementid: editor.id,\n elementidescaped: CSS.escape(editor.id),\n libraries: libraries,\n texdocsurl: texDocsUrl,\n delimiters: Selectors.delimiters,\n }, data);\n};\n\n/**\n * Handle select library item.\n * @param {Object} libraryItem\n * @param {number} contextId\n */\nconst selectLibraryItem = (libraryItem, contextId) => {\n const tex = libraryItem.getAttribute('data-tex');\n const input = currentForm.querySelector(Selectors.elements.equationTextArea);\n let oldValue;\n let newValue;\n let focusPoint = 0;\n\n oldValue = input.value;\n\n newValue = oldValue.substring(0, lastCursorPos);\n if (newValue.charAt(newValue.length - 1) !== ' ') {\n newValue += ' ';\n }\n newValue += tex;\n focusPoint = newValue.length;\n\n if (oldValue.charAt(lastCursorPos) !== ' ') {\n newValue += ' ';\n }\n newValue += oldValue.substring(lastCursorPos, oldValue.length);\n\n input.value = newValue;\n input.focus();\n\n input.selectionStart = input.selectionEnd = focusPoint;\n\n updatePreview(contextId);\n};\n\n/**\n * Update the preview section.\n * @param {number} contextId\n */\nconst updatePreview = (contextId) => {\n const textarea = currentForm.querySelector(Selectors.elements.equationTextArea);\n const preview = currentForm.querySelector(Selectors.elements.preview);\n const prefix = '';\n const cursorLatex = Selectors.cursorLatex;\n const isChar = /[a-zA-Z{]/;\n let currentPos = textarea.selectionStart;\n let equation = textarea.value;\n\n // Move the cursor so it does not break expressions.\n // Start at the very beginning.\n if (!currentPos) {\n currentPos = 0;\n }\n\n if (getSourceEquation()) {\n currentPos = equation.length;\n }\n\n // First move back to the beginning of the line.\n while (equation.charAt(currentPos) === '\\\\' && currentPos >= 0) {\n currentPos -= 1;\n }\n if (currentPos !== 0) {\n if (equation.charAt(currentPos - 1) != '{') {\n // Now match to the end of the line.\n while (isChar.test(equation.charAt(currentPos)) &&\n currentPos < equation.length &&\n isChar.test(equation.charAt(currentPos - 1))) {\n currentPos += 1;\n }\n }\n }\n // Save the cursor position - for insertion from the library.\n lastCursorPos = currentPos;\n equation = prefix + equation.substring(0, currentPos) + cursorLatex + equation.substring(currentPos);\n\n equation = Selectors.delimiters.start + ' ' + equation + ' ' + Selectors.delimiters.end;\n TinyEquationRepository.filterEquation(contextId, equation, true).then((data) => {\n preview.innerHTML = data.content;\n notifyFilter(preview);\n\n return data;\n }).catch(displayException);\n};\n\n/**\n * Notify the filters about the modified nodes\n * @param {Element} element\n */\nconst notifyFilter = (element) => {\n notifyFilterContentUpdated(element);\n};\n\n/**\n * Callback handling the keyboard navigation in the groups of the library.\n * @param {Event} e\n */\nconst groupNavigation = (e) => {\n e.preventDefault();\n\n const current = e.target.closest(Selectors.elements.libraryItem);\n const parent = current.parentNode; // This must be the <div> containing all the buttons of the group.\n const buttons = Array.prototype.slice.call(parent.querySelectorAll(Selectors.elements.libraryItem));\n const direction = e.keyCode !== 37 ? 1 : -1;\n let index = buttons.indexOf(current);\n let nextButton;\n\n if (index < 0) {\n index = 0;\n }\n\n index += direction;\n if (index < 0) {\n index = buttons.length - 1;\n } else if (index >= buttons.length) {\n index = 0;\n }\n nextButton = buttons[index];\n nextButton.focus();\n};\n"],"names":["currentForm","lastCursorPos","editor","displayDialogue","async","data","currentEquationData","Object","assign","modal","EquationModal","create","templateContext","getTemplateContext","$root","getRoot","root","querySelector","Selectors","elements","form","contextId","debouncedPreviewUpdater","updatePreview","on","ModalEvents","shown","library","TinyEquationRepository","filterEquation","innerHTML","then","content","notifyFilter","catch","displayException","addEventListener","e","libraryItem","target","closest","submitAction","actions","submit","textArea","preventDefault","selectLibraryItem","destroy","equationTextArea","keyCode","groupNavigation","libraries","texDocsUrl","elementid","id","elementidescaped","CSS","escape","texdocsurl","delimiters","tex","getAttribute","input","oldValue","newValue","focusPoint","value","substring","charAt","length","focus","selectionStart","selectionEnd","textarea","preview","cursorLatex","isChar","currentPos","equation","test","start","end","element","current","parent","parentNode","buttons","Array","prototype","slice","call","querySelectorAll","direction","nextButton","index","indexOf"],"mappings":"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"} \ No newline at end of file
public/lib/editor/tiny/plugins/equation/amd/src/repository.js+3 −1 modified@@ -27,14 +27,16 @@ import Ajax from 'core/ajax'; * * @param {Number} contextId The context id * @param {String} content Content to filter + * @param {Bool} stripTags Whether to strip HTML tags (optional) * @return {promise} */ -export const filterEquation = (contextId, content) => { +export const filterEquation = (contextId, content, stripTags = false) => { const request = { methodname: 'tiny_equation_filter', args: { contextid: contextId, content: content, + striptags: stripTags, } };
public/lib/editor/tiny/plugins/equation/amd/src/ui.js+1 −1 modified@@ -204,7 +204,7 @@ const updatePreview = (contextId) => { equation = prefix + equation.substring(0, currentPos) + cursorLatex + equation.substring(currentPos); equation = Selectors.delimiters.start + ' ' + equation + ' ' + Selectors.delimiters.end; - TinyEquationRepository.filterEquation(contextId, equation).then((data) => { + TinyEquationRepository.filterEquation(contextId, equation, true).then((data) => { preview.innerHTML = data.content; notifyFilter(preview);
public/lib/editor/tiny/plugins/equation/classes/external/filter.php+13 −4 modified@@ -41,7 +41,8 @@ class filter extends external_api { public static function execute_parameters(): external_function_parameters { return new external_function_parameters([ 'contextid' => new external_value(PARAM_INT, 'The context ID', VALUE_REQUIRED), - 'content' => new external_value(PARAM_RAW, 'The equation content', VALUE_REQUIRED) + 'content' => new external_value(PARAM_RAW, 'The equation content', VALUE_REQUIRED), + 'striptags' => new external_value(PARAM_BOOL, 'Whether to strip HTML tags', VALUE_DEFAULT, false), ]); } @@ -50,18 +51,26 @@ public static function execute_parameters(): external_function_parameters { * * @param int $contextid Context ID. * @param string $content Equation content. + * @param string $striptags Strip HTML tags. * @return array * @since Moodle 4.1 */ - public static function execute(int $contextid, string $content): array { + public static function execute(int $contextid, string $content, bool $striptags = false): array { [ 'contextid' => $contextid, - 'content' => $content + 'content' => $content, + 'striptags' => $striptags, ] = self::validate_parameters(self::execute_parameters(), [ 'contextid' => $contextid, - 'content' => $content + 'content' => $content, + 'striptags' => $striptags, ]); + // Strip all HTML tags before filtering the text (avoiding XSS risk). + if ($striptags) { + $content = clean_param($content, PARAM_NOTAGS); + } + $context = context::instance_by_id($contextid); self::validate_context($context); $result = filter_manager::instance()->filter_text($content, $context);
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
6- github.com/advisories/GHSA-6mmv-f6c6-v6q8ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2025-67850ghsaADVISORY
- access.redhat.com/security/cve/CVE-2025-67850ghsavdb-entryx_refsource_REDHATWEB
- bugzilla.redhat.com/show_bug.cgighsaissue-trackingx_refsource_REDHATWEB
- github.com/moodle/moodle/commit/c85f153068a717a3b28bc122e75154bac99e67e1ghsaWEB
- moodle.org/mod/forum/discuss.phpghsaWEB
News mentions
0No linked articles in our index yet.