
SureForms <= 2.2.0 - Unauthenticated Stored Cross-Site Scripting
A critical Stored XSS vulnerability exists in the SureForms plugin. The vulnerability stems from an insecure client-side implementation in entries.js where user input is programmatically decoded (reversing server-side sanitization) and then rendered using dangerouslySetInnerHTML without proper client-side sanitization. This allows unauthenticated attackers to inject malicious JavaScript payloads via standard form fields, bypassing WordPress's default filters.
wp_ksesTo verify if a target site is running SureForms, inspect the translation file header which typically contains the version number.
Target URL:
http://target-site.com/wp-content/plugins/sureforms/languages/sureforms.pot
Verification Command:
curl -s "http://target-site.com/wp-content/plugins/sureforms/languages/sureforms.pot" | grep "Project-Id-Version"
The vulnerability resides in the React-based admin interface responsible for viewing form entries (entries.js).
In the minified entries.js file, there is a helper function (identified as Xv in the analyzed build) designed to handle HTML entities.
Code Logic (Reconstructed):
// Function Xv: Pre-processing field values
var Xv = function(input) {
var value = input.value;
// VULNERABILITY ROOT CAUSE:
// If the string contains HTML entities (e.g., <), create a textarea,
// inject the content, and extract the value.
// This effectively DECODES HTML entities back to raw HTML tags.
// Example: "<img ...>" becomes ""
if (typeof value === "string" && value.match(/&[a-zA-Z0-9#]+;/)) {
var textarea = document.createElement("textarea");
textarea.innerHTML = value;
// Logic to revert sanitization
if (textarea.value.includes("<") || textarea.value.includes(">")) {
value = textarea.value; // Now 'value' contains RAW HTML
}
}
return { ...input, value: value };
}
Analysis: This function defeats backend security. Even if WordPress correctly stores <script> as <script> in the database, this function converts it back to <script> immediately after fetching it from the API.
After decoding, the data is passed to the rendering component (identified as Jv).
Code Logic (Reconstructed):
// Function Jv: Rendering the field
var Jv = function(props) {
var field = props.field;
var val = field.value; // This value is now raw HTML (post-decoding)
// THE TRIGGER: Check if the string looks like HTML
if (typeof val === "string" && val.match(/<[^>]+>/g)) {
// THE SINK: Render using dangerouslySetInnerHTML WITHOUT client-side sanitization (e.g., DOMPurify)
return React.createElement("span", {
dangerouslySetInnerHTML: { __html: val }
});
}
// Safe render for non-HTML text
return React.createElement("span", null, val);
}
Injection: An unauthenticated attacker submits a form on the frontend. Instead of using raw HTML tags (which would be stripped/sanitized by WordPress backend), the attacker sends HTML Entities.
Storage: WordPress sees the input (e.g., <img...) as safe text and stores it in the database.
Execution (The Trap):
Xv function detects the entities and decodes <img... back to ` Entries.Click on the entry submitted in Step 1 to view details.
Result: The browser will execute the JavaScript payload (alert('XSS_SUREFORMS')).