Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -921,17 +921,71 @@ private void render(PrintWriter pw) {
"<p>To enable tracking url pointing to Tez UI, set the config <b>" +
TezConfiguration.TEZ_HISTORY_URL_BASE + "</b> in the tez-site.xml.</p>");
} 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("<h1>Redirecting to Tez UI</h1>. <p>If you are not redirected shortly, click " +
"<a href='" + historyUrl + "'><b>here</b></a></p>"
"<a href=\"" + escapeHtmlAttribute(historyUrl) + "\"><b>here</b></a></p>"
);
pw.write("<script type='text/javascript'>setTimeout(function() { " +
"window.location.replace('" + historyUrl + "');" +
"}, 0); </script>");
"window.location.replace('" + escapeJsString(historyUrl) + "');" +
"}, 0); </script>");
}
pw.write("</body>");
pw.write("</html>");
pw.flush();
}

static String escapeHtmlAttribute(String s) {
StringBuilder sb = new StringBuilder(s.length() + 16);
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
switch (c) {
case '&': sb.append("&amp;"); break;
case '<': sb.append("&lt;"); break;
case '>': sb.append("&gt;"); break;
case '"': sb.append("&quot;"); break;
case '\'': sb.append("&#39;"); break;
default: sb.append(c);
}
}
return sb.toString();
}

static String escapeJsString(String s) {
StringBuilder sb = new StringBuilder(s.length() + 16);
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
switch (c) {
case '\\': sb.append("\\\\"); break;
case '\'': sb.append("\\'"); break;
case '"': sb.append("\\\""); break;
case '\n': sb.append("\\n"); break;
case '\r': sb.append("\\r"); break;
case '\t': sb.append("\\t"); break;
case '\b': sb.append("\\b"); break;
case '\f': sb.append("\\f"); break;
case '<': sb.append("\\u003c"); break;
case '>': sb.append("\\u003e"); break;
case '&': sb.append("\\u0026"); break;
case '/': sb.append("\\/"); break;
// JS line terminators: illegal raw inside a string literal pre-ES2019.
// Written numerically because a \\u2028 source escape would be decoded
// by javac's lexer into a real line break and split this file.
case 0x2028: sb.append("\\u2028"); break;
case 0x2029: sb.append("\\u2029"); break;
default:
if (c < 0x20) {
sb.append(String.format("\\u%04x", (int) c));
} else {
sb.append(c);
}
}
}
return sb.toString();
}
}

@VisibleForTesting
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -890,4 +890,42 @@ private void verifySingleAttemptResult(TaskAttempt mockTask, Map<String, String>
assertEquals(Float.toString(mockTask.getProgress()), taskResult.get("progress"));
}

@Test
public void testStaticAMViewEscapesHistoryUrl() {
// Angle brackets and quotes in the submitter-controlled history url
// must not appear literally in the rendered attribute or JS literal.
String malicious = "http://ui/'\"><script>alert(1)</script>";
String htmlEscaped = AMWebController.StaticAMView.escapeHtmlAttribute(malicious);
assertFalse(htmlEscaped.contains("<script"),
"Angle brackets must be HTML-escaped: " + htmlEscaped);
assertFalse(htmlEscaped.contains("\""),
"Double quote must be HTML-escaped: " + htmlEscaped);
assertFalse(htmlEscaped.contains("'"),
"Single quote must be HTML-escaped: " + htmlEscaped);

String jsEscaped = AMWebController.StaticAMView.escapeJsString(malicious);
// No bare single quote may remain — every one must be preceded by \.
String stripped = jsEscaped.replace("\\'", "");
assertFalse(stripped.contains("'"),
"Single quote must be JS-escaped: " + jsEscaped);
// The closing </script> that would end the enclosing script element
// must be neutralised too.
assertFalse(jsEscaped.contains("</script>"),
"Closing </script> must be neutralised: " + jsEscaped);
}

@Test
public void testEscapeJsStringEscapesLineTerminators() {
// 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 escaped = AMWebController.StaticAMView.escapeJsString("http://ui/" + ls + ps + "x");
assertFalse(escaped.contains(ls), "U+2028 must be escaped: " + escaped);
assertFalse(escaped.contains(ps), "U+2029 must be escaped: " + escaped);
assertTrue(escaped.contains("\\u2028"), "Expected \\u2028 escape: " + escaped);
assertTrue(escaped.contains("\\u2029"), "Expected \\u2029 escape: " + escaped);
}

}
Loading