diff --git a/pom.xml b/pom.xml index dbd8b1c2f3..1b25d56789 100644 --- a/pom.xml +++ b/pom.xml @@ -74,6 +74,7 @@ 4.4 2.22.0 3.19.0 + 1.15.0 5.9.0 3.2.0 4.9.3 @@ -328,6 +329,11 @@ commons-lang3 ${commons-lang3.version} + + org.apache.commons + commons-text + ${commons-text.version} + io.netty netty-all diff --git a/tez-dag/pom.xml b/tez-dag/pom.xml index f4c9ed8867..af9b4235cd 100644 --- a/tez-dag/pom.xml +++ b/tez-dag/pom.xml @@ -111,6 +111,10 @@ org.apache.commons commons-lang3 + + org.apache.commons + commons-text + commons-codec commons-codec diff --git a/tez-dag/src/main/java/org/apache/tez/dag/app/web/AMWebController.java b/tez-dag/src/main/java/org/apache/tez/dag/app/web/AMWebController.java index e547dad520..b99bd69986 100644 --- a/tez-dag/src/main/java/org/apache/tez/dag/app/web/AMWebController.java +++ b/tez-dag/src/main/java/org/apache/tez/dag/app/web/AMWebController.java @@ -36,6 +36,7 @@ import javax.servlet.http.HttpServletResponse; +import org.apache.commons.text.StringEscapeUtils; import org.apache.hadoop.security.UserGroupInformation; import org.apache.hadoop.yarn.webapp.Controller; import org.apache.hadoop.yarn.webapp.MimeType; @@ -909,7 +910,8 @@ public void render() { } } - private void render(PrintWriter pw) { + @VisibleForTesting + void render(PrintWriter pw) { pw.write(""); pw.write(""); pw.write(""); @@ -921,12 +923,17 @@ private void render(PrintWriter pw) { "

To enable tracking url pointing to Tez UI, set the config " + TezConfiguration.TEZ_HISTORY_URL_BASE + " in the tez-site.xml.

"); } else { + // historyUrl is derived from a submitter-supplied AM configuration + // property (tez.tez-ui.history-url.base). Escape it before splicing + // into the HTML attribute and the inline JS string literal so a + // value like ' or " cannot break out and run script in the browser + // of whoever opens the AM tracking URL. pw.write("

Redirecting to Tez UI

.

If you are not redirected shortly, click " + - "here

" + "here

" ); pw.write(""); + "window.location.replace('" + StringEscapeUtils.escapeEcmaScript(historyUrl) + "');" + + "}, 0); "); } pw.write(""); pw.write(""); diff --git a/tez-dag/src/test/java/org/apache/tez/dag/app/web/TestAMWebController.java b/tez-dag/src/test/java/org/apache/tez/dag/app/web/TestAMWebController.java index e5a8c0e0a7..7acfdf631a 100644 --- a/tez-dag/src/test/java/org/apache/tez/dag/app/web/TestAMWebController.java +++ b/tez-dag/src/test/java/org/apache/tez/dag/app/web/TestAMWebController.java @@ -35,6 +35,8 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import java.io.PrintWriter; +import java.io.StringWriter; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -890,4 +892,46 @@ private void verifySingleAttemptResult(TaskAttempt mockTask, Map assertEquals(Float.toString(mockTask.getProgress()), taskResult.get("progress")); } + private static String renderStaticAMView(String historyUrl) { + AMWebController.StaticAMView view = new AMWebController.StaticAMView(); + view.historyUrl = historyUrl; + StringWriter out = new StringWriter(); + view.render(new PrintWriter(out)); + return out.toString(); + } + + @Test + public void testStaticAMViewEscapesHistoryUrl() { + // Angle brackets and quotes in the submitter-controlled history url + // must not break out of the rendered attribute or JS literal. + String rendered = renderStaticAMView("http://ui/'\">"); + + // Double-quoted href: " < > & are entity-encoded, a bare ' is harmless. + assertTrue(rendered.contains( + ""), + "Unexpected href encoding: " + rendered); + // Single-quoted JS literal: ' and " are backslash-escaped and / becomes \/, + // so the injected cannot close the enclosing script element. + assertTrue(rendered.contains( + "window.location.replace('http:\\/\\/ui\\/\\'\\\">", -1).length - 1, + "Only the page's own may appear: " + rendered); + } + + @Test + public void testStaticAMViewEscapesJsLineTerminators() { + // U+2028/U+2029 are JS line terminators: raw, they break the string + // literal pre-ES2019 and the redirect never runs. Built numerically — + // a source \\u2028 escape is decoded by javac before string parsing. + String ls = String.valueOf((char) 0x2028); + String ps = String.valueOf((char) 0x2029); + String rendered = renderStaticAMView("http://ui/" + ls + ps + "x"); + String script = rendered.substring(rendered.indexOf("