GHSA-996F-334J-67G7

Vulnerability from github – Published: 2026-07-29 16:30 – Updated: 2026-07-29 16:30
VLAI
Summary
Easy!Appointments disable_booking_message rendered as raw HTML on public booking page — Stored XSS
Details

Summary

Easy!Appointments allows administrators to define a custom "booking disabled" message through the booking settings page. That value is stored in the disable_booking_message setting via a rich-text editor and later passed directly to the public booking_message view without escaping or sanitization:

<p><?= vars('message_text') ?></p>

An authenticated administrator can store HTML or JavaScript in this field, enable disabled-booking mode, and trigger stored XSS in every unauthenticated visitor who opens the public booking page.


Root Cause — Step by Step Code Flow

Step 1 — Rich text editor value stored without sanitization

The booking settings page collects the message value from the Trumbowyg rich-text editor and submits it as raw HTML:

// assets/js/pages/booking_settings.js line 61-92
bookingSettings.push({
    name: 'disable_booking_message',
    value: $disableBookingMessage.trumbowyg('html'),
});

Step 2 — Settings controller saves value verbatim

The backend settings controller persists the submitted value without any HTML sanitization:

// application/controllers/Booking_settings.php line 76-104
$this->settings_model->save($setting);

Step 3 — Public booking controller forwards stored value to view

When booking is disabled, the public booking controller loads the stored message and passes it directly to the view:

// application/controllers/Booking.php line 113-132
$disable_booking_message = setting('disable_booking_message');

html_vars([
    'message_text' => $disable_booking_message,
]);

Step 4 — Public view renders value without escaping

The booking message view emits the value raw using PHP's short echo tag with no escaping:

// application/views/pages/booking_message.php line 10-12
<p><?= vars('message_text') ?></p>

No htmlspecialchars(), no sanitization, no template escaping is applied at any point in this rendering path.


Proof of Concept

Step 1 — Store malicious disabled-booking message as admin:

POST /index.php/booking_settings/save HTTP/1.1
Host: 127.0.0.1:18094
Cookie: <admin-session-cookie>
Content-Type: application/x-www-form-urlencoded

csrf_token=<token>&booking_settings[0][name]=disable_booking&booking_settings[0][value]=1&booking_settings[1][name]=disable_booking_message&booking_settings[1][value]=<img src=x onerror=alert("easyappointments xss by ashrexon")>

Response: 200 OK — settings saved successfully

Step 2 — Unauthenticated visitor opens public booking page:

GET / HTTP/1.1
Host: 127.0.0.1:18094
(no authentication)

Observed response fragment:

<p><img src=x onerror=alert("easyappointments xss by ashrexon")></p>

Observed browser behavior:

alert("easyappointments xss by ashrexon") executes immediately on page load with no authentication required. Confirmed via browser screenshot attached as comment.

Runtime verification result:

admin login ok
settings save ok
payload reflected on public page
PASS

Real World Impact

Easy!Appointments is deployed as a public-facing appointment booking surface for businesses, clinics, and service providers. An administrator can abuse the disabled-booking message — a customer-facing feature intended for maintenance or holiday notices — to plant JavaScript that executes in every visitor's browser when the booking page is disabled. This can be used to:

  • Execute arbitrary JavaScript in visitor browsers on the trusted booking domain
  • Phish visitor credentials or personal information during booking downtime
  • Deface the public booking page during maintenance or outage windows
  • Redirect visitors to attacker-controlled sites

Suggested Fix

Escape the message value before rendering in the view:

// application/views/pages/booking_message.php
<p><?= e(vars('message_text')) ?></p>

Alternatively apply a strict HTML sanitizer (allowing only safe formatting tags, no event handlers or script elements) to the disable_booking_message value before storage or before rendering, to preserve intended rich-text formatting while preventing script injection.


Reporter

Yash Shendge (ashrexon) 2026-05-25

Show details on source website

{
  "affected": [
    {
      "package": {
        "ecosystem": "Packagist",
        "name": "alextselegidis/easyappointments"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "last_affected": "1.5.2"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-52838"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-79"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-07-29T16:30:09Z",
    "nvd_published_at": "2026-07-14T16:17:00Z",
    "severity": "LOW"
  },
  "details": "## Summary\n \nEasy!Appointments allows administrators to define a custom \"booking disabled\" message through the booking settings page. That value is stored in the `disable_booking_message` setting via a rich-text editor and later passed directly to the public `booking_message` view without escaping or sanitization:\n \n```php\n\u003cp\u003e\u003c?= vars(\u0027message_text\u0027) ?\u003e\u003c/p\u003e\n```\n \nAn authenticated administrator can store HTML or JavaScript in this field, enable disabled-booking mode, and trigger stored XSS in every unauthenticated visitor who opens the public booking page.\n \n---\n \n## Root Cause \u2014 Step by Step Code Flow\n \n### Step 1 \u2014 Rich text editor value stored without sanitization\nThe booking settings page collects the message value from the Trumbowyg rich-text editor and submits it as raw HTML:\n \n```javascript\n// assets/js/pages/booking_settings.js line 61-92\nbookingSettings.push({\n    name: \u0027disable_booking_message\u0027,\n    value: $disableBookingMessage.trumbowyg(\u0027html\u0027),\n});\n```\n \n### Step 2 \u2014 Settings controller saves value verbatim\nThe backend settings controller persists the submitted value without any HTML sanitization:\n \n```php\n// application/controllers/Booking_settings.php line 76-104\n$this-\u003esettings_model-\u003esave($setting);\n```\n \n### Step 3 \u2014 Public booking controller forwards stored value to view\nWhen booking is disabled, the public booking controller loads the stored message and passes it directly to the view:\n \n```php\n// application/controllers/Booking.php line 113-132\n$disable_booking_message = setting(\u0027disable_booking_message\u0027);\n \nhtml_vars([\n    \u0027message_text\u0027 =\u003e $disable_booking_message,\n]);\n```\n \n### Step 4 \u2014 Public view renders value without escaping\nThe booking message view emits the value raw using PHP\u0027s short echo tag with no escaping:\n \n```php\n// application/views/pages/booking_message.php line 10-12\n\u003cp\u003e\u003c?= vars(\u0027message_text\u0027) ?\u003e\u003c/p\u003e\n```\n \nNo `htmlspecialchars()`, no sanitization, no template escaping is applied at any point in this rendering path.\n \n---\n \n## Proof of Concept\n \n**Step 1 \u2014 Store malicious disabled-booking message as admin:**\n \n```http\nPOST /index.php/booking_settings/save HTTP/1.1\nHost: 127.0.0.1:18094\nCookie: \u003cadmin-session-cookie\u003e\nContent-Type: application/x-www-form-urlencoded\n \ncsrf_token=\u003ctoken\u003e\u0026booking_settings[0][name]=disable_booking\u0026booking_settings[0][value]=1\u0026booking_settings[1][name]=disable_booking_message\u0026booking_settings[1][value]=\u003cimg src=x onerror=alert(\"easyappointments xss by ashrexon\")\u003e\n```\n \nResponse: `200 OK` \u2014 settings saved successfully\n \n**Step 2 \u2014 Unauthenticated visitor opens public booking page:**\n \n```http\nGET / HTTP/1.1\nHost: 127.0.0.1:18094\n(no authentication)\n```\n \n**Observed response fragment:**\n```html\n\u003cp\u003e\u003cimg src=x onerror=alert(\"easyappointments xss by ashrexon\")\u003e\u003c/p\u003e\n```\n \n**Observed browser behavior:**\n \n`alert(\"easyappointments xss by ashrexon\")` executes immediately on page load with no authentication required. Confirmed via browser screenshot attached as comment.\n \n**Runtime verification result:**\n```\nadmin login ok\nsettings save ok\npayload reflected on public page\nPASS\n```\n \n---\n \n## Real World Impact\n \nEasy!Appointments is deployed as a public-facing appointment booking surface for businesses, clinics, and service providers. An administrator can abuse the disabled-booking message \u2014 a customer-facing feature intended for maintenance or holiday notices \u2014 to plant JavaScript that executes in every visitor\u0027s browser when the booking page is disabled. This can be used to:\n \n- Execute arbitrary JavaScript in visitor browsers on the trusted booking domain\n- Phish visitor credentials or personal information during booking downtime\n- Deface the public booking page during maintenance or outage windows\n- Redirect visitors to attacker-controlled sites\n---\n \n## Suggested Fix\n \nEscape the message value before rendering in the view:\n \n```php\n// application/views/pages/booking_message.php\n\u003cp\u003e\u003c?= e(vars(\u0027message_text\u0027)) ?\u003e\u003c/p\u003e\n```\n \nAlternatively apply a strict HTML sanitizer (allowing only safe formatting tags, no event handlers or script elements) to the `disable_booking_message` value before storage or before rendering, to preserve intended rich-text formatting while preventing script injection.\n \n---\n \n## Reporter\n**Yash Shendge (ashrexon)**\n2026-05-25",
  "id": "GHSA-996f-334j-67g7",
  "modified": "2026-07-29T16:30:09Z",
  "published": "2026-07-29T16:30:09Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/alextselegidis/easyappointments/security/advisories/GHSA-996f-334j-67g7"
    },
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-52838"
    },
    {
      "type": "WEB",
      "url": "https://github.com/alextselegidis/easyappointments/commit/629a0415f54f75556c17f4f5d9c77fda1fdbdeae"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/alextselegidis/easyappointments"
    },
    {
      "type": "WEB",
      "url": "https://github.com/alextselegidis/easyappointments/releases/tag/1.6.0"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:H/PR:H/UI:R/S:C/C:N/I:L/A:N",
      "type": "CVSS_V3"
    }
  ],
  "summary": "Easy!Appointments disable_booking_message rendered as raw HTML on public booking page \u2014 Stored XSS"
}



Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

Forecast uses a logistic model when the trend is rising, or an exponential decay model when the trend is falling. Fitted via linearized least squares.

Sightings

Author Source Type Date Other

Nomenclature

  • Seen: The vulnerability was mentioned, discussed, or observed by the user.
  • Confirmed: The vulnerability has been validated from an analyst's perspective.
  • Published Proof of Concept: A public proof of concept is available for this vulnerability.
  • Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
  • Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
  • Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
  • Not confirmed: The user expressed doubt about the validity of the vulnerability.
  • Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.

Loading…

Detection rules are retrieved from Rulezet.

Loading…

Loading…

Loading…