diff --git a/pkgs/markdown/CHANGELOG.md b/pkgs/markdown/CHANGELOG.md index 5a4988e49..6cf1848fa 100644 --- a/pkgs/markdown/CHANGELOG.md +++ b/pkgs/markdown/CHANGELOG.md @@ -12,6 +12,7 @@ * Optimize email autolink regex parsing in `AutolinkExtensionSyntax` by bounding quantifiers to RFC limits, improving performance on inputs with long sequences of dots or alphanumeric characters. +* Escape image description text when assigning it to the `alt` attribute. ## 7.3.1 diff --git a/pkgs/markdown/lib/src/inline_syntaxes/image_syntax.dart b/pkgs/markdown/lib/src/inline_syntaxes/image_syntax.dart index 031adf915..345869aa2 100644 --- a/pkgs/markdown/lib/src/inline_syntaxes/image_syntax.dart +++ b/pkgs/markdown/lib/src/inline_syntaxes/image_syntax.dart @@ -24,15 +24,19 @@ class ImageSyntax extends LinkSyntax { element.attributes['src'] = normalizeLinkDestination( escapePunctuation(destination), ); - element.attributes['alt'] = children.map((node) { - // See https://spec.commonmark.org/0.30/#image-description. - // An image description may contain links. Fetch text from the alt - // attribute if this nested link is an image. - if (node is Element && node.tag == 'img') { - return node.attributes['alt']; - } - return node.textContent; - }).join(); + final alt = children + .map((node) { + // See https://spec.commonmark.org/0.30/#image-description. + // An image description may contain links. Fetch text from the alt + // attribute if this nested link is an image. + if (node case Element(tag: 'img', attributes: {'alt': final alt})) { + return alt; + } + return node.textContent; + }) + .nonNulls + .join(); + element.attributes['alt'] = escapeAttributeCharactersValue(alt); if (title != null && title.isNotEmpty) { element.attributes['title'] = normalizeLinkTitle(title); } diff --git a/pkgs/markdown/lib/src/util.dart b/pkgs/markdown/lib/src/util.dart index 724879ea1..81f003980 100644 --- a/pkgs/markdown/lib/src/util.dart +++ b/pkgs/markdown/lib/src/util.dart @@ -23,6 +23,17 @@ String escapeHtml(String html, {bool escapeApos = true}) => HtmlEscape( String escapeHtmlAttribute(String text) => const HtmlEscape(HtmlEscapeMode.attribute).convert(text); +/// Escapes raw `"` (double quote), `'` (single quote), `<` (less than) and +/// `>` (greater than) characters. +/// +/// Unlike [escapeHtmlAttribute], this does not escape `&` (ampersand) to +/// avoid double-escaping already-escaped HTML entities. +String escapeAttributeCharactersValue(String text) => text + .replaceAll('"', '"') + .replaceAll("'", ''') + .replaceAll('<', '<') + .replaceAll('>', '>'); + /// "Normalizes" a link label, according to the [CommonMark spec]. /// /// [CommonMark spec] https://spec.commonmark.org/0.30/#link-label diff --git a/pkgs/markdown/test/markdown_test.dart b/pkgs/markdown/test/markdown_test.dart index 787066124..23335190a 100644 --- a/pkgs/markdown/test/markdown_test.dart +++ b/pkgs/markdown/test/markdown_test.dart @@ -146,6 +146,32 @@ void main() async { ''', ); + validateCore( + 'Image alt attribute breakout XSS', + '![](bad)', + '

<b x=" onerror=alert(1) y=">

\n', + inlineSyntaxes: [InlineHtmlSyntax()], + ); + + validateCore( + 'Image alt attribute escaping ampersand and quotes', + '![a & " b](bad)', + '

a & " b

\n', + ); + + validateCore( + 'Image alt attribute double-encoded entity', + '![&amp;](bad)', + '

&amp;

\n', + ); + + validateCore( + 'Image alt attribute with raw HTML tag under GFM', + '![foo bar](bad)', + '

foo <a> bar

\n', + inlineSyntaxes: [InlineHtmlSyntax()], + ); + validateCore( 'Unicode ellipsis as punctuation', '''