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

garydgregory pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/commons-lang.git


The following commit(s) were added to refs/heads/master by this push:
     new 792a5569e StringEscapeUtils.escapeHtml4/3 apostrophe gap: 
single-quoted and unquoted HTML attribute contexts are trivially breakable, and 
the javadoc discloses the gap only as an HTML4 entity-legality footnote (f022).
792a5569e is described below

commit 792a5569e5bc41dec9ae669c9b8fe03049215abe
Author: Gary Gregory <[email protected]>
AuthorDate: Sat Sep 5 15:06:49 2026 -0400

    StringEscapeUtils.escapeHtml4/3 apostrophe gap: single-quoted and
    unquoted HTML attribute contexts are trivially breakable, and the
    javadoc discloses the gap only as an HTML4 entity-legality footnote
    (f022).
---
 src/changes/changes.xml                            |  1 +
 .../apache/commons/lang3/StringEscapeUtils.java    | 27 ++++++++++++++--------
 .../commons/lang3/text/translate/EntityArrays.java |  5 +++-
 .../commons/lang3/StringEscapeUtilsTest.java       | 20 +++++++++++++++-
 .../lang3/text/translate/EntityArraysTest.java     |  7 ++++++
 5 files changed, 49 insertions(+), 11 deletions(-)

diff --git a/src/changes/changes.xml b/src/changes/changes.xml
index 846bae61c..9c23421e4 100644
--- a/src/changes/changes.xml
+++ b/src/changes/changes.xml
@@ -271,6 +271,7 @@ java.lang.NullPointerException: Cannot invoke
     <action                   type="fix" dev="ggregory" due-to="Gary 
Gregory">BitField long-mask fields silently mis-answer int/short/byte 
accessors: shifts masked mod 32, and sign extension makes above-bit-31 flags 
read as set on any negative holder (f019).</action>
     <action                   type="fix" dev="ggregory" due-to="Gary 
Gregory">ClassUtils.getClass() name normalization (whitespace stripping + 
dot-to-dollar rewriting) defeats host-side class-name string checks 
(f020).</action>
     <action                   type="fix" dev="ggregory" due-to="Gary 
Gregory">Fraction.getFraction(String) throws undeclared ArithmeticException on 
crafted numeric strings ('9999999999.5', '1/0', mixed-number overflow) 
(f021).</action>
+    <action                   type="fix" dev="ggregory" due-to="Gary 
Gregory">StringEscapeUtils.escapeHtml4/3 apostrophe gap: single-quoted and 
unquoted HTML attribute contexts are trivially breakable, and the javadoc 
discloses the gap only as an HTML4 entity-legality footnote (f022).</action>
     <!-- ADD -->
     <action                   type="add" dev="ggregory" due-to="Gary 
Gregory">Add JavaVersion.JAVA_27.</action>
     <action                   type="add" dev="ggregory" due-to="Gary 
