Vulnerability: Cross-site Scripting (XSS) - Reflected in GitHub repository rtxteam/rtx prior to checkpoint_2022-05-18.
Vulnerability ID: CVE-2022-1806
CWE: CWE-79
Severity score: 2.9
Project: https://github.com/rtxteam/rtx
Published: 2022-05-20
File: code/autocomplete/server.py
Commit message: remove some cruft and implement a sanitizer for the client-supplied callback function name
Commit author: Eric Deutsch

Vulnerable version (before fix):
 import sys import rtxcomplete import traceback  #class MainHandler(tornado.web.RequestHandler): #    def get(self): #        self.write("Hello, world") #print __file__  root = os.path.dirname(os.path.abspath(__file__)) rtxcomplete.load() #conn = sqlite3.connect('dict.db') #conn.enable_load_extension(True) #conn.load_extension("./spellfix") #cursor = conn.cursor()  class autoSearch(tornado.web.RequestHandler):      def get(self, arg,word=None):         #print "match auto"         try:             limit = self.get_argument("limit")             word = self.get_argument("word")             callback = self.get_argument("callback") #jsonp              result = rtxcomplete.prefix(word,limit)              result = callback+"("+json.dumps(result)+");" #jsonp             #result = json.dumps(result) #typeahead                          self.write(result)              def get(self, arg,word=None):         try:             limit = self.get_argument("limit")             word = self.get_argument("word")             callback = self.get_argument("callback")             #print word             #cursor.execute("SELECT word FROM spell WHERE word MATCH \"" + word + "\" LIMIT " + limit)             #cursor.execute("SELECT word FROM spell WHERE word MATCH \"" + word + "*\" LIMIT " + limit) def get(self, arg,word=None):         try:             limit = self.get_argument("limit")             word = self.get_argument("word")             callback = self.get_argument("callback")             #print word             #cursor.execute("SELECT word FROM spell WHERE word MATCH \"" + word + "\" LIMIT " + limit)             #cursor.execute("SELECT word FROM spell WHERE word MATCH \"" + word + "*\" LIMIT " + limit) def get(self, arg,word=None):  class nodesLikeSearch(tornado.web.RequestHandler):     def get(self, arg,word=None):         #try:         if 1 == 1:             limit = self.get_argument("limit")             word = self.get_argument("word")             callback = self.get_argument("callback")             result = rtxcomplete.get_nodes_like(word,limit);             result = callback+"("+json.dumps(result)+");"             self.write(result)         #except:         #    print(sys.exc_info()[:])         #    traceback.print_tb(sys.exc_info()[-1])         #    self.write("error")   class defineSearch(tornado.web.RequestHandler):

Fixed version (after fix):
 import sys import rtxcomplete import traceback import re  root = os.path.dirname(os.path.abspath(__file__)) rtxcomplete.load()   #### Sanitize the client-provided callback function name def sanitize_callback(callback):     if callback is None or not isinstance(callback,str):         return 'autocomplete_callback'     match = re.match(r'([a-zA-Z0-9_]+).*$', callback)     if match:          callback = match.group(1)     else:          callback = 'autocomplete_callback'     return callback   class autoSearch(tornado.web.RequestHandler):      def get(self, arg,word=None):         try:             limit = self.get_argument("limit")             word = self.get_argument("word")             callback = sanitize_callback(self.get_argument("callback"))              result = rtxcomplete.prefix(word,limit)                          result = callback+"("+json.dumps(result)+");"                          self.write(result)              def get(self, arg,word=None):         try:             limit = self.get_argument("limit")             word = self.get_argument("word")             callback = sanitize_callback(self.get_argument("callback"))             #print word             #cursor.execute("SELECT word FROM spell WHERE word MATCH \"" + word + "\" LIMIT " + limit)             #cursor.execute("SELECT word FROM spell WHERE word MATCH \"" + word + "*\" LIMIT " + limit) def get(self, arg,word=None):         try:             limit = self.get_argument("limit")             word = self.get_argument("word")             callback = sanitize_callback(self.get_argument("callback"))             #print word             #cursor.execute("SELECT word FROM spell WHERE word MATCH \"" + word + "\" LIMIT " + limit)             #cursor.execute("SELECT word FROM spell WHERE word MATCH \"" + word + "*\" LIMIT " + limit) def get(self, arg,word=None):  class nodesLikeSearch(tornado.web.RequestHandler):     def get(self, arg,word=None):         try:             limit = self.get_argument("limit")             word = self.get_argument("word")             callback = sanitize_callback(self.get_argument("callback"))             result = rtxcomplete.get_nodes_like(word,limit);             result = callback+"("+json.dumps(result)+");"             self.write(result)         except:             print(sys.exc_info()[:])             traceback.print_tb(sys.exc_info()[-1])             self.write("error")   class defineSearch(tornado.web.RequestHandler):

Patch:
patch: @@ -6,32 +6,35 @@
 import sys
 import rtxcomplete
 import traceback
-
-#class MainHandler(tornado.web.RequestHandler):
-#    def get(self):
-#        self.write("Hello, world")
-#print __file__
+import re
 
 root = os.path.dirname(os.path.abspath(__file__))
 rtxcomplete.load()
-#conn = sqlite3.connect('dict.db')
-#conn.enable_load_extension(True)
-#conn.load_extension("./spellfix")
-#cursor = conn.cursor()
+
+
+#### Sanitize the client-provided callback function name
+def sanitize_callback(callback):
+    if callback is None or not isinstance(callback,str):
+        return 'autocomplete_callback'
+    match = re.match(r'([a-zA-Z0-9_]+).*$', callback)
+    if match:
+         callback = match.group(1)
+    else:
+         callback = 'autocomplete_callback'
+    return callback
+
 
 class autoSearch(tornado.web.RequestHandler):
 
     def get(self, arg,word=None):
-        #print "match auto"
         try:
             limit = self.get_argument("limit")
             word = self.get_argument("word")
-            callback = self.get_argument("callback") #jsonp
+            callback = sanitize_callback(self.get_argument("callback"))
 
             result = rtxcomplete.prefix(word,limit)
-
-            result = callback+"("+json.dumps(result)+");" #jsonp
-            #result = json.dumps(result) #typeahead
+            
+            result = callback+"("+json.dumps(result)+");"
             
             self.write(result)
             
@@ -47,7 +50,7 @@ def get(self, arg,word=None):
         try:
             limit = self.get_argument("limit")
             word = self.get_argument("word")
-            callback = self.get_argument("callback")
+            callback = sanitize_callback(self.get_argument("callback"))
             #print word
             #cursor.execute("SELECT word FROM spell WHERE word MATCH \"" + word + "\" LIMIT " + limit)
             #cursor.execute("SELECT word FROM spell WHERE word MATCH \"" + word + "*\" LIMIT " + limit)
@@ -73,7 +76,7 @@ def get(self, arg,word=None):
         try:
             limit = self.get_argument("limit")
             word = self.get_argument("word")
-            callback = self.get_argument("callback")
+            callback = sanitize_callback(self.get_argument("callback"))
             #print word
             #cursor.execute("SELECT word FROM spell WHERE word MATCH \"" + word + "\" LIMIT " + limit)
             #cursor.execute("SELECT word FROM spell WHERE word MATCH \"" + word + "*\" LIMIT " + limit)
@@ -96,18 +99,17 @@ def get(self, arg,word=None):
 
 class nodesLikeSearch(tornado.web.RequestHandler):
     def get(self, arg,word=None):
-        #try:
-        if 1 == 1:
+        try:
             limit = self.get_argument("limit")
             word = self.get_argument("word")
-            callback = self.get_argument("callback")
+            callback = sanitize_callback(self.get_argument("callback"))
             result = rtxcomplete.get_nodes_like(word,limit);
             result = callback+"("+json.dumps(result)+");"
             self.write(result)
-        #except:
-        #    print(sys.exc_info()[:])
-        #    traceback.print_tb(sys.exc_info()[-1])
-        #    self.write("error")
+        except:
+            print(sys.exc_info()[:])
+            traceback.print_tb(sys.exc_info()[-1])
+            self.write("error")
 
 
 class defineSearch(tornado.web.RequestHandler):
path: code/autocomplete/server.py