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("