Gregory">Add SystemUtils.IS_JAVA_27.</action>
diff --git a/src/main/java/org/apache/commons/lang3/StringEscapeUtils.java 
b/src/main/java/org/apache/commons/lang3/StringEscapeUtils.java
index cf35295a5..c45e3469b 100644
--- a/src/main/java/org/apache/commons/lang3/StringEscapeUtils.java
+++ b/src/main/java/org/apache/commons/lang3/StringEscapeUtils.java
@@ -175,8 +175,8 @@ public int translate(final CharSequence input, final int 
index, final Writer out
     @Deprecated
     public static final CharSequenceTranslator ESCAPE_XML =
         new AggregateTranslator(
-            new LookupTranslator(EntityArrays.BASIC_ESCAPE()),
-            new LookupTranslator(EntityArrays.APOS_ESCAPE())
+            new LookupTranslator(EntityArrays.APOS_ESCAPE()),
+            new LookupTranslator(EntityArrays.BASIC_ESCAPE())
         );
 
     /**
@@ -190,8 +190,8 @@ public int translate(final CharSequence input, final int 
index, final Writer out
      */
     public static final CharSequenceTranslator ESCAPE_XML10 =
         new AggregateTranslator(
-            new LookupTranslator(EntityArrays.BASIC_ESCAPE()),
             new LookupTranslator(EntityArrays.APOS_ESCAPE()),
+            new LookupTranslator(EntityArrays.BASIC_ESCAPE()),
             new LookupTranslator(
                     new String[][] {
                             { "\u0000", StringUtils.EMPTY },
@@ -242,8 +242,8 @@ public int translate(final CharSequence input, final int 
index, final Writer out
      */
     public static final CharSequenceTranslator ESCAPE_XML11 =
         new AggregateTranslator(
-            new LookupTranslator(EntityArrays.BASIC_ESCAPE()),
             new LookupTranslator(EntityArrays.APOS_ESCAPE()),
+            new LookupTranslator(EntityArrays.BASIC_ESCAPE()),
             new LookupTranslator(
                     new String[][] {
                             { "\u0000", StringUtils.EMPTY },
@@ -476,7 +476,13 @@ public static final String escapeEcmaScript(final String 
input) {
 
     /**
      * Escapes the characters in a {@link String} using HTML entities.
-     * <p>Supports only the HTML 3.0 entities.</p>
+     *
+     * <p>
+     * Supports only the HTML 3.0 entities. Apostrophes are escaped as the 
numeric reference {@code &#39;}.
+     * </p>
+     * <p>
+     * HTML attribute values must be quoted. This method does not escape all 
characters that delimit unquoted attribute values.
+     * </p>
      *
      * @param input  The {@link String} to escape, may be null
      * @return A new escaped {@link String}, {@code null} if null string input
@@ -497,10 +503,13 @@ public static final String escapeHtml3(final String 
input) {
      * <p>
      * {@code &amp;quot;bread&amp;quot; &amp;amp; &amp;quot;butter&amp;quot;}.
      * </p>
-     *
-     * <p>Supports all known HTML 4.0 entities, including funky accents.
-     * Note that the commonly used apostrophe escape character (&amp;apos;)
-     * is not a legal entity and so is not supported).</p>
+     * <p>
+     * Supports all known HTML 4.0 entities, including funky accents.
+     * Apostrophes are escaped as the numeric reference {@code &#39;} because 
{@code &apos;} is not a legal HTML 4.0 entity.
+     * </p>
+     * <p>
+     * HTML attribute values must be quoted. This method does not escape all 
characters that delimit unquoted attribute values.
+     * </p>
      *
      * @param input  The {@link String} to escape, may be null
      * @return A new escaped {@link String}, {@code null} if null string input
diff --git 
a/src/main/java/org/apache/commons/lang3/text/translate/EntityArrays.java 
b/src/main/java/org/apache/commons/lang3/text/translate/EntityArrays.java
index b021204c7..cf8a51c06 100644
--- a/src/main/java/org/apache/commons/lang3/text/translate/EntityArrays.java
+++ b/src/main/java/org/apache/commons/lang3/text/translate/EntityArrays.java
@@ -330,6 +330,7 @@ public class EntityArrays {
     private static final String[][] HTML40_EXTENDED_UNESCAPE = 
invert(HTML40_EXTENDED_ESCAPE);
 
     private static final String[][] BASIC_ESCAPE = {
+        {"'", "&#39;"},   // ' - apostrophe
         {"\"", "&quot;"}, // " - double-quote
         {"&", "&amp;"},   // & - ampersand
         {"<", "&lt;"},    // < - less-than
@@ -375,7 +376,9 @@ public static String[][] APOS_UNESCAPE() {
     /**
      * Mapping to escape the basic XML and HTML character entities.
      *
-     * Namely: {@code " & < >}
+     * <p>
+     * Namely: {@code " ' & < >}. The apostrophe is escaped as the numeric 
reference {@code &#39;}.
+     * </p>
      *
      * @return The mapping table.
      */
diff --git a/src/test/java/org/apache/commons/lang3/StringEscapeUtilsTest.java 
b/src/test/java/org/apache/commons/lang3/StringEscapeUtilsTest.java
index 4af256cd8..132885b3b 100644
--- a/src/test/java/org/apache/commons/lang3/StringEscapeUtilsTest.java
+++ b/src/test/java/org/apache/commons/lang3/StringEscapeUtilsTest.java
@@ -53,7 +53,7 @@ class StringEscapeUtilsTest extends AbstractLangTest {
         {"quotes", "&quot;bread&quot; &amp; butter", "\"bread\" & butter"},
         {"final character only", "greater than &gt;", "greater than >"},
         {"first character only", "&lt; less than", "< less than"},
-        {"apostrophe", "Huntington's chorea", "Huntington's chorea"},
+        {"apostrophe", "Huntington&#39;s chorea", "Huntington's chorea"},
         {"languages", "English,Fran&ccedil;ais,\u65E5\u672C\u8A9E (nihongo)", 
"English,Fran\u00E7ais,\u65E5\u672C\u8A9E (nihongo)"},
         {"8-bit ascii shouldn't number-escape", "\u0080\u009F", 
"\u0080\u009F"},
     };
@@ -216,6 +216,24 @@ void testEscapeHtml() throws IOException {
         }
     }
 
+    @Test
+    void testEscapeHtmlApostrophes() throws IOException {
+        final String input = "' autofocus onfocus=alert(1) x='";
+        final String expected = "&#39; autofocus onfocus=alert(1) x=&#39;";
+        assertEquals(expected, StringEscapeUtils.escapeHtml3(input));
+        assertEquals(expected, StringEscapeUtils.escapeHtml4(input));
+        for (final CharSequenceTranslator translator : new 
CharSequenceTranslator[] {
+                StringEscapeUtils.ESCAPE_HTML3, StringEscapeUtils.ESCAPE_HTML4 
}) {
+            final StringWriter writer = new StringWriter();
+            translator.translate(input, writer);
+            assertEquals(expected, writer.toString());
+            assertEquals("&#39;&#39;", translator.translate("''"));
+            assertEquals("&amp;#39;", translator.translate("&#39;"));
+        }
+        assertEquals(input, StringEscapeUtils.unescapeHtml3(expected));
+        assertEquals(input, StringEscapeUtils.unescapeHtml4(expected));
+    }
+
     /**
      * Tests // https://issues.apache.org/jira/browse/LANG-480
      */
diff --git 
a/src/test/java/org/apache/commons/lang3/text/translate/EntityArraysTest.java 
b/src/test/java/org/apache/commons/lang3/text/translate/EntityArraysTest.java
index a9cc9e3a1..91fdebd1c 100644
--- 
a/src/test/java/org/apache/commons/lang3/text/translate/EntityArraysTest.java
+++ 
b/src/test/java/org/apache/commons/lang3/text/translate/EntityArraysTest.java
@@ -17,6 +17,7 @@
 
 package org.apache.commons.lang3.text.translate;
 
+import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
 import java.util.HashSet;
@@ -31,6 +32,12 @@
 @Deprecated
 class EntityArraysTest extends AbstractLangTest {
 
+    @Test
+    void testBasicApostrophe() {
+        assertEquals("&#39;", new 
LookupTranslator(EntityArrays.BASIC_ESCAPE()).translate("'"));
+        assertEquals("'", new 
LookupTranslator(EntityArrays.BASIC_UNESCAPE()).translate("&#39;"));
+    }
+
     @Test
     void testConstructorExists() {
         new EntityArrays();

Reply via email to