When both the metadata and completeHTMLDocument options are enabled,
showdown parses YAML-style frontmatter and builds a full HTML document.
The metadata parser in src/subParsers/makehtml/metadata.js escapes &
and " in the metadata values, but doesn't touch < or >:
content = content
.replace(/&/g, '&')
.replace(/"/g, '"');
Then in src/subParsers/makehtml/completeHTMLDocument.js, the title
value is inserted directly into <title> tags:
title = '<title>' + globals.metadata.parsed.title + '</title>\n';
Since < and > aren't escaped, you can close the <title> element and
inject arbitrary HTML:
---
title: </title><script>alert(document.cookie)</script>
---
# Hello
Showdown produces:
<!DOCTYPE HTML>
<html>
<head>
<title></title><script>alert(document.cookie)</script></title>
<meta charset="utf-8">
</head>
<body>
<h1 id="hello">Hello</h1>
</body>
</html>
The browser parses </title> as closing the title element, then
executes the <script> block.
This requires a specific combination of options (metadata: true +
completeHTMLDocument: true), which limits the attack surface. But
any application using showdown to render user-supplied markdown into
complete HTML pages is vulnerable.
When both the metadata and completeHTMLDocument options are enabled,
showdown parses YAML-style frontmatter and builds a full HTML document.
The metadata parser in src/subParsers/makehtml/metadata.js escapes &
and " in the metadata values, but doesn't touch < or >:
Then in src/subParsers/makehtml/completeHTMLDocument.js, the title
value is inserted directly into <title> tags:
Since < and > aren't escaped, you can close the <title> element and
inject arbitrary HTML:
Showdown produces:
The browser parses </title> as closing the title element, then
executes the <script> block.
This requires a specific combination of options (metadata: true +
completeHTMLDocument: true), which limits the attack surface. But
any application using showdown to render user-supplied markdown into
complete HTML pages is vulnerable.