Skip to content

Commit 6764b56

Browse files
committed
escape link, image and table cell text in markdown handler
1 parent af28c19 commit 6764b56

2 files changed

Lines changed: 129 additions & 5 deletions

File tree

tika-core/src/main/java/org/apache/tika/sax/ToMarkdownContentHandler.java

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,8 @@ public void startElement(String uri, String localName, String qName, Attributes
150150
case "img":
151151
String alt = atts.getValue("alt");
152152
String src = atts.getValue("src");
153-
write("![" + (alt != null ? alt : "") + "](" + (src != null ? src : "") + ")");
153+
write("![" + escapeMarkdown(alt != null ? alt : "") + "](" +
154+
formatLinkDestination(src) + ")");
154155
break;
155156
case "ul":
156157
case "ol":
@@ -282,9 +283,8 @@ public void endElement(String uri, String localName, String qName) throws SAXExc
282283
break;
283284
case "a":
284285
if (linkText != null) {
285-
String text = linkText.toString();
286-
String href = linkHref != null ? linkHref : "";
287-
write("[" + text + "](" + href + ")");
286+
write("[" + escapeMarkdown(linkText.toString()) + "](" +
287+
formatLinkDestination(linkHref) + ")");
288288
linkText = null;
289289
linkHref = null;
290290
}
@@ -343,7 +343,7 @@ public void endElement(String uri, String localName, String qName) throws SAXExc
343343
case "th":
344344
case "td":
345345
if (tableDepth == 1 && currentRow != null && currentCell != null) {
346-
currentRow.add(currentCell.toString().trim());
346+
currentRow.add(escapeTableCell(currentCell.toString()));
347347
currentCell = null;
348348
}
349349
break;
@@ -525,6 +525,28 @@ private static String escapeMarkdown(String text) {
525525
return sb.toString();
526526
}
527527

528+
private static String escapeTableCell(String text) {
529+
// a newline ends a GFM table row and a pipe ends a cell; fold the former and
530+
// escape the latter (with the other markers) so cell content can't break out
531+
// of its column or row.
532+
return escapeMarkdown(text.replaceAll("[\\r\\n]+", " ").trim());
533+
}
534+
535+
private static String formatLinkDestination(String url) {
536+
if (url == null || url.isEmpty()) {
537+
return "";
538+
}
539+
// angle brackets and line breaks can't appear in any markdown destination
540+
String cleaned = url.replaceAll("[\\u0000-\\u001f<>]", "");
541+
// a space or paren would otherwise close the bare (url) form early, so wrap
542+
// those in <>, where spaces and parens are allowed
543+
if (cleaned.indexOf(' ') >= 0 || cleaned.indexOf('(') >= 0
544+
|| cleaned.indexOf(')') >= 0) {
545+
return "<" + cleaned + ">";
546+
}
547+
return cleaned;
548+
}
549+
528550
private static String repeatChar(char c, int count) {
529551
StringBuilder sb = new StringBuilder(count);
530552
for (int i = 0; i < count; i++) {

tika-core/src/test/java/org/apache/tika/sax/ToMarkdownContentHandlerTest.java

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -697,6 +697,108 @@ private static void assertContains(String needle, String haystack) {
697697
"Expected to find '" + needle + "' in: " + haystack);
698698
}
699699

700+
@Test
701+
public void testLinkTextEscaped() throws Exception {
702+
ToMarkdownContentHandler handler = new ToMarkdownContentHandler();
703+
handler.startDocument();
704+
705+
startElement(handler, "p");
706+
startElement(handler, "a", "href", "https://good.example");
707+
chars(handler, "x](https://evil.example)");
708+
endElement(handler, "a");
709+
endElement(handler, "p");
710+
711+
handler.endDocument();
712+
713+
String result = handler.toString();
714+
// the ] in the link text must be escaped so it cannot close the link early
715+
assertTrue(result.contains("x\\]"), result);
716+
assertFalse(result.contains("[x](https://evil.example)"), result);
717+
}
718+
719+
@Test
720+
public void testLinkHrefNoBreakout() throws Exception {
721+
ToMarkdownContentHandler handler = new ToMarkdownContentHandler();
722+
handler.startDocument();
723+
724+
startElement(handler, "p");
725+
startElement(handler, "a", "href", "https://evil.example) text");
726+
chars(handler, "click");
727+
endElement(handler, "a");
728+
endElement(handler, "p");
729+
730+
handler.endDocument();
731+
732+
String result = handler.toString();
733+
// a destination with spaces/parens must be wrapped so it cannot terminate (..) early
734+
assertTrue(result.contains("](<https://evil.example"), result);
735+
assertFalse(result.contains("](https://evil.example)"), result);
736+
}
737+
738+
@Test
739+
public void testImageAltAndSrcEscaped() throws Exception {
740+
ToMarkdownContentHandler handler = new ToMarkdownContentHandler();
741+
handler.startDocument();
742+
743+
startElement(handler, "p");
744+
AttributesImpl atts = new AttributesImpl();
745+
atts.addAttribute("", "alt", "alt", "CDATA", "a]b");
746+
atts.addAttribute("", "src", "src", "CDATA", "https://evil.example) x");
747+
startElement(handler, "img", atts);
748+
endElement(handler, "img");
749+
endElement(handler, "p");
750+
751+
handler.endDocument();
752+
753+
String result = handler.toString();
754+
assertTrue(result.contains("a\\]b"), result);
755+
assertTrue(result.contains("](<https://evil.example"), result);
756+
assertFalse(result.contains("](https://evil.example)"), result);
757+
}
758+
759+
@Test
760+
public void testTableCellPipeEscaped() throws Exception {
761+
ToMarkdownContentHandler handler = new ToMarkdownContentHandler();
762+
handler.startDocument();
763+
764+
startElement(handler, "table");
765+
startElement(handler, "tr");
766+
startElement(handler, "td");
767+
chars(handler, "a|b");
768+
endElement(handler, "td");
769+
startElement(handler, "td");
770+
chars(handler, "c");
771+
endElement(handler, "td");
772+
endElement(handler, "tr");
773+
endElement(handler, "table");
774+
775+
handler.endDocument();
776+
777+
String result = handler.toString();
778+
// a pipe inside a cell must be escaped so it does not inject an extra column
779+
assertTrue(result.contains("| a\\|b | c |"), result);
780+
}
781+
782+
@Test
783+
public void testTableCellNewlineFolded() throws Exception {
784+
ToMarkdownContentHandler handler = new ToMarkdownContentHandler();
785+
handler.startDocument();
786+
787+
startElement(handler, "table");
788+
startElement(handler, "tr");
789+
startElement(handler, "td");
790+
chars(handler, "a\nb");
791+
endElement(handler, "td");
792+
endElement(handler, "tr");
793+
endElement(handler, "table");
794+
795+
handler.endDocument();
796+
797+
String result = handler.toString();
798+
// a newline inside a cell must not terminate the table row
799+
assertTrue(result.contains("| a b |"), result);
800+
}
801+
700802
@Test
701803
public void testHandlerTypeParsingMarkdown() {
702804
assertEquals(BasicContentHandlerFactory.HANDLER_TYPE.MARKDOWN,

0 commit comments

Comments
 (0)