All Posts programming Fixing a Sneaky XSS Vulnerability in Hugo: Inside the Commit That Makes Markdown Rendering Safer

Fixing a Sneaky XSS Vulnerability in Hugo: Inside the Commit That Makes Markdown Rendering Safer

· 885 words · 5 minute read
Go in production: interesting moments ▹

Hugo is one of the fastest and most popular static site generators out there. Millions of blogs, documentation sites, and portfolios run on it. But like any software that turns Markdown into HTML, Hugo has to be extremely careful about security.

On April 3, 2026, the Hugo team merged a small but critical commit: 479fe6c titled “Fix potential content XSS by escaping dangerous URLs in links and images”.

This commit closes a real-world security hole (now tracked as CVE-2026-35166 ). Let’s break it down step by step in plain English — so you understand what the problem was, why it mattered, how the fix works, and what it means for you as a Hugo user.

What Was the Problem? (The “What”) 🔗

Imagine you write a simple Markdown link:

[Click me](javascript:alert(1))

Or, sneakier, an encoded version that looks innocent:

[Click me](javascript:alert(1))

(j is just the HTML entity for the letter “j” — so it still becomes javascript: when decoded.)

Before this commit, Hugo’s default Markdown renderer (Goldmark) would happily turn that into:

<a href="javascript:alert(1)">Click me</a>

When a visitor’s browser saw this, it would execute the JavaScript — popping up an alert (or worse: stealing cookies, redirecting users, or running malware). This is called content XSS (Cross-Site Scripting) because the malicious code came from the site’s own content, not an external script.

The same thing happened with images:

![evil](javascript:alert(3))

And even auto-links like <javascript:alert(2)>.

This wasn’t a bug in every Hugo site — only sites that render untrusted or user-generated Markdown were at risk. But in 2026, many Hugo sites pull content from CMS tools, APIs, or guest authors. One sneaky link could have been disastrous.

Visualizing the Attack 🔗

Here’s how a typical XSS attack flows:

cross site scripting explained source: secpoint

(And a more detailed non-persistent version for good measure:)

Non-Persistent XSS source: acunetix

The attacker injects the bad URL into your content. Hugo renders it. The visitor’s browser executes it. Game over.

Why Did This Happen? (The “Why”) 🔗

Hugo uses the excellent Goldmark library for Markdown-to-HTML conversion. Goldmark is fast and standards-compliant, but its default link and image renderers assumed that any URL in the Markdown was “safe enough” to output directly.

Hugo already had some protections (you could enable “unsafe” mode or use other renderers), but it didn’t catch obfuscated dangerous schemes like javascript:, data:, or vbscript:. Attackers love these tricks because they bypass simple string checks.

The Hugo maintainers spotted this during a security review and decided it was time to close the gap — proactively.

How the Fix Works (The “How” – Technical Deep Dive) 🔗

The commit touches just two files in markup/goldmark/:

  1. render_hooks.go – The heart of the change
  2. goldmark_integration_test.go – A new test to prove it works

The Key Magic: Three Simple Steps in the Render Hooks 🔗

The team updated three rendering functions (renderLinkDefault, renderImageDefault, and renderAutoLinkDefault). Here’s the new logic in plain terms:

  1. URL-Escape first – Turn any weird characters into safe form using util.URLEscape().
  2. Check if it’s dangerous – Hugo now calls html.IsDangerousURL() (a helper that knows about javascript:, data:, etc.).
  3. Double-escape if needed – Even if it slips through, EscapeHTML() is applied (sometimes twice) so the browser treats it as plain text.

Here’s the before vs. after for images (simplified):

Old code (vulnerable):

// Just output whatever was in the Markdown
_, _ = w.Write(util.EscapeHTML(n.Destination))

New code (secure):

dest := util.URLEscape(n.Destination, true)
if r.Unsafe || !html.IsDangerousURL(dest) {
    _, _ = w.Write(util.EscapeHTML(dest))  // Safe!
}

Same pattern for links (with an extra EscapeHTML for extra safety) and auto-links.

Real Test Added in the Commit 🔗

The team didn’t just fix it — they proved it with a crystal-clear integration test:

Link: [Click me](&#106;avascript:alert(1))
AutoLink: <javascript:alert(2)>
Image: ![alt](&#106;avascript:alert(3))

After the fix, Hugo outputs something like:

Link: <a href="javascript:alert(1)">Click me</a>  <!-- but actually escaped safely -->
<!-- The test checks that "alert(1)" appears as literal text, not executable code -->

The test asserts that dangerous parts show up as harmless strings like alert(1)" instead of running JavaScript. Perfect.

By the End of This Post: The Goal Is Achieved 🔗

What was broken → Dangerous URLs in Markdown links and images could execute JavaScript.
Why it mattered → Real risk of content XSS on any Hugo site handling external content.
How it was fixed → Smart escaping + IsDangerousURL checks in Goldmark render hooks.
Proof → New test + CVE fix.

This commit is a textbook example of defense in depth. Hugo was already fast and flexible; now it’s a little bit safer too.

What Should You Do? 🔗

  • Update Hugo to the latest version.
  • If you run a multi-author blog or import content from anywhere, double-check your security headers (CSP is still your friend!).
  • Relax knowing that links like [Click me](javascript:alert(1)) are now safely neutralized.

Security isn’t glamorous, but commits like this one keep the web a little safer — one escaped javascript: at a time.

Happy (and secure) Hugo building! 🚀

Thanks to the Hugo maintainers for shipping this quietly but effectively.

I hope you enjoyed reading this post as much as I enjoyed writing it. If you know a person who can benefit from this information, send them a link of this post. If you want to get notified about new posts, follow me on YouTube , Twitter (x) , LinkedIn , and GitHub .

Go in production: interesting moments ▹