Submit #862052: Microweber Microweber CMS v1.2.6 (April 2021) through v2.0.20 (latest release as of 2026-06-17) Cross Site Scripting (Persistent/Stored)info

TitleMicroweber Microweber CMS v1.2.6 (April 2021) through v2.0.20 (latest release as of 2026-06-17) Cross Site Scripting (Persistent/Stored)
DescriptionPRODUCT: Microweber CMS VENDOR: Microweber (https://microweber.com) AFFECTED: v1.2.6 (April 2021) through v2.0.20 (latest release as of 2026-06-17) VULNERABILITY: Stored Cross-Site Scripting (XSS) via Content Tag Names CWE: CWE-79 (Improper Neutralization of Input During Web Page Generation) CVSS 4.0: 6.3 (Medium) VECTOR: CVSS:4.0/AV:N/AC:L/AT:N/PR:H/UI:P/VC:L/VI:L/VA:N/SC:H/SI:H/SA:N RESEARCHER: Theodosis Paidakis REPORTED: 2026-06-17 (vendor notified same date, no response at time of submission) STATUS: Unpatched DESCRIPTION A stored cross-site scripting vulnerability exists in the content tagging component of Microweber CMS. An attacker with administrative credentials can inject arbitrary HTML and JavaScript into a content tag name via a crafted GET request to the tag save endpoint. The injected payload is persisted in the database and executes in the context of any browser that loads two confirmed rendering surfaces: the public-facing blog index page, which fires for unauthenticated visitors, and the admin post editor page, which fires for any administrator who opens a tagged post for editing. The vulnerability is the result of three independent sanitization failures that must be chained together to produce a working payload. Each layer alone would be insufficient to prevent exploitation; together they leave the injection path completely unprotected. TECHNICAL DETAILS Injection Endpoint The tag save endpoint is /api/save_content_admin, which accepts both POST and GET requests. A GET request to this endpoint with a crafted tag_names[] parameter is sufficient to inject the payload: GET /api/save_content_admin?id=1&tag_names[0]=<img src=x onerror=alert(1)> The endpoint requires an authenticated admin session. No CSRF token is required for GET requests. Layer 1 - XSS Middleware Does Not Cover GET Requests Microweber's XSS sanitization middleware (src/MicroweberPackages/App/Http/Middleware/XSS.php) contains an explicit method check that restricts sanitization to POST, PATCH, and PUT requests: if (($request->isMethod('post') or $request->isMethod('patch') or $request->isMethod('put')) and !empty($input)) { // sanitize } Because the tag save endpoint is invoked via GET, the middleware block is never entered. The tag_names parameter is not sanitized at this stage. This bypass dates to the initial introduction of the XSS middleware in v1.0.4 (August 2015) and has never been corrected. Layer 2 - strip_unsafe() Regex Requires Double-Quoted onerror Attribute After the middleware check, Microweber applies a secondary sanitization function, strip_unsafe(), defined in src/MicroweberPackages/Helper/Format.php at line 722. The function attempts to strip onerror event handlers using a regular expression: $data = preg_replace('/onerror="(.*?)"/is', '', $data); The regex matches only the double-quoted form onerror="...". An unquoted event handler attribute (onerror=payload, which is valid HTML5) is not matched and passes through the function unchanged. Layer 3 - Str::title() Displayer Is Bypassed with HTML Decimal Entities The rtconner/laravel-tagging package, which Microweber uses for its tagging system, is configured to apply a displayer function to all tag names before storage. The configured displayer is Illuminate\Support\Str::title, as set in vendor/rtconner/laravel-tagging/config/tagging.php: 'displayer' => '\Illuminate\Support\Str::title', Str::title calls mb_convert_case(MB_CASE_TITLE) internally, which title-cases the first alphabetic character after each word boundary. In this context, punctuation characters including parentheses, dots, apostrophes, and plus signs are treated as word boundaries. A naive payload such as alert(document.domain) becomes Alert(Document.Domain) after processing, breaking the JavaScript. The bypass is to encode all JavaScript as HTML decimal entities. Entity references of the form &#NNN; contain only ASCII digits and the characters &, #, and ;, none of which mb_convert_case treats as alphabetic. The entities pass through Str::title unchanged, and the browser decodes them at parse time before executing the event handler. Spaces within the payload must also be encoded as &#32; because unquoted HTML attributes terminate at literal whitespace. Single-quote characters must be encoded as &#39; for the same reason. In the specific payload used during testing, the string XSS required encoding of the two non-initial S characters as &#83; because Str::title lowercases non-initial letters within a word. Final payload as stored in mw_tagging_tags.name after Str::title processing: <Img Src=X Onerror=&#97;&#108;&#101;&#114;&#116;(&#39;Stored&#32;X&#83;&#83;&#32;|&#32;Microweber&#39;)> The browser decodes the entities, producing: alert('Stored XSS | Microweber') Rendering Surface A - Public Blog Page (No Authentication Required) The tag name is echoed without escaping into the HTML of the public blog index page in userfiles/modules/posts/templates/default.php at line 85: <span class="badge badge-primary"><?php echo $tag; ?></span> The stored payload renders inside a tag badge overlaid on each post card. Any visitor to /blog triggers execution on page load with no interaction required. The payload fires for unauthenticated users and is persistent across sessions. Rendering Surface B - Admin Post Editor The admin post editor at /admin/post/ID/edit loads stored tag data into the page source as an inline JavaScript array. The mw.tags widget, implemented in userfiles/modules/microweber/api/tags.js at line 253, builds tag badge elements using innerHTML: tag_holder.innerHTML = '<span class="' + config.tagBtnClass + '">' + this.dataTitle(config) + '</span>'; The stored tag name is inserted directly into innerHTML, causing the browser to parse and execute the payload on page load. Any administrator who opens the post editor for a post with the malicious tag assigned triggers execution. Out-of-Band Confirmation Execution was confirmed beyond the alert dialog by substituting a fetch()-based payload pointing at a Burp Collaborator endpoint. Visiting /blog as an unauthenticated user caused the browser to issue a DNS lookup and HTTP request to the collaborator server with no user interaction beyond the page load, confirming real network activity from a victim browser. IMPACT The injection requires admin-level credentials, making this a post-authentication vulnerability. The realistic attack path is credential compromise of an existing admin account followed by a single GET request to plant the payload. From that point the XSS is persistent and site-wide: - The payload fires for every unauthenticated visitor to /blog with no interaction required - The payload resides in the database, not in a cookie or session - Session cookies (laravel_session and remember_web) are HttpOnly and SameSite=Lax, preventing direct theft via document.cookie - The practical exploitation path is session riding: the CSRF token is embedded in every Microweber page as a meta tag and is readable via JavaScript. A two-stage payload (a short entity-encoded loader stored in the tag field, loading a full script from an attacker-controlled server via a dynamic import()) allows arbitrary authenticated API calls on behalf of any visitor, including creating backdoor accounts, modifying content, or performing any admin-level operation if the visitor triggering the XSS is an administrator - The tag name field is varchar(125), constraining inline payloads, but the two-stage loader approach fits within this limit at approximately 74 characters using an IP address for the external host (IP addresses contain no alphabetic characters and require no encoding to survive Str::title processing) REMEDIATION The root cause is a combination of missing input method coverage in the XSS middleware and incomplete regex-based sanitization. The recommended fixes are: 1. Extend XSS.php to process GET request parameters, or validate and sanitize the tag_names parameter at the controller or model layer regardless of HTTP method. 2. Fix strip_unsafe() to handle both quoted and unquoted event handler attribute forms, or replace the regex with an HTML parser capable of identifying and stripping event handler attributes in all valid forms. 3. At each rendering site, apply output escaping: use htmlspecialchars($tag, ENT_QUOTES, 'UTF-8') in default.php and use textContent instead of innerHTML in tags.js. 4. At the storage layer, enforce an allowlist on tag name characters (alphanumeric, spaces, hyphens) to prevent structured HTML from entering the database at all. NOTE: No public disclosure yet, waiting for maintainer confirmation, but project not updated in 10 months and other researchers are getting ghosted from what I see online.
Source⚠️ https://github.com/microweber/microweber
User
 theodosis (UID 99066)
Submission06/17/2026 17:53 (2 months ago)
Moderation08/05/2026 18:02 (2 months later)
StatusDuplicate
VulDB entry385705 [Microweber CMS up to 2.0.20 Content Tagging System /api/save_content_admin strip_unsafe tag_names cross site scripting]
Points0

Do you know our Splunk app?

Download it now for free!