Vulnerability: Cross-Site Scripting LinkedIn Oncall through 1.4.0 allows reflected XSS via /query because of mishandling of the "No results found for" message in the search bar.
Vulnerability ID: GHSA-rfw2-x9f8-2f6m
CWE: CWE-79
Severity score: 0
Project: https://github.com/linkedin/oncall
Published: 2021-04-30
File: src/oncall/ui/static/js/oncall.js
Commit message: prevent potential XSS from searchbar results (#342)

* prevent potential XSS from searchbar results

* use built in handlebars expression escaping

* use handlebars encodeURIComponent
Commit author: Diego Cepeda

Vulnerable version (before fix):
 var oncall = {               },               footer: function(resp){                 if (teamsCt > typeaheadLimit) {                   return '<div class="tt-see-all"><a href="/query/' + resp.query + '/teams" data-navigo> See all ' + teamsCt + ' results for teams »</a></div>';                 }               },               empty: function(resp){                 return '<h4> No results found for "' + resp.query + '" </h4>';               }             }           }, var oncall = {               },               footer: function(resp){                 if (servicesCt > typeaheadLimit) {                   return '<div class="tt-see-all"><a href="/query/' + resp.query + '/services" data-navigo> See all ' + servicesCt + ' results for services »</a></div>';                 }               }             } var oncall = {               },               footer: function(resp){                 if (usersCt > typeaheadLimit) {                   return '<div class="tt-see-all"><a href="/query/' + resp.query + '/users" data-navigo> See all ' + usersCt + ' results for users »</a></div>';                 }               }             }

Fixed version (after fix):
 var oncall = {               },               footer: function(resp){                 if (teamsCt > typeaheadLimit) {                   return '<div class="tt-see-all"><a href="/query/' + Handlebars.escapeExpression(encodeURIComponent(resp.query)) + '/teams" data-navigo> See all ' + teamsCt + ' results for teams »</a></div>';                 }               },               empty: function(resp){                 return '<h4> No results found for "' + Handlebars.escapeExpression(resp.query) + '" </h4>';               }             }           }, var oncall = {               },               footer: function(resp){                 if (servicesCt > typeaheadLimit) {                   return '<div class="tt-see-all"><a href="/query/' + Handlebars.escapeExpression(encodeURIComponent(resp.query)) + '/services" data-navigo> See all ' + servicesCt + ' results for services »</a></div>';                 }               }             } var oncall = {               },               footer: function(resp){                 if (usersCt > typeaheadLimit) {                   return '<div class="tt-see-all"><a href="/query/' + Handlebars.escapeExpression(encodeURIComponent(resp.query)) + '/users" data-navigo> See all ' + usersCt + ' results for users »</a></div>';                 }               }             }

Patch:
patch: @@ -579,11 +579,11 @@ var oncall = {
               },
               footer: function(resp){
                 if (teamsCt > typeaheadLimit) {
-                  return '<div class="tt-see-all"><a href="/query/' + resp.query + '/teams" data-navigo> See all ' + teamsCt + ' results for teams »</a></div>';
+                  return '<div class="tt-see-all"><a href="/query/' + Handlebars.escapeExpression(encodeURIComponent(resp.query)) + '/teams" data-navigo> See all ' + teamsCt + ' results for teams »</a></div>';
                 }
               },
               empty: function(resp){
-                return '<h4> No results found for "' + resp.query + '" </h4>';
+                return '<h4> No results found for "' + Handlebars.escapeExpression(resp.query) + '" </h4>';
               }
             }
           },
@@ -604,7 +604,7 @@ var oncall = {
               },
               footer: function(resp){
                 if (servicesCt > typeaheadLimit) {
-                  return '<div class="tt-see-all"><a href="/query/' + resp.query + '/services" data-navigo> See all ' + servicesCt + ' results for services »</a></div>';
+                  return '<div class="tt-see-all"><a href="/query/' + Handlebars.escapeExpression(encodeURIComponent(resp.query)) + '/services" data-navigo> See all ' + servicesCt + ' results for services »</a></div>';
                 }
               }
             }
@@ -626,7 +626,7 @@ var oncall = {
               },
               footer: function(resp){
                 if (usersCt > typeaheadLimit) {
-                  return '<div class="tt-see-all"><a href="/query/' + resp.query + '/users" data-navigo> See all ' + usersCt + ' results for users »</a></div>';
+                  return '<div class="tt-see-all"><a href="/query/' + Handlebars.escapeExpression(encodeURIComponent(resp.query)) + '/users" data-navigo> See all ' + usersCt + ' results for users »</a></div>';
                 }
               }
             }
path: src/oncall/ui/static/js/oncall.js