This is an automated email from the ASF dual-hosted git repository.

tballison pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tika.git


The following commit(s) were added to refs/heads/main by this push:
     new 628543bd44 escape link, image and table cell text in markdown handler 
(#2891)
628543bd44 is described below

commit 628543bd4423bddb3c818c4d79fffb56f92f2f86
Author: Dexter.k <[email protected]>
AuthorDate: Mon Jun 22 21:20:38 2026 +0000

    escape link, image and table cell text in markdown handler (#2891)
---
 .../apache/tika/sax/ToMarkdownContentHandler.java  |  32 ++++++-
 .../tika/sax/ToMarkdownContentHandlerTest.java     | 102 +++++++++++++++++++++
 2 files changed, 129 insertions(+), 5 deletions(-)

diff --git 
a/tika-core/src/main/java/org/apache/tika/sax/ToMarkdownContentHandler.java 
b/tika-core/src/main/java/org/apache/tika/sax/ToMarkdownContentHandler.java
index c92ae55549..13a54dcbd6 100644
--- a/tika-core/src/main/java/org/apache/tika/sax/ToMarkdownContentHandler.java
+++ b/tika-core/src/main/java/org/apache/tika/sax/ToMarkdownContentHandler.java
@@ -150,7 +150,8 @@ public class ToMarkdownContentHandler extends 
DefaultHandler {
             case "img":
                 String alt = atts.getValue("alt");
                 String src = atts.getValue("src");
-                write("![" + (alt != null ? alt : "") + "](" + (src != null ? 
src : "") + ")");
+                write("![" + escapeMarkdown(alt != null ? alt : "") + "](" +
+                        formatLinkDestination(src) + ")");
                 break;
             case "ul":
             case "ol":
@@ -282,9 +283,8 @@ public class ToMarkdownContentHandler extends 
DefaultHandler {
                 break;
             case "a":
                 if (linkText != null) {
-                    String text = linkText.toString();
-                    String href = linkHref != null ? linkHref : "";
-                    write("[" + text + "](" + href + ")");
+                    write("[" + escapeMarkdown(linkText.toString()) + "](" +
+                            formatLinkDestination(linkHref) + ")");
                     linkText = null;
                     linkHref = null;
                 }
@@ -343,7 +343,7 @@ public class ToMarkdownContentHandler extends 
DefaultHandler {
             case "th":
             case "td":
                 if (tableDepth == 1 && currentRow != null && currentCell != 
null) {
-                    currentRow.add(currentCell.toString().trim());
+                    currentRow.add(escapeTableCell(currentCell.toString()));
                     currentCell = null;
                 }
                 break;
@@ -525,6 +525,28 @@ public class ToMarkdownContentHandler extends 
DefaultHandler {
         return sb.toString();
     }
 
+    private static String escapeTableCell(String text) {
+        // a newline ends a GFM table row and a pipe ends a cell; fold the 
former and
+        // escape the latter (with the other markers) so cell content can't 
break out
+        // of its column or row.
+        return escapeMarkdown(text.replaceAll("[\\r\\n]+", " ").trim());
+    }
+
+    private static String formatLinkDestination(String url) {
+        if (url == null || url.isEmpty()) {
+            return "";
+        }
+        // angle brackets and line breaks can't appear in any markdown 
destination
+        String cleaned = url.replaceAll("[\\u0000-\\u001f<>]", "");
+        // a space or paren would otherwise close the bare (url) form early, 
so wrap
+        // those in <>, where spaces and parens are allowed
+        if (cleaned.indexOf(' ') >= 0 || cleaned.indexOf('(') >= 0
+                || cleaned.indexOf(')') >= 0) {
+            return "<" + cleaned + ">";
+        }
+        return cleaned;
+    }
+
     private static String repeatChar(char c, int count) {
         StringBuilder sb = new StringBuilder(count);
         for (int i = 0; i < count; i++) {
diff --git 
a/tika-core/src/test/java/org/apache/tika/sax/ToMarkdownContentHandlerTest.java 
b/tika-core/src/test/java/org/apache/tika/sax/ToMarkdownContentHandlerTest.java
index 298d11c70f..c4bcf6323d 100644
--- 
a/tika-core/src/test/java/org/apache/tika/sax/ToMarkdownContentHandlerTest.java
+++ 
b/tika-core/src/test/java/org/apache/tika/sax/ToMarkdownContentHandlerTest.java
@@ -697,6 +697,108 @@ public class ToMarkdownContentHandlerTest {
                 "Expected to find '" + needle + "' in: " + haystack);
     }
 
+    @Test
+    public void testLinkTextEscaped() throws Exception {
+        ToMarkdownContentHandler handler = new ToMarkdownContentHandler();
+        handler.startDocument();
+
+        startElement(handler, "p");
+        startElement(handler, "a", "href", "https://good.example";);
+        chars(handler, "x](https://evil.example)");
+        endElement(handler, "a");
+        endElement(handler, "p");
+
+        handler.endDocument();
+
+        String result = handler.toString();
+        // the ] in the link text must be escaped so it cannot close the link 
early
+        assertTrue(result.contains("x\\]"), result);
+        assertFalse(result.contains("[x](https://evil.example)"), result);
+    }
+
+    @Test
+    public void testLinkHrefNoBreakout() throws Exception {
+        ToMarkdownContentHandler handler = new ToMarkdownContentHandler();
+        handler.startDocument();
+
+        startElement(handler, "p");
+        startElement(handler, "a", "href", "https://evil.example) text");
+        chars(handler, "click");
+        endElement(handler, "a");
+        endElement(handler, "p");
+
+        handler.endDocument();
+
+        String result = handler.toString();
+        // a destination with spaces/parens must be wrapped so it cannot 
terminate (..) early
+        assertTrue(result.contains("](<https://evil.example";), result);
+        assertFalse(result.contains("](https://evil.example)"), result);
+    }
+
+    @Test
+    public void testImageAltAndSrcEscaped() throws Exception {
+        ToMarkdownContentHandler handler = new ToMarkdownContentHandler();
+        handler.startDocument();
+
+        startElement(handler, "p");
+        AttributesImpl atts = new AttributesImpl();
+        atts.addAttribute("", "alt", "alt", "CDATA", "a]b");
+        atts.addAttribute("", "src", "src", "CDATA", "https://evil.example) 
x");
+        startElement(handler, "img", atts);
+        endElement(handler, "img");
+        endElement(handler, "p");
+
+        handler.endDocument();
+
+        String result = handler.toString();
+        assertTrue(result.contains("a\\]b"), result);
+        assertTrue(result.contains("](<https://evil.example";), result);
+        assertFalse(result.contains("](https://evil.example)"), result);
+    }
+
+    @Test
+    public void testTableCellPipeEscaped() throws Exception {
+        ToMarkdownContentHandler handler = new ToMarkdownContentHandler();
+        handler.startDocument();
+
+        startElement(handler, "table");
+        startElement(handler, "tr");
+        startElement(handler, "td");
+        chars(handler, "a|b");
+        endElement(handler, "td");
+        startElement(handler, "td");
+        chars(handler, "c");
+        endElement(handler, "td");
+        endElement(handler, "tr");
+        endElement(handler, "table");
+
+        handler.endDocument();
+
+        String result = handler.toString();
+        // a pipe inside a cell must be escaped so it does not inject an extra 
column
+        assertTrue(result.contains("| a\\|b | c |"), result);
+    }
+
+    @Test
+    public void testTableCellNewlineFolded() throws Exception {
+        ToMarkdownContentHandler handler = new ToMarkdownContentHandler();
+        handler.startDocument();
+
+        startElement(handler, "table");
+        startElement(handler, "tr");
+        startElement(handler, "td");
+        chars(handler, "a\nb");
+        endElement(handler, "td");
+        endElement(handler, "tr");
+        endElement(handler, "table");
+
+        handler.endDocument();
+
+        String result = handler.toString();
+        // a newline inside a cell must not terminate the table row
+        assertTrue(result.contains("| a b |"), result);
+    }
+
     @Test
     public void testHandlerTypeParsingMarkdown() {
         assertEquals(BasicContentHandlerFactory.HANDLER_TYPE.MARKDOWN,

Reply via email to