diff --git a/tez-ui/src/main/webapp/app/models/vertex.js b/tez-ui/src/main/webapp/app/models/vertex.js
index a5904ef717..7926f2e41e 100644
--- a/tez-ui/src/main/webapp/app/models/vertex.js
+++ b/tez-ui/src/main/webapp/app/models/vertex.js
@@ -151,7 +151,8 @@ export default AMTimelineModel.extend({
description: Ember.computed("dag.vertices", "name", function () {
try {
let vertex = this.get("dag.vertices").findBy("vertexName", this.get("name"));
- return JSON.parse(vertex.userPayloadAsText).desc;
+ let desc = JSON.parse(vertex.userPayloadAsText).desc;
+ return desc ? Ember.Handlebars.Utils.escapeExpression(desc) : undefined;
}catch(e) {}
}),
diff --git a/tez-ui/src/main/webapp/app/serializers/timeline.js b/tez-ui/src/main/webapp/app/serializers/timeline.js
index 52bba51be4..ecd43421a4 100644
--- a/tez-ui/src/main/webapp/app/serializers/timeline.js
+++ b/tez-ui/src/main/webapp/app/serializers/timeline.js
@@ -23,6 +23,9 @@ import LoaderSerializer from './loader';
function getDiagnostics(source) {
var diagnostics = Ember.get(source, 'otherInfo.diagnostics') || "";
+ // Escape HTML entities before adding formatting markup
+ diagnostics = Ember.Handlebars.Utils.escapeExpression(diagnostics);
+
diagnostics = diagnostics.replace(/\t/g, " ");
diagnostics = diagnostics.replace(/\[/g, "
» ");
diagnostics = diagnostics.replace(/\]/g, "
");
diff --git a/tez-ui/src/main/webapp/tests/unit/models/vertex-test.js b/tez-ui/src/main/webapp/tests/unit/models/vertex-test.js
index c5c6cd29fa..1cfe02adfb 100644
--- a/tez-ui/src/main/webapp/tests/unit/models/vertex-test.js
+++ b/tez-ui/src/main/webapp/tests/unit/models/vertex-test.js
@@ -96,3 +96,54 @@ test('description test', function(assert) {
assert.equal(model.get("description"), testDesc);
});
});
+
+test('description escapes HTML to prevent XSS', function(assert) {
+ let testVertexName = "TestVertexName",
+ model = this.subject({
+ name: testVertexName
+ });
+
+ Ember.run(function () {
+ // Script injection must be escaped
+ model.set("dag", Ember.Object.create({
+ vertices: [{
+ vertexName: testVertexName,
+ userPayloadAsText: JSON.stringify({
+ desc: ""
+ })
+ }]
+ }));
+ assert.equal(model.get("description"), "<script>alert(1)</script>");
+
+ // IMG onerror injection must be escaped
+ model.set("dag", Ember.Object.create({
+ vertices: [{
+ vertexName: testVertexName,
+ userPayloadAsText: JSON.stringify({
+ desc: "
"
+ })
+ }]
+ }));
+ assert.equal(model.get("description"), "<img src=x onerror=alert(1)>");
+
+ // Ampersands are escaped
+ model.set("dag", Ember.Object.create({
+ vertices: [{
+ vertexName: testVertexName,
+ userPayloadAsText: JSON.stringify({
+ desc: "a & b"
+ })
+ }]
+ }));
+ assert.equal(model.get("description"), "a & b");
+
+ // Null/undefined desc returns undefined
+ model.set("dag", Ember.Object.create({
+ vertices: [{
+ vertexName: testVertexName,
+ userPayloadAsText: JSON.stringify({})
+ }]
+ }));
+ assert.equal(model.get("description"), undefined);
+ });
+});
diff --git a/tez-ui/src/main/webapp/tests/unit/serializers/timeline-test.js b/tez-ui/src/main/webapp/tests/unit/serializers/timeline-test.js
index 1aaabac2fb..8a0b145924 100644
--- a/tez-ui/src/main/webapp/tests/unit/serializers/timeline-test.js
+++ b/tez-ui/src/main/webapp/tests/unit/serializers/timeline-test.js
@@ -42,3 +42,42 @@ test('extractArrayPayload test', function(assert) {
assert.equal(serializer.extractArrayPayload(testPayload), testPayload.entities);
});
+
+test('getDiagnostics escapes HTML', function(assert) {
+ let serializer = this.subject(),
+ mapper = serializer.maps.diagnostics;
+
+ // Empty/missing diagnostics
+ assert.equal(mapper({}), "");
+ assert.equal(mapper({otherInfo: {}}), "");
+
+ // Script injection must be escaped
+ assert.equal(
+ mapper({otherInfo: {diagnostics: ""}}),
+ "<script>alert(1)</script>"
+ );
+
+ // IMG onerror injection must be escaped
+ assert.equal(
+ mapper({otherInfo: {diagnostics: "
"}}),
+ "<img src=x onerror=alert(1)>"
+ );
+
+ // Tab formatting still works after escaping
+ assert.equal(
+ mapper({otherInfo: {diagnostics: "\tindented"}}),
+ " indented"
+ );
+
+ // Bracket formatting still works after escaping
+ assert.equal(
+ mapper({otherInfo: {diagnostics: "[section]"}}),
+ "» section
"
+ );
+
+ // Ampersands in diagnostics are escaped
+ assert.equal(
+ mapper({otherInfo: {diagnostics: "a & b"}}),
+ "a & b"
+ );
+});