Security Tip: What Can We Learn from CommonMark's XSS?

[Tip #111] The recently patched XSS in CommonMark's Attributes extension offers an interesting look at what happens when two different features conflict, one being a security feature, the other a knowingly vulnerable extension.

Security Tip: What Can We Learn from CommonMark's XSS?

This week, a Cross-Site Scripting (XSS) vulnerability in the CommonMark package from The League of Extraordinary Packages caught my eye. This is the package Laravel uses for it's Str::markdown() helpers, and we've talked about it before.

To save you figuring out which of those links to click on, here are the details from the advisory:

Cross-site scripting (XSS) vulnerability in the Attributes extension of the league/commonmark library (versions 1.5.0 through 2.6.x) allows remote attackers to insert malicious JavaScript calls into HTML.

The league/commonmark library provides configuration options such as html_input: 'strip' and allow_unsafe_links: false to mitigate cross-site scripting (XSS) attacks by stripping raw HTML and disallowing unsafe links. However, when the Attributes Extension is enabled, it introduces a way for users to inject arbitrary HTML attributes into elements via Markdown syntax using curly braces.

As a result, even with the secure configuration shown above, an attacker can inject dangerous attributes into applications using this extension via a payload such as:

![](){onerror=alert(1)}

Which results in the following HTML:

<p><img onerror="alert(1)" src="" alt="" /></p>

Which causes the JS to execute immediately on page load.

As is noted in the advisory, the vulnerability only affects the AttributesExtension extension, which is not used by Laravel's helpers, so you should only be vulnerable to this if you've manually configured CommonMark to include this extension. Although I did notice that Tighten's Jigsaw does include it by default.

If you're using this extension (or the CommonMark package in general), then you should go run composer update to get the patched version!

If you can't run an update, either disable AttributesExtension for untrusted users, or add some sanitising with somethign like HTMLPurifier or Symfony Sanitizer.

💡
While you're at it, run composer audit to check for any other vulns that need patching!

Why are you mentioning this?

I don't normally announce minor vulnerabilities like this, but what caught my eye with this is how it came about.

The CommonMark spec specifically allows inline HTML, meaning XSS will be rendered unescaped when it's included in a block of Markdown:

> use League\CommonMark\CommonMarkConverter;

> $converter = new CommonMarkConverter();
> echo $converter->convert('<script>alert("Hello XSS!");</script>');

<script>alert("Hello XSS!");</script>

Raw CommonMark rendering XSS.

To add some protections against this, the CommonMark package added the html_input attribute (and others):

> $converter = new CommonMarkConverter(['html_input' => 'escape']);
> echo $converter->convert('<script>alert("Hello XSS!");</script>');

&lt;script&gt;alert("Hello XSS!");&lt;/script&gt;

CommonMark escaping XSS.

Problem, solved right?

CommonMark also supports extensions, and a common need when building complex Markdown documents is the ability to inject custom HTML attributes. Hence the AttributesExtension:

> use League\CommonMark\Environment\Environment;
> use League\CommonMark\Extension\Attributes\AttributesExtension;
> use League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension;
> use League\CommonMark\MarkdownConverter;

> $environment = new Environment();
> $environment->addExtension(new CommonMarkCoreExtension());
> $environment->addExtension(new AttributesExtension());
> $converter = new MarkdownConverter($environment);

> echo $converter->convert('This is *red*{style="color: red"}.');

<p>This is <em style="color: red">red</em>.</p>

CommonMark supporting custom attributes.

By default, this screams "Insecure! Do not use with user input!", but now we have an extension which is by default insecure, and a global setting that is supposed to make the converter secure (i.e. html_input).

There is a conflict of interest here, and the outcome is uncertain:

> $environment = new Environment(['html_input' => 'escape']);
> $environment->addExtension(new CommonMarkCoreExtension());
> $environment->addExtension(new AttributesExtension());
> $converter = new MarkdownConverter($environment);

> echo $converter->convert('This is *red*{onerror=alert(1)}. <script>alert("Hello XSS!");</script>');

<p>This is <em onerror="alert(1)">red</em>. &lt;script&gt;alert(&quot;Hello XSS!&quot;);&lt;/script&gt;</p>

CommonMark allowing injected XSS but escaping extra HTML.

In this specific case, CommonMark chose to treat this as a vulnerability and implemented an allowlist of attributes and explicitly blocked on* attributes, but they could just have easily have made the two options incompatable.

My takeaway here is to consider how different aspects of your application interact together.

This was a case of two related but different features, one an intentional security feature (html_input), and the other a knowingly insecure extension, that combined to leave a vulnerability wide open. Assumptions were made that the security feature would apply to the insecure extension.

Don't make those assumptions in your own apps.


If you found this security tip useful, subscribe to get weekly Security Tips straight to your inbox. Upgrade to a premium subscription for exclusive monthly In Depth articles, or drop a coin in the tip jar to show your support.

When was the last time you had a penetration test? Book a Laravel Security Audit and Penetration Test, or a budget-friendly Security Review!

You can also connect with me on Bluesky, or other socials, and check out Practical Laravel Security, my interactive course designed to boost your Laravel security skills